|
5 | 5 | # This file is covered by the GNU General Public License. |
6 | 6 | # See the file COPYING for more details. |
7 | 7 |
|
8 | | -import typing |
| 8 | +from typing import Dict, Optional |
9 | 9 | import time |
10 | 10 | import os |
11 | 11 | import sys |
|
25 | 25 | import queueHandler |
26 | 26 | import core |
27 | 27 | from . import guiHelper |
28 | | -from . import settingsDialogs |
| 28 | +from .settingsDialogs import SettingsDialog |
29 | 29 | from .settingsDialogs import * |
30 | 30 | from .inputGestures import InputGesturesDialog |
31 | 31 | import speechDictHandler |
32 | 32 | from . import logViewer |
33 | 33 | import speechViewer |
34 | 34 | import winUser |
35 | 35 | import api |
| 36 | +import globalPluginHandler |
| 37 | +import brailleViewer |
| 38 | +import watchdog |
36 | 39 |
|
37 | 40 | try: |
38 | 41 | import updateCheck |
|
47 | 50 | ### Globals |
48 | 51 | mainFrame = None |
49 | 52 | isInMessageBox = False |
| 53 | +_hasAppExited = False |
50 | 54 |
|
51 | 55 |
|
52 | 56 | class MainFrame(wx.Frame): |
@@ -357,25 +361,6 @@ def onConfigProfilesCommand(self, evt): |
357 | 361 | ProfilesDialog(gui.mainFrame).Show() |
358 | 362 | self.postPopup() |
359 | 363 |
|
360 | | - |
361 | | -def safeAppExit(): |
362 | | - """ |
363 | | - Ensures the app is exited by all the top windows being destroyed |
364 | | - """ |
365 | | - |
366 | | - for window in wx.GetTopLevelWindows(): |
367 | | - if isinstance(window, wx.Dialog) and window.IsModal(): |
368 | | - log.info(f"ending modal {window} during exit process") |
369 | | - wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL) |
370 | | - if isinstance(window, MainFrame): |
371 | | - log.info(f"destroying main frame during exit process") |
372 | | - # the MainFrame has EVT_CLOSE bound to the ExitDialog |
373 | | - # which calls this function on exit, so destroy this window |
374 | | - wx.CallAfter(window.Destroy) |
375 | | - else: |
376 | | - log.info(f"closing window {window} during exit process") |
377 | | - wx.CallAfter(window.Close) |
378 | | - |
379 | 364 | class SysTrayIcon(wx.adv.TaskBarIcon): |
380 | 365 |
|
381 | 366 | def __init__(self, frame): |
@@ -558,6 +543,74 @@ def onActivate(self, evt): |
558 | 543 | appModules.nvda.nvdaMenuIaIdentity = None |
559 | 544 | mainFrame.postPopup() |
560 | 545 |
|
| 546 | + |
| 547 | +def _terminateModule(module, name: Optional[str] = None): |
| 548 | + if name is None: |
| 549 | + name = module.__name__ |
| 550 | + log.debug("Terminating %s" % name) |
| 551 | + try: |
| 552 | + module.terminate() |
| 553 | + except Exception as e: |
| 554 | + log.exception(f"Error terminating {name}\nException: {e}") |
| 555 | + |
| 556 | + |
| 557 | +def safeAppExit(): |
| 558 | + """ |
| 559 | + Ensures the app is exited by all the top windows being destroyed. |
| 560 | + wx objects that don't inherit from wx.Window (eg sysTrayIcon, Menu) need to be manually destroyed. |
| 561 | + """ |
| 562 | + global _hasAppExited, mainFrame |
| 563 | + |
| 564 | + if _hasAppExited: |
| 565 | + return |
| 566 | + |
| 567 | + if updateCheck: |
| 568 | + _terminateModule(updateCheck) |
| 569 | + |
| 570 | + _terminateModule(watchdog) |
| 571 | + _terminateModule(globalPluginHandler) |
| 572 | + brailleViewer.destroyBrailleViewer() |
| 573 | + |
| 574 | + app = wx.GetApp() |
| 575 | + |
| 576 | + # prevent race condition with object deletion |
| 577 | + # prevent deletion of the object while we work on it. |
| 578 | + _SettingsDialog = SettingsDialog |
| 579 | + nonWeak: Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances) |
| 580 | + |
| 581 | + for instance, state in nonWeak.items(): |
| 582 | + if state is _SettingsDialog.DialogState.DESTROYED: |
| 583 | + log.error( |
| 584 | + "Destroyed but not deleted instance of gui.SettingsDialog exists" |
| 585 | + f": {instance.title} - {instance.__class__.__qualname__} - {instance}" |
| 586 | + ) |
| 587 | + else: |
| 588 | + log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance)) |
| 589 | + |
| 590 | + # wx.Windows destroy child Windows automatically but wx.Menu and TaskBarIcon don't inherit from wx.Window. |
| 591 | + # They must be manually destroyed when exiting the app. |
| 592 | + # Note: this doesn't consistently clean them from the tray and appears to be a wx issue. (#12286, #12238) |
| 593 | + if mainFrame is not None: |
| 594 | + log.debug("destroying system tray icon and menu") |
| 595 | + app.ScheduleForDestruction(mainFrame.sysTrayIcon.menu) |
| 596 | + mainFrame.sysTrayIcon.RemoveIcon() |
| 597 | + app.ScheduleForDestruction(mainFrame.sysTrayIcon) |
| 598 | + |
| 599 | + for window in wx.GetTopLevelWindows(): |
| 600 | + if isinstance(window, wx.Dialog) and window.IsModal(): |
| 601 | + log.debug(f"ending modal {window} during exit process") |
| 602 | + wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL) |
| 603 | + if isinstance(window, MainFrame): |
| 604 | + log.debug("destroying main frame during exit process") |
| 605 | + # the MainFrame has EVT_CLOSE bound to the ExitDialog |
| 606 | + # which calls this function on exit, so destroy this window |
| 607 | + app.ScheduleForDestruction(window) |
| 608 | + else: |
| 609 | + log.debug(f"closing window {window} during exit process") |
| 610 | + wx.CallAfter(window.Close) |
| 611 | + |
| 612 | + _hasAppExited = True |
| 613 | + |
561 | 614 | def initialize(): |
562 | 615 | global mainFrame |
563 | 616 | if mainFrame: |
@@ -585,33 +638,12 @@ def wx_CallAfter_wrapper(func, *args, **kwargs): |
585 | 638 | wx.CallAfter = wx_CallAfter_wrapper |
586 | 639 |
|
587 | 640 | def terminate(): |
588 | | - import brailleViewer |
589 | | - brailleViewer.destroyBrailleViewer() |
590 | | - |
591 | | - # prevent race condition with object deletion |
592 | | - # prevent deletion of the object while we work on it. |
593 | | - _SettingsDialog = settingsDialogs.SettingsDialog |
594 | | - nonWeak: typing.Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances) |
595 | | - |
596 | | - for instance, state in nonWeak.items(): |
597 | | - if state is _SettingsDialog.DialogState.DESTROYED: |
598 | | - log.error( |
599 | | - "Destroyed but not deleted instance of gui.SettingsDialog exists" |
600 | | - f": {instance.title} - {instance.__class__.__qualname__} - {instance}" |
601 | | - ) |
602 | | - else: |
603 | | - log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance)) |
604 | 641 | global mainFrame |
605 | | - # This is called after the main loop exits because WM_QUIT exits the main loop |
606 | | - # without destroying all objects correctly and we need to support WM_QUIT. |
607 | | - # Therefore, any request to exit should exit the main loop. |
| 642 | + |
| 643 | + # If MainLoop is terminated through WM_QUIT, such as starting an NVDA instance older than 2021.1, |
| 644 | + # safeAppExit has not been called yet |
608 | 645 | safeAppExit() |
609 | | - # #4460: We need another iteration of the main loop |
610 | | - # so that everything (especially the TaskBarIcon) is cleaned up properly. |
611 | | - # ProcessPendingEvents doesn't seem to work, but MainLoop does. |
612 | | - # Because the top window gets destroyed, |
613 | | - # MainLoop thankfully returns pretty quickly. |
614 | | - wx.GetApp().MainLoop() |
| 646 | + |
615 | 647 | mainFrame = None |
616 | 648 |
|
617 | 649 | def showGui(): |
|
0 commit comments