diff --git a/source/gui/__init__.py b/source/gui/__init__.py index 43b1c80b5d4..ea29f09dc10 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -53,7 +53,12 @@ def __init__(self): style = wx.DEFAULT_FRAME_STYLE ^ wx.MAXIMIZE_BOX ^ wx.MINIMIZE_BOX | wx.FRAME_NO_TASKBAR super(MainFrame, self).__init__(None, wx.ID_ANY, versionInfo.name, size=(1,1), style=style) self.Bind(wx.EVT_CLOSE, self.onExitCommand) + self.sysTrayIcon = SysTrayIcon(self) + # wx destroys child Windows automatically but `wx.adv.TaskBarIcon` is not a window + # so it must be set to be destroyed before destroying our main frame window (#12243) + self.Bind(wx.EVT_WINDOW_DESTROY, self._onDestroy, source=self) + #: The focus before the last popup or C{None} if unknown. #: This is only valid before L{prePopup} is called, #: so it should be used as early as possible in any popup that needs it. @@ -75,6 +80,13 @@ def __init__(self): self.Show() self.Hide() + def _onDestroy(self, evt: wx.EVT_WINDOW_DESTROY): + evt.Skip(skip=False) # blocks other handlers as wxCommandEvents must only be processed by one handler + # wx destroys child Windows automatically but `wx.adv.TaskBarIcon` is not a window + # so it must be set to be destroyed when destroying our main frame window (#12243) + log.debug(f"destroying systray icon") + wx.CallAfter(self.sysTrayIcon.Destroy) # queue the destruction event safely + def prePopup(self): """Prepare for a popup. This should be called before any dialog or menu which should pop up for the user. @@ -358,22 +370,24 @@ def onConfigProfilesCommand(self, evt): def safeAppExit(): """ - Ensures the app is exited by all the top windows being destroyed + Ensures the app is exited by all the top windows being destroyed. + wx objects that don't inherit from wx.Window (eg sysTrayIcon, Menu) need to be manually destroyed. """ for window in wx.GetTopLevelWindows(): if isinstance(window, wx.Dialog) and window.IsModal(): - log.info(f"ending modal {window} during exit process") + log.debug(f"ending modal {window} during exit process") wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL) if isinstance(window, MainFrame): - log.info(f"destroying main frame during exit process") + log.debug(f"destroying main frame during exit process") # the MainFrame has EVT_CLOSE bound to the ExitDialog # which calls this function on exit, so destroy this window wx.CallAfter(window.Destroy) else: - log.info(f"closing window {window} during exit process") + log.debug(f"closing window {window} during exit process") wx.CallAfter(window.Close) + class SysTrayIcon(wx.adv.TaskBarIcon): def __init__(self, frame): @@ -381,7 +395,10 @@ def __init__(self, frame): icon=wx.Icon(ICON_PATH,wx.BITMAP_TYPE_ICO) self.SetIcon(icon, versionInfo.name) - self.menu=wx.Menu() + # wx.Windows destroy child Windows automatically but wx.Menu and TaskBarIcon don't inherit from + # wx.Window. The menu must be manually destroyed when destroying our system tray icon (#12243) + self.menu = wx.Menu() + menu_preferences=self.preferencesMenu=wx.Menu() item = menu_preferences.Append(wx.ID_ANY, # Translators: The label for the menu item to open NVDA Settings dialog. @@ -542,6 +559,13 @@ def __init__(self, frame): self.Bind(wx.adv.EVT_TASKBAR_LEFT_DOWN, self.onActivate) self.Bind(wx.adv.EVT_TASKBAR_RIGHT_DOWN, self.onActivate) + def Destroy(self): + # wx.Windows destroy child Windows automatically but wx.Menu and TaskBarIcon don't inherit from + # wx.Window. The menu must be manually destroyed when destroying our system tray icon (#12243) + log.debug(f"destroying systray menu during exit process") + wx.CallAfter(self.menu.Destroy) + return super().Destroy() + def onActivate(self, evt): mainFrame.prePopup() import appModules.nvda