Skip to content

AsyncBind with popup menu: Exception: object must be a wx.Window #32

@luckydonald

Description

@luckydonald
     def CreatePopupMenu(self, event=None):
        menu = wx.Menu()
        item_open = menu.Append(wx.ID_ANY, "Open App")
        item_about = menu.Append(wx.ID_ANY, "About…")
        item_settings = menu.Append(wx.ID_SETUP, "Settings…")
        item_exit = menu.Append(wx.ID_EXIT, "Exit")

        self.Bind(wx.EVT_MENU, self.on_open_app, item_open)
        self.Bind(wx.EVT_MENU, self.on_about, item_about)

        AsyncBind(wx.EVT_MENU, self.on_open_settings, item_settings, self)
        self.Bind(wx.EVT_MENU, self.on_exit, item_exit)
        # parent will do self.PopupMenu(menu)
        return menu
        
     async def on_open_settings(self, parent=None, event=None):
        print("Would you like to open the settings dialog?")
        dialog = SettingsDialog(self.parent)
        # dialog.ShowModal()
        await AsyncShowDialog(dialog)
Traceback (most recent call last):
  File "/Users/user/Documents/programming/Python/FaceIt/root/ui.py", line 34, in CreatePopupMenu
    AsyncBind(wx.EVT_MENU, self.on_open_settings, item_settings, self)
  File "/Users/user/Documents/programming/Python/FaceIt/.venv/lib/python3.12/site-packages/wxasync.py", line 109, in AsyncBind
    app.AsyncBind(event, async_callback, object, source=source, id=id, id2=id2)
  File "/Users/user/Documents/programming/Python/FaceIt/.venv/lib/python3.12/site-packages/wxasync.py", line 57, in AsyncBind
    raise Exception("object must be a wx.Window")
Exception: object must be a wx.Window

A code comment there says:

We restrict the object to wx.Windows to be able to cancel the coroutines on EVT_WINDOW_DESTROY, even if wx.Bind works with any wx.EvtHandler

wxasync/src/wxasync.py

Lines 52 to 64 in 3ecfe36

def AsyncBind(self, event_binder, async_callback, object, source=None, id=wx.ID_ANY, id2=wx.ID_ANY):
"""Bind a coroutine to a wx Event. Note that when wx object is destroyed, any coroutine still running will be cancelled automatically.
"""
# We restrict the object to wx.Windows to be able to cancel the coroutines on EVT_WINDOW_DESTROY, even if wx.Bind works with any wx.EvtHandler
if not isinstance(object, wx.Window):
raise Exception("object must be a wx.Window")
if not iscoroutinefunction(async_callback):
raise Exception("async_callback is not a coroutine function")
if object not in self.BoundObjects:
self.BoundObjects[object] = defaultdict(list)
object.Bind(wx.EVT_WINDOW_DESTROY, lambda event: self.OnDestroy(event, object), object)
self.BoundObjects[object][event_binder.typeId].append(async_callback)
object.Bind(event_binder, lambda event: StartCoroutine(async_callback(event.Clone()), object), source=source, id=id, id2=id2)


Possible solution

From my understanding the following could allow a user to use this for tray menus:

-    def AsyncBind(self, event_binder, async_callback, object, source=None, id=wx.ID_ANY, id2=wx.ID_ANY):
+    def AsyncBind(self, event_binder, async_callback, object, source=None, id=wx.ID_ANY, id2=wx.ID_ANY, parent_window: wx.Window | None = None):
        """Bind a coroutine to a wx Event. Note that when wx object is destroyed, any coroutine still running will be cancelled automatically.
        """ 
        # We restrict the object to wx.Windows to be able to cancel the coroutines on EVT_WINDOW_DESTROY, even if wx.Bind works with any wx.EvtHandler
-       if not isinstance(object, wx.Window):
+       if not isinstance(object, wx.Window) and (not parent_window or not isinstance(parent_window, wx.Window)):
-           raise Exception("object must be a wx.Window")
+           raise Exception("object must be a wx.Window, or you need to provide parent_window")
        if not iscoroutinefunction(async_callback):
            raise Exception("async_callback is not a coroutine function")
+      bind_on = object if isinstance(object, wx.Window) else parent_window
        if object not in self.BoundObjects:
            self.BoundObjects[object] = defaultdict(list)
-           object.Bind(wx.EVT_WINDOW_DESTROY, lambda event: self.OnDestroy(event, object), object)
+           bind_on.Bind(wx.EVT_WINDOW_DESTROY, lambda event: self.OnDestroy(event, bind_on), bind_on)
        self.BoundObjects[object][event_binder.typeId].append(async_callback)
-       object.Bind(event_binder, lambda event: StartCoroutine(async_callback(event.Clone()), object), source=source, id=id, id2=id2)
+       bind_on.Bind(event_binder, lambda event: StartCoroutine(async_callback(event.Clone()), object), source=source, id=id, id2=id2)

For my code above, I'd do

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions