Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
When an application is responding slowly and you switch away from tha…
…t application, NVDA is now much more responsive in other applications in most cases.

This is done by filtering out events from background windows in most cases to avoid freezes when a background app is unresponsive. One hard-coded exception is when background progress bar reporting is enabled, in which case background valueChange events are allowed.
To facilitate this, eventHandler.shouldAcceptEvent was introduced.
eventHandler.requestEvents was also added to request particular events that are blocked by default; e.g. show events from a specific control or certain events even when in the background.
Fixes #3831.
  • Loading branch information
jcsteh committed Jul 20, 2015
1 parent 5502b74 commit f0a4cb5
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 12 deletions.
49 changes: 37 additions & 12 deletions source/IAccessibleHandler.py
Expand Up @@ -140,6 +140,12 @@ def flushEvents(self):
#A place to store live IAccessible NVDAObjects, that can be looked up by their window,objectID,childID event params.
liveNVDAObjectTable=weakref.WeakValueDictionary()

# #3831: Stuff related to deferring of events for foreground changes.
# See pumpAll for details.
MAX_FOREGROUND_DEFERS=2
_deferUntilForegroundWindow = None
_foregroundDefers = 0

IAccessibleRolesToNVDARoles={
oleacc.ROLE_SYSTEM_WINDOW:controlTypes.ROLE_WINDOW,
oleacc.ROLE_SYSTEM_CLIENT:controlTypes.ROLE_PANE,
Expand Down Expand Up @@ -567,9 +573,15 @@ def winEventCallback(handle,eventID,window,objectID,childID,threadID,timestamp):
#Move up the ancestry to find the real mozilla Window and use that
if winUser.getClassName(window)=='MozillaDropShadowWindowClass':
return
#We never want to see foreground events for the Program Manager or Shell (task bar)
if eventID==winUser.EVENT_SYSTEM_FOREGROUND and windowClassName in ("Progman","Shell_TrayWnd"):
return
if eventID==winUser.EVENT_SYSTEM_FOREGROUND:
#We never want to see foreground events for the Program Manager or Shell (task bar)
if windowClassName in ("Progman","Shell_TrayWnd"):
return
# #3831: Event handling can be deferred if Windows takes a while to change the foreground window.
# See pumpAll for details.
global _deferUntilForegroundWindow,_foregroundDefers
_deferUntilForegroundWindow=window
_foregroundDefers=0
if windowClassName=="MSNHiddenWindowClass":
# HACK: Events get fired by this window in Windows Live Messenger 2009 when it starts.
# If we send a WM_NULL to this window at this point (which happens in accessibleObjectFromEvent), Messenger will silently exit (#677).
Expand Down Expand Up @@ -632,11 +644,6 @@ def processFocusWinEvent(window,objectID,childID,force=False):
# as this is a child control and the SDM MSAA events don't handle child controls.
if childID==0 and not windowClassName.startswith('bosa_sdm') and winUser.getClassName(winUser.getAncestor(window,winUser.GA_PARENT)).startswith('bosa_sdm'):
return False
rootWindow=winUser.getAncestor(window,winUser.GA_ROOT)
# If this window is not within the foreground window and this window or its root window is not a popup window, and this window's root window is not the highest in the z-order
if not winUser.isDescendantWindow(winUser.getForegroundWindow(),window) and not (winUser.getWindowStyle(window) & winUser.WS_POPUP or winUser.getWindowStyle(rootWindow)&winUser.WS_POPUP) and winUser.getPreviousWindow(rootWindow)!=0:
# This is a focus event from a background window, so ignore it.
return False
#Notify appModuleHandler of this new foreground window
appModuleHandler.update(winUser.getWindowThreadProcessID(window)[0])
#If Java access bridge is running, and this is a java window, then pass it to java and forget about it
Expand Down Expand Up @@ -751,10 +758,9 @@ def processForegroundWinEvent(window,objectID,childID):
return True

def processShowWinEvent(window,objectID,childID):
className=winUser.getClassName(window)
#For now we only support 'show' event for tooltips, IMM candidates, notification bars and other specific notification area alerts as otherwize we get flooded
# #4741: TTrayAlert is for Skype.
if className in ("Frame Notification Bar","tooltips_class32","mscandui21.candidate","mscandui40.candidate","MSCandUIWindow_Candidate","TTrayAlert") and objectID==winUser.OBJID_CLIENT:
# eventHandler.shouldAcceptEvent only accepts show events for a few specific cases.
# Narrow this further to only accept events for clients or custom objects.
if objectID==winUser.OBJID_CLIENT or objectID>0:
NVDAEvent=winEventToNVDAEvent(winUser.EVENT_OBJECT_SHOW,window,objectID,childID)
if NVDAEvent:
eventHandler.queueEvent(*NVDAEvent)
Expand Down Expand Up @@ -831,12 +837,31 @@ def initialize():
log.error("initialize: could not register callback for event %s (%s)"%(eventType,winEventIDsToNVDAEventNames[eventType]))

def pumpAll():
global _deferUntilForegroundWindow,_foregroundDefers
if _deferUntilForegroundWindow:
# #3831: Sometimes, a foreground event is fired,
# but GetForegroundWindow() takes a short while to return this new foreground.
if _foregroundDefers<MAX_FOREGROUND_DEFERS and winUser.getForegroundWindow()!=_deferUntilForegroundWindow:
# Wait a core cycle before handling events to give the foreground window time to update.
core.requestPump()
_foregroundDefers+=1
return
else:
# Either the foreground window is now correct
# or we've already had the maximum number of defers.
# (Sometimes, foreground events are fired even when the foreground hasn't actually changed.)
_deferUntilForegroundWindow=None

#Receive all the winEvents from the limiter for this cycle
winEvents=winEventLimiter.flushEvents()
focusWinEvents=[]
validFocus=False
fakeFocusEvent=None
for winEvent in winEvents[0-MAX_WINEVENTS:]:
# #4001: Ideally, we'd call shouldAcceptEvent in winEventCallback,
# but this causes focus issues when starting applications.
if not eventHandler.shouldAcceptEvent(winEventIDsToNVDAEventNames[winEvent[0]], windowHandle=winEvent[1]):
continue
#We want to only pass on one focus event to NVDA, but we always want to use the most recent possible one
if winEvent[0] in (winUser.EVENT_OBJECT_FOCUS,winUser.EVENT_SYSTEM_FOREGROUND):
focusWinEvents.append(winEvent)
Expand Down
18 changes: 18 additions & 0 deletions source/_UIAHandler.py
Expand Up @@ -188,6 +188,12 @@ def IUIAutomationEventHandler_HandleAutomationEvent(self,sender,eventID):
return
if not self.isNativeUIAElement(sender):
return
try:
window=sender.cachedNativeWindowHandle
except COMError:
window=None
if window and not eventHandler.shouldAcceptEvent(NVDAEventName,windowHandle=window):
return
import NVDAObjects.UIA
obj=NVDAObjects.UIA.UIA(UIAElement=sender)
if not obj or (NVDAEventName=="gainFocus" and not obj.shouldAllowUIAFocusEvent):
Expand Down Expand Up @@ -217,6 +223,12 @@ def IUIAutomationFocusChangedEventHandler_HandleFocusChangedEvent(self,sender):
# Therefore, don't ignore the event if the last focus object has lost its hasKeyboardFocus state.
if self.clientObject.compareElements(sender,lastFocus) and lastFocus.currentHasKeyboardFocus:
return
try:
window=sender.cachedNativeWindowHandle
except COMError:
window=None
if window and not eventHandler.shouldAcceptEvent("gainFocus",windowHandle=window):
return
obj=NVDAObjects.UIA.UIA(UIAElement=sender)
if not obj or not obj.shouldAllowUIAFocusEvent:
return
Expand All @@ -234,6 +246,12 @@ def IUIAutomationPropertyChangedEventHandler_HandlePropertyChangedEvent(self,sen
return
if not self.isNativeUIAElement(sender):
return
try:
window=sender.cachedNativeWindowHandle
except COMError:
window=None
if window and not eventHandler.shouldAcceptEvent(NVDAEventName,windowHandle=window):
return
import NVDAObjects.UIA
obj=NVDAObjects.UIA.UIA(UIAElement=sender)
if not obj:
Expand Down
2 changes: 2 additions & 0 deletions source/appModuleHandler.py
Expand Up @@ -138,6 +138,8 @@ def cleanup():
if deadMod in set(o.appModule for o in api.getFocusAncestors()+[api.getFocusObject()] if o and o.appModule):
if hasattr(deadMod,'event_appLoseFocus'):
deadMod.event_appLoseFocus()
import eventHandler
eventHandler.handleAppTerminate(deadMod)
try:
deadMod.terminate()
except:
Expand Down
81 changes: 81 additions & 0 deletions source/eventHandler.py
Expand Up @@ -14,6 +14,8 @@
import controlTypes
from logHandler import log
import globalPluginHandler
import config
import winUser

#Some dicts to store event counts by name and or obj
_pendingEventCountsByName={}
Expand Down Expand Up @@ -178,3 +180,82 @@ def doPreDocumentLoadComplete(obj):
#Focus may be in this new treeInterceptor, so force focus to look up its treeInterceptor
focusObject.treeInterceptor=treeInterceptorHandler.getTreeInterceptor(focusObject)
return True

#: set of (eventName, processId, windowClassName) of events to accept.
_acceptEvents = set()
#: Maps process IDs to sets of events so they can be cleaned up when the process exits.
_acceptEventsByProcess = {}

def requestEvents(eventName=None, processId=None, windowClassName=None):
"""Request that particular events be accepted from a platform API.
Normally, L{shouldAcceptEvent} rejects certain events, including
most show events, events indicating changes in background processes, etc.
This function allows plugins to override this for specific cases;
e.g. to receive show events from a specific control or
to receive certain events even when in the background.
Note that NVDA may block some events at a lower level and doesn't listen for some event types at all.
In these cases, you will not be able to override this.
This should generally be called when a plugin is instantiated.
All arguments must be provided.
"""
if not eventName or not processId or not windowClassName:
raise ValueError("eventName, processId or windowClassName not specified")
entry = (eventName, processId, windowClassName)
procEvents = _acceptEventsByProcess.get(processId)
if not procEvents:
procEvents = _acceptEventsByProcess[processId] = set()
procEvents.add(entry)
_acceptEvents.add(entry)

def handleAppTerminate(appModule):
global _acceptEvents
events = _acceptEventsByProcess.pop(appModule.processID, None)
if not events:
return
_acceptEvents -= events

def shouldAcceptEvent(eventName, windowHandle=None):
"""Check whether an event should be accepted from a platform API.
Creating NVDAObjects and executing events can be expensive
and might block the main thread noticeably if the object is slow to respond.
Therefore, this should be used before NVDAObject creation to filter out any unnecessary events.
A platform API handler may do its own filtering before this.
"""
if not windowHandle:
# We can't filter without a window handle.
return True
key = (eventName,
winUser.getWindowThreadProcessID(windowHandle)[0],
winUser.getClassName(windowHandle))
if key in _acceptEvents:
return True
if eventName == "valueChange" and config.conf["presentation"]["progressBarUpdates"]["reportBackgroundProgressBars"]:
return True
if eventName == "show":
# Only accept 'show' events for specific cases, as otherwise we get flooded.
return winUser.getClassName(windowHandle) in (
"Frame Notification Bar", # notification bars
"tooltips_class32", # tooltips
"mscandui21.candidate", "mscandui40.candidate", "MSCandUIWindow_Candidate", # IMM candidates
"TTrayAlert", # #4741: Skype
)
if eventName == "alert" and winUser.getClassName(winUser.getAncestor(windowHandle, winUser.GA_PARENT)) == "ToastChildWindowClass":
# Toast notifications.
return True
if windowHandle == winUser.getDesktopWindow():
# #3897: We fire some events such as switchEnd and menuEnd on the desktop window
# because the original window is now invalid.
return True

fg = winUser.getForegroundWindow()
if (winUser.isDescendantWindow(fg, windowHandle)
# #3899, #3905: Covers cases such as the Firefox Page Bookmarked window and OpenOffice/LibreOffice context menus.
or winUser.isDescendantWindow(fg, winUser.getAncestor(windowHandle, winUser.GA_ROOTOWNER))):
# This is for the foreground application.
return True
if (winUser.user32.GetWindowLongW(windowHandle, winUser.GWL_EXSTYLE) & winUser.WS_EX_TOPMOST
or winUser.user32.GetWindowLongW(winUser.getAncestor(windowHandle, winUser.GA_ROOT), winUser.GWL_EXSTYLE) & winUser.WS_EX_TOPMOST):
# This window or its root is a topmost window.
# This includes menus, combo box pop-ups and the task switching list.
return True
return False
2 changes: 2 additions & 0 deletions source/winUser.py
Expand Up @@ -85,6 +85,7 @@ class GUITHREADINFO(Structure):
WS_HSCROLL=0x100000
WS_VSCROLL=0x200000
WS_CAPTION=0xC00000
WS_EX_TOPMOST=0x00000008
BS_GROUPBOX=7
ES_MULTILINE=4
LBS_OWNERDRAWFIXED=0x0010
Expand All @@ -109,6 +110,7 @@ class GUITHREADINFO(Structure):
#getWindowLong
GWL_ID=-12
GWL_STYLE=-16
GWL_EXSTYLE=-20
#getWindow
GW_HWNDNEXT=2
GW_HWNDPREV=3
Expand Down
2 changes: 2 additions & 0 deletions user_docs/en/changes.t2t
Expand Up @@ -29,10 +29,12 @@
- In browse mode in Internet Explorer and other MSHTML controls, the correct content is now reported when an element appears or changes and is immediately focused. (#5040)
- In browse mode in Microsoft Word, single letter navigation now updates the braille display and the review cursor as expected. (#4968)
- In braille, extraneous spaces are no longer displayed between or after indicators for controls and formatting. (#5043)
- When an application is responding slowly and you switch away from that application, NVDA is now much more responsive in other applications in most cases. (#3831)


== Changes for Developers ==
- You can now injet raw input from a system keyboard that is not handled natively by Windows (e.g. a QWERTY keyboard on a braille display) using the new keyboardHandler.injectRawKeyboardInput function. (#4576)
- eventHandler.requestEvents has been added to request particular events that are blocked by default; e.g. show events from a specific control or certain events even when in the background. (#3831)


= 2015.2 =
Expand Down

0 comments on commit f0a4cb5

Please sign in to comment.