Custom collapsable list, mouse events not propagated to parent

I’m writing todo-list application and need a list with collapsible items. I know about wx.CollapsiblePane and wx.lib.agw.foldpanelbar.FoldPanelBar, but neither let’s you customize the collapsed item label in a complex way. I want to have the simple todo title together with a clickable checkbox as the collapsed view and, upon clicking on the checkboxe’s label, it expands to show a note, a deadline date, etc. I’ve created a protype using static text, but even that one is not working. Using the checkboxes will only make it more complicated by adding more mouse event listeners.

If you just use StaticText windows in ItemPanel (comented out), it works. Why does it not work for self defined panel classes?

import wx

PADDING = 10

class App(wx.App):
    def OnInit(self):
        self.data = [
            {'title':'1st', 'content':'First'},
            {'title':'2nd', 'content':'Second'},
            {'title':'3rd', 'content':'Third'},
        ]
        frame = ListFrame()
        frame.Show()
        self.SetTopWindow(frame)
        return True

class ListFrame(wx.Frame):
    def __init__(self, title="List", size=(250, 500)):
        wx.Frame.__init__(self, None, -1, title=title, size=size)
        panel = wx.Panel(self)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(wx.StaticText(panel, label='List Title'), flag=wx.EXPAND|wx.ALL, border=PADDING)
        sizer.Add(wx.StaticLine(panel, style=wx.LI_HORIZONTAL), flag=wx.EXPAND)
        sizer.Add(ListPanel(panel), proportion=1, flag=wx.EXPAND)
        panel.SetSizer(sizer)

class ListPanel(wx.ScrolledWindow):
    def __init__(self, parent):
        super().__init__(parent)
        self.data = wx.GetApp().data
        self.SetScrollRate(0,5)
        self.sizer = wx.BoxSizer(wx.VERTICAL)
        for item in self.data:
            item_panel = ItemPanel(self, item)
            self.sizer.Add(item_panel, flag=wx.ALIGN_LEFT|wx.LEFT|wx.RIGHT|wx.BOTTOM, border=PADDING)
        self.SetSizer(self.sizer)

class ItemPanel(wx.Control):
    def __init__(self, parent, item):
        self.parent = parent
        super().__init__(self.parent)
        self.collapsed = True
        self.item = item

        self.sizer = wx.BoxSizer(wx.VERTICAL)

        #self.panel_collapsed = wx.StaticText(self, label='> '+item['title'])
        #self.panel_expanded = wx.StaticText(self, label='v '+item['title']+'\n\t'+item['content'])
        self.panel_collapsed = ItemPanelCollapsed(self)
        self.panel_expanded = ItemPanelExpanded(self)

        self.panel_collapsed.Bind(wx.EVT_LEFT_UP, self.OnToggleView)
        self.panel_expanded.Bind(wx.EVT_LEFT_UP, self.OnToggleView)
        #self.Bind(wx.EVT_LEFT_UP, self.OnToggleView)

        self.sizer.Add(self.panel_collapsed, proportion=1, flag=wx.EXPAND)
        self.sizer.Add(self.panel_expanded, proportion=1, flag=wx.EXPAND)
        self.sizer.Hide(self.panel_expanded)
        self.SetSizer(self.sizer)

    def OnToggleView(self, event):
        if self.collapsed:
            self.sizer.Hide(self.panel_collapsed)
            self.sizer.Show(self.panel_expanded)
        else:
            self.sizer.Hide(self.panel_expanded)
            self.sizer.Show(self.panel_collapsed)
        self.collapsed = not self.collapsed
        self.sizer.Layout()
        self.parent.sizer.Layout()

class ItemPanelCollapsed(wx.Control):
    def __init__(self, parent):
        super().__init__(parent, style=wx.BORDER_NONE)
        sizer = wx.BoxSizer(wx.VERTICAL)
        #sizer.Add(wx.CheckBox(self, label=parent.item['title']), proportion=1, flag=wx.EXPAND)
        sizer.Add(wx.StaticText(self, label='> '+parent.item['title']), proportion=1, flag=wx.EXPAND)
        self.SetSizer(sizer)

class ItemPanelExpanded(wx.Control):
    def __init__(self, parent):
        super().__init__(parent, style=wx.BORDER_NONE)
        sizer = wx.BoxSizer(wx.VERTICAL)
        #sizer.Add(wx.CheckBox(self, label=parent.item['title']), proportion=1, flag=wx.EXPAND)
        sizer.Add(wx.StaticText(self, label='v '+parent.item['title']), proportion=1, flag=wx.EXPAND)
        sizer.Add(wx.StaticText(self, label=parent.item['content']), proportion=1, flag=wx.EXPAND)
        self.SetSizer(sizer)

if __name__ == '__main__':
    app = App(False)
    app.MainLoop()

I’ve also tried binding the mouse event to the ItemPanel (as comented out) instead of the child windows, but then it didn’t work again. So I guess it has something to do with how the mouse events are propagated up the chain. So I also tried letting all my custom classes inherit from wx.Panel or wx.Window instead, but that didn’t change the behaviour.

Optional: Since I’m new to wxPython, it would be kind, if you could maybe answer these related style questions

  1. Is this the correct way of achieving the task? Should I rather be modifying the ItemPanel directly instead of hiding/unhiding nested elements?
  2. If this method is somewhat right, whats the best way to “tricke up” the layout changes from ItemPane.OnToggleView? Calling all parent.sizer.Layout()s up the chain (imagining even more nesting) seems somewhat cumbersome…
  3. Is this the right way to do Model/View separation? Having the data in App.data and accessing it from the windows using wx.GetApp(). How do I automatically update the views when App.data changes?
  4. What’s the right way to define a global style? I’ve used a module wide constant PADDING, but maybe putting it into the App and accessing it via wx.GetApp() would be better? But then basically every window calls this function?
  5. What’s the correct class to inherit from in this case? I went with wx.Panel for the “high-level” elements and wx.Control for more elementary widgets. I don’t really understand the difference between wx.Window, wx.Panel, wx.control (that’s why I called it ListPanel when it’s inheriting from wx.ScrolledWindow…)

Look at the class hierarchy for the event classes. wx.MouseEvent is not a wx.CommandEvent so they are sent only to the widget where the event happened, and do not propagate to parent widgets. See self.Bind vs. self.button.Bind