@@ -358,22 +358,36 @@ def onConfigProfilesCommand(self, evt):
358358
359359def safeAppExit ():
360360 """
361- Ensures the app is exited by all the top windows being destroyed
361+ Ensures the app is exited by all the top windows being destroyed.
362+ wx objects that don't inherit from wx.Window (eg sysTrayIcon, Menu) need to be manually destroyed.
362363 """
363364
365+ import brailleViewer
366+ brailleViewer .destroyBrailleViewer ()
367+
368+ # wx.Windows destroy child Windows automatically but wx.Menu and TaskBarIcon don't inherit from wx.Window.
369+ # They must be manually destroyed when exiting the app.
370+ # Note: this doesn't consistently clean them from the tray and appears to be a wx issue. (#12286, #12238)
371+ log .debug (f"destroying system tray icon and menu" )
372+
373+ mainFrame .sysTrayIcon .menu .Destroy ()
374+ mainFrame .sysTrayIcon .RemoveIcon ()
375+ mainFrame .sysTrayIcon .Destroy ()
376+
364377 for window in wx .GetTopLevelWindows ():
365378 if isinstance (window , wx .Dialog ) and window .IsModal ():
366- log .info (f"ending modal { window } during exit process" )
379+ log .debug (f"ending modal { window } during exit process" )
367380 wx .CallAfter (window .EndModal , wx .ID_CLOSE_ALL )
368381 if isinstance (window , MainFrame ):
369- log .info (f"destroying main frame during exit process" )
382+ log .debug (f"destroying main frame during exit process" )
370383 # the MainFrame has EVT_CLOSE bound to the ExitDialog
371384 # which calls this function on exit, so destroy this window
372385 wx .CallAfter (window .Destroy )
373386 else :
374- log .info (f"closing window { window } during exit process" )
387+ log .debug (f"closing window { window } during exit process" )
375388 wx .CallAfter (window .Close )
376389
390+
377391class SysTrayIcon (wx .adv .TaskBarIcon ):
378392
379393 def __init__ (self , frame ):
@@ -582,27 +596,7 @@ def wx_CallAfter_wrapper(func, *args, **kwargs):
582596 wx .CallAfter = wx_CallAfter_wrapper
583597
584598def terminate ():
585- import brailleViewer
586- brailleViewer .destroyBrailleViewer ()
587-
588- for instance , state in gui .SettingsDialog ._instances .items ():
589- if state is gui .SettingsDialog ._DIALOG_DESTROYED_STATE :
590- log .error (
591- "Destroyed but not deleted instance of settings dialog exists: {!r}" .format (instance )
592- )
593- else :
594- log .debug ("Exiting NVDA with an open settings dialog: {!r}" .format (instance ))
595599 global mainFrame
596- # This is called after the main loop exits because WM_QUIT exits the main loop
597- # without destroying all objects correctly and we need to support WM_QUIT.
598- # Therefore, any request to exit should exit the main loop.
599- safeAppExit ()
600- # #4460: We need another iteration of the main loop
601- # so that everything (especially the TaskBarIcon) is cleaned up properly.
602- # ProcessPendingEvents doesn't seem to work, but MainLoop does.
603- # Because the top window gets destroyed,
604- # MainLoop thankfully returns pretty quickly.
605- wx .GetApp ().MainLoop ()
606600 mainFrame = None
607601
608602def showGui ():
0 commit comments