|
47 | 47 | ### Globals |
48 | 48 | mainFrame = None |
49 | 49 | isInMessageBox = False |
50 | | -hasAppExited = False |
| 50 | + |
51 | 51 |
|
52 | 52 | class MainFrame(wx.Frame): |
53 | 53 |
|
@@ -360,54 +360,22 @@ 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. |
364 | | - wx objects that don't inherit from wx.Window (eg sysTrayIcon, Menu) need to be manually destroyed. |
| 363 | + Ensures the app is exited by all the top windows being destroyed |
365 | 364 | """ |
366 | 365 |
|
367 | | - import brailleViewer |
368 | | - brailleViewer.destroyBrailleViewer() |
369 | | - |
370 | | - app = wx.GetApp() |
371 | | - |
372 | | - # prevent race condition with object deletion |
373 | | - # prevent deletion of the object while we work on it. |
374 | | - _SettingsDialog = settingsDialogs.SettingsDialog |
375 | | - nonWeak: typing.Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances) |
376 | | - |
377 | | - for instance, state in nonWeak.items(): |
378 | | - if state is _SettingsDialog.DialogState.DESTROYED: |
379 | | - log.error( |
380 | | - "Destroyed but not deleted instance of gui.SettingsDialog exists" |
381 | | - f": {instance.title} - {instance.__class__.__qualname__} - {instance}" |
382 | | - ) |
383 | | - else: |
384 | | - log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance)) |
385 | | - |
386 | | - # wx.Windows destroy child Windows automatically but wx.Menu and TaskBarIcon don't inherit from wx.Window. |
387 | | - # They must be manually destroyed when exiting the app. |
388 | | - # Note: this doesn't consistently clean them from the tray and appears to be a wx issue. (#12286, #12238) |
389 | | - log.debug("destroying system tray icon and menu") |
390 | | - app.ScheduleForDestruction(mainFrame.sysTrayIcon.menu) |
391 | | - mainFrame.sysTrayIcon.RemoveIcon() |
392 | | - app.ScheduleForDestruction(mainFrame.sysTrayIcon) |
393 | | - |
394 | 366 | for window in wx.GetTopLevelWindows(): |
395 | 367 | if isinstance(window, wx.Dialog) and window.IsModal(): |
396 | | - log.debug(f"ending modal {window} during exit process") |
| 368 | + log.info(f"ending modal {window} during exit process") |
397 | 369 | wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL) |
398 | 370 | if isinstance(window, MainFrame): |
399 | | - log.debug("destroying main frame during exit process") |
| 371 | + log.info(f"destroying main frame during exit process") |
400 | 372 | # the MainFrame has EVT_CLOSE bound to the ExitDialog |
401 | 373 | # which calls this function on exit, so destroy this window |
402 | | - app.ScheduleForDestruction(window) |
| 374 | + wx.CallAfter(window.Destroy) |
403 | 375 | else: |
404 | | - log.debug(f"closing window {window} during exit process") |
| 376 | + log.info(f"closing window {window} during exit process") |
405 | 377 | wx.CallAfter(window.Close) |
406 | 378 |
|
407 | | - global hasAppExited |
408 | | - hasAppExited = True |
409 | | - |
410 | | - |
411 | 379 | class SysTrayIcon(wx.adv.TaskBarIcon): |
412 | 380 |
|
413 | 381 | def __init__(self, frame): |
@@ -616,13 +584,33 @@ def wx_CallAfter_wrapper(func, *args, **kwargs): |
616 | 584 | wx.CallAfter = wx_CallAfter_wrapper |
617 | 585 |
|
618 | 586 | def terminate(): |
619 | | - global mainFrame |
| 587 | + import brailleViewer |
| 588 | + brailleViewer.destroyBrailleViewer() |
620 | 589 |
|
621 | | - # If MainLoop is terminated through WM_QUIT, such as starting an NVDA instance older than 2021.1, |
622 | | - # safeAppExit has not been called yet |
623 | | - if not hasAppExited: |
624 | | - safeAppExit() |
| 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) |
625 | 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)) |
| 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() |
626 | 614 | mainFrame = None |
627 | 615 |
|
628 | 616 | def showGui(): |
|
0 commit comments