Skip to content

Commit 074e5b2

Browse files
authored
Merge 299bb81 into 626e9b9
2 parents 626e9b9 + 299bb81 commit 074e5b2

5 files changed

Lines changed: 48 additions & 30 deletions

File tree

source/core.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,13 +391,20 @@ def __init__(self, windowName=None):
391391
self.orientationStateCache = self.ORIENTATION_NOT_INITIALIZED
392392
self.orientationCoordsCache = (0,0)
393393
self.handlePowerStatusChange()
394+
# Accept WM_EXIT_NVDA from other NVDA instances
395+
import winUser
396+
if not winUser.user32.ChangeWindowMessageFilterEx(self.handle, winUser.WM_EXIT_NVDA, 1, None):
397+
raise winUser.WinError()
394398

395399
def windowProc(self, hwnd, msg, wParam, lParam):
396400
post_windowMessageReceipt.notify(msg=msg, wParam=wParam, lParam=lParam)
397401
if msg == self.WM_POWERBROADCAST and wParam == self.PBT_APMPOWERSTATUSCHANGE:
398402
self.handlePowerStatusChange()
399403
elif msg == winUser.WM_DISPLAYCHANGE:
400404
self.handleScreenOrientationChange(lParam)
405+
elif msg == winUser.WM_EXIT_NVDA:
406+
log.debug("NVDA Instance being closed from another instance")
407+
gui.safeAppExit()
401408

402409
def handleScreenOrientationChange(self, lParam):
403410
import ui

source/gui/__init__.py

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -358,22 +358,36 @@ def onConfigProfilesCommand(self, evt):
358358

359359
def 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+
377391
class 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

584598
def 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

608602
def showGui():

source/gui/startupDialogs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def run(cls):
117117
gui.mainFrame.prePopup()
118118
d = cls(gui.mainFrame)
119119
d.ShowModal()
120-
d.Destroy()
120+
wx.CallAfter(d.Destroy)
121121
gui.mainFrame.postPopup()
122122

123123

source/nvda.pyw

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,18 @@ for name in pathAppArgs:
162162

163163
def terminateRunningNVDA(window):
164164
processID,threadID=winUser.getWindowThreadProcessID(window)
165-
winUser.PostMessage(window,winUser.WM_QUIT,0,0)
165+
try:
166+
winUser.PostSafeQuitMessage(window)
167+
except PermissionError:
168+
# allow for updating between NVDA versions, deprecated in 2022.1
169+
# in 2022.1 just call winUser.PostSafeQuitMessage(window) without the try/except
170+
winUser.PostMessage(window, winUser.WM_QUIT, 0, 0)
166171
h=winKernel.openProcess(winKernel.SYNCHRONIZE,False,processID)
167172
if not h:
168173
# The process is already dead.
169174
return
170175
try:
171-
res=winKernel.waitForSingleObject(h,4000)
176+
res = winKernel.waitForSingleObject(h, 5000)
172177
if res==0:
173178
# The process terminated within the timeout period.
174179
return
@@ -251,9 +256,7 @@ if customVenvDetected:
251256
log.warning("NVDA launched using a custom Python virtual environment.")
252257
if globalVars.appArgs.changeScreenReaderFlag:
253258
winUser.setSystemScreenReaderFlag(True)
254-
#Accept wm_quit from other processes, even if running with higher privilages
255-
if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT,1):
256-
raise WinError()
259+
257260
# Make this the last application to be shut down and don't display a retry dialog box.
258261
winKernel.SetProcessShutdownParameters(0x100, winKernel.SHUTDOWN_NORETRY)
259262
if not isSecureDesktop and not config.isAppX:

source/winUser.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,11 @@ class GUITHREADINFO(Structure):
377377
# The height of the virtual screen, in pixels.
378378
SM_CYVIRTUALSCREEN = 79
379379

380+
# Registers an application wide Window Message so that NVDA can be exited across instances
381+
WM_EXIT_NVDA = user32.RegisterWindowMessageW("WM_EXIT_NVDA")
382+
if not WM_EXIT_NVDA:
383+
raise WinError()
384+
380385
def setSystemScreenReaderFlag(val):
381386
user32.SystemParametersInfoW(SPI_SETSCREENREADER,val,0,SPIF_UPDATEINIFILE|SPIF_SENDCHANGE)
382387

@@ -601,6 +606,15 @@ def PostMessage(hwnd, msg, wParam, lParam):
601606
if not user32.PostMessageW(hwnd, msg, wParam, lParam):
602607
raise WinError()
603608

609+
610+
def PostSafeQuitMessage(hwnd: HWND):
611+
"""
612+
Posts a WM_EXIT_NVDA quit message across windows to exit NVDA safely from another instance
613+
@param hwnd: Target NVDA window id
614+
"""
615+
if not user32.PostMessageW(hwnd, WM_EXIT_NVDA, None, None):
616+
raise WinError()
617+
604618
user32.VkKeyScanExW.restype = SHORT
605619
def VkKeyScanEx(ch, hkl):
606620
res = user32.VkKeyScanExW(WCHAR(ch), hkl)

0 commit comments

Comments
 (0)