@@ -360,22 +360,50 @@ def onConfigProfilesCommand(self, evt):
360360
361361def safeAppExit ():
362362 """
363- Ensures the app is exited by all the top windows being destroyed
363+ Ensures the app is exited by all the top windows being destroyed.
364+ wx objects that don't inherit from wx.Window (eg sysTrayIcon, Menu) need to be manually destroyed.
364365 """
365366
367+ import brailleViewer
368+ brailleViewer .destroyBrailleViewer ()
369+
370+ # prevent race condition with object deletion
371+ # prevent deletion of the object while we work on it.
372+ _SettingsDialog = settingsDialogs .SettingsDialog
373+ nonWeak : typing .Dict [_SettingsDialog , _SettingsDialog ] = dict (_SettingsDialog ._instances )
374+
375+ for instance , state in nonWeak .items ():
376+ if state is _SettingsDialog .DialogState .DESTROYED :
377+ log .error (
378+ "Destroyed but not deleted instance of gui.SettingsDialog exists"
379+ f": { instance .title } - { instance .__class__ .__qualname__ } - { instance } "
380+ )
381+ else :
382+ log .debug ("Exiting NVDA with an open settings dialog: {!r}" .format (instance ))
383+
384+ # wx.Windows destroy child Windows automatically but wx.Menu and TaskBarIcon don't inherit from wx.Window.
385+ # They must be manually destroyed when exiting the app.
386+ # Note: this doesn't consistently clean them from the tray and appears to be a wx issue. (#12286, #12238)
387+ log .debug ("destroying system tray icon and menu" )
388+
389+ mainFrame .sysTrayIcon .menu .Destroy ()
390+ mainFrame .sysTrayIcon .RemoveIcon ()
391+ mainFrame .sysTrayIcon .Destroy ()
392+
366393 for window in wx .GetTopLevelWindows ():
367394 if isinstance (window , wx .Dialog ) and window .IsModal ():
368- log .info (f"ending modal { window } during exit process" )
395+ log .debug (f"ending modal { window } during exit process" )
369396 wx .CallAfter (window .EndModal , wx .ID_CLOSE_ALL )
370397 if isinstance (window , MainFrame ):
371- log .info ( f "destroying main frame during exit process" )
398+ log .debug ( "destroying main frame during exit process" )
372399 # the MainFrame has EVT_CLOSE bound to the ExitDialog
373400 # which calls this function on exit, so destroy this window
374401 wx .CallAfter (window .Destroy )
375402 else :
376- log .info (f"closing window { window } during exit process" )
403+ log .debug (f"closing window { window } during exit process" )
377404 wx .CallAfter (window .Close )
378405
406+
379407class SysTrayIcon (wx .adv .TaskBarIcon ):
380408
381409 def __init__ (self , frame ):
@@ -584,33 +612,7 @@ def wx_CallAfter_wrapper(func, *args, **kwargs):
584612 wx .CallAfter = wx_CallAfter_wrapper
585613
586614def terminate ():
587- import brailleViewer
588- brailleViewer .destroyBrailleViewer ()
589-
590- # prevent race condition with object deletion
591- # prevent deletion of the object while we work on it.
592- _SettingsDialog = settingsDialogs .SettingsDialog
593- nonWeak : typing .Dict [_SettingsDialog , _SettingsDialog ] = dict (_SettingsDialog ._instances )
594-
595- for instance , state in nonWeak .items ():
596- if state is _SettingsDialog .DialogState .DESTROYED :
597- log .error (
598- "Destroyed but not deleted instance of gui.SettingsDialog exists"
599- f": { instance .title } - { instance .__class__ .__qualname__ } - { instance } "
600- )
601- else :
602- log .debug ("Exiting NVDA with an open settings dialog: {!r}" .format (instance ))
603615 global mainFrame
604- # This is called after the main loop exits because WM_QUIT exits the main loop
605- # without destroying all objects correctly and we need to support WM_QUIT.
606- # Therefore, any request to exit should exit the main loop.
607- safeAppExit ()
608- # #4460: We need another iteration of the main loop
609- # so that everything (especially the TaskBarIcon) is cleaned up properly.
610- # ProcessPendingEvents doesn't seem to work, but MainLoop does.
611- # Because the top window gets destroyed,
612- # MainLoop thankfully returns pretty quickly.
613- wx .GetApp ().MainLoop ()
614616 mainFrame = None
615617
616618def showGui ():
0 commit comments