|
47 | 47 | ### Globals |
48 | 48 | mainFrame = None |
49 | 49 | isInMessageBox = False |
50 | | - |
| 50 | +hasAppExited = False |
51 | 51 |
|
52 | 52 | class MainFrame(wx.Frame): |
53 | 53 |
|
@@ -360,22 +360,53 @@ def onConfigProfilesCommand(self, evt): |
360 | 360 |
|
361 | 361 | def safeAppExit(): |
362 | 362 | """ |
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. |
364 | 365 | """ |
365 | 366 |
|
| 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 | + |
366 | 393 | for window in wx.GetTopLevelWindows(): |
367 | 394 | 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") |
369 | 396 | wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL) |
370 | 397 | if isinstance(window, MainFrame): |
371 | | - log.info(f"destroying main frame during exit process") |
| 398 | + log.debug("destroying main frame during exit process") |
372 | 399 | # the MainFrame has EVT_CLOSE bound to the ExitDialog |
373 | 400 | # which calls this function on exit, so destroy this window |
374 | 401 | wx.CallAfter(window.Destroy) |
375 | 402 | else: |
376 | | - log.info(f"closing window {window} during exit process") |
| 403 | + log.debug(f"closing window {window} during exit process") |
377 | 404 | wx.CallAfter(window.Close) |
378 | 405 |
|
| 406 | + global hasAppExited |
| 407 | + hasAppExited = True |
| 408 | + |
| 409 | + |
379 | 410 | class SysTrayIcon(wx.adv.TaskBarIcon): |
380 | 411 |
|
381 | 412 | def __init__(self, frame): |
@@ -584,33 +615,13 @@ def wx_CallAfter_wrapper(func, *args, **kwargs): |
584 | 615 | wx.CallAfter = wx_CallAfter_wrapper |
585 | 616 |
|
586 | 617 | def terminate(): |
587 | | - import brailleViewer |
588 | | - brailleViewer.destroyBrailleViewer() |
| 618 | + global mainFrame |
589 | 619 |
|
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) |
| 620 | + # If MainLoop is terminated through WM_QUIT, such as starting an NVDA instance older than 2021.1, |
| 621 | + # safeAppExit has not been called yet |
| 622 | + if not hasAppExited: |
| 623 | + safeAppExit() |
594 | 624 |
|
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)) |
603 | | - 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() |
614 | 625 | mainFrame = None |
615 | 626 |
|
616 | 627 | def showGui(): |
|
0 commit comments