Skip to content

Commit

Permalink
Disable UIA text change events outside of Word, Windows Console, and …
Browse files Browse the repository at this point in the history
…Windows Terminal (#14067)

Mitigation for #11002.
Blocking #14047.

Summary of the issue:
UIA textChange NVDA events are seldom (if ever) used outside of a few specific situations, but have an extreme performance impact (see #11002).

Description of user facing changes
Improved performance/less chance of NVDA hanging in UIA applications.

Description of development approach
Explicitly do not process UIA textChange events outside of Windows Console, Terminal, and Word. The eventual end goal is to remove TermControl/TermControl2 from UIAHandler.textChangeUIAClassNames in #14047, which will very greatly improve performance in Windows Terminal. (conhost will remain, as there don't seem to be any plans to add notifications, especially as wt is becoming the default).

I'm very reluctant to add a mechanism by which add-ons/app modules can request textChange events unless someone requests it, especially given #11002.
  • Loading branch information
codeofdusk authored Aug 29, 2022
1 parent a4644ef commit 6fdabfe
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 5 deletions.
81 changes: 76 additions & 5 deletions source/UIAHandler/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2008-2021 NV Access Limited, Joseph Lee, Babbage B.V., Leonard de Ruijter, Bill Dengler
# Copyright (C) 2008-2022 NV Access Limited, Joseph Lee, Babbage B.V., Leonard de Ruijter, Bill Dengler
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

Expand Down Expand Up @@ -99,6 +99,16 @@
"Shell_SystemDialog", # Various dialogs in Windows 10 Settings app
]

textChangeUIAAutomationIDs = (
"Text Area", # Windows Console Host
)

textChangeUIAClassNames = (
"_WwG", # Microsoft Word
"TermControl",
"TermControl2"
)

NVDAUnitsToUIAUnits: Dict[str, int] = {
textInfos.UNIT_CHARACTER: UIA.TextUnit_Character,
textInfos.UNIT_WORD: UIA.TextUnit_Word,
Expand Down Expand Up @@ -206,11 +216,9 @@
autoSelectDetectionAvailable = False
if winVersion.getWinVer() >= winVersion.WIN10:
UIAEventIdsToNVDAEventNames.update({
UIA.UIA_Text_TextChangedEventId: "textChange",
UIA.UIA_Text_TextSelectionChangedEventId: "caret",
})
localEventHandlerGroupUIAEventIds.update({
UIA.UIA_Text_TextChangedEventId,
UIA.UIA_Text_TextSelectionChangedEventId,
})
autoSelectDetectionAvailable = True
Expand Down Expand Up @@ -268,6 +276,7 @@ def __init__(self):
super(UIAHandler,self).__init__()
self.globalEventHandlerGroup = None
self.localEventHandlerGroup = None
self.localEventHandlerGroupWithTextChanges = None
self._localEventHandlerGroupElements = set()
self.MTAThreadInitEvent=threading.Event()
self.MTAThreadQueue = Queue()
Expand Down Expand Up @@ -403,6 +412,17 @@ def _registerGlobalEventHandlers(self):
self.baseCacheRequest,
self
)
if (
not utils._shouldSelectivelyRegister()
and winVersion.getWinVer() >= winVersion.WIN10
):
# #14067: Due to poor performance, textChange requires special handling
self.globalEventHandlerGroup.AddAutomationEventHandler(
UIA.UIA_Text_TextChangedEventId,
UIA.TreeScope_Subtree,
self.baseCacheRequest,
self
)
# #7984: add support for notification event (IUIAutomation5, part of Windows 10 build 16299 and later).
if isinstance(self.clientObject, UIA.IUIAutomation5):
self.globalEventHandlerGroup.AddNotificationEventHandler(
Expand All @@ -421,21 +441,41 @@ def _registerGlobalEventHandlers(self):
def _createLocalEventHandlerGroup(self):
if isinstance(self.clientObject, UIA.IUIAutomation6):
self.localEventHandlerGroup = self.clientObject.CreateEventHandlerGroup()
self.localEventHandlerGroupWithTextChanges = self.clientObject.CreateEventHandlerGroup()
else:
self.localEventHandlerGroup = utils.FakeEventHandlerGroup(self.clientObject)
self.localEventHandlerGroupWithTextChanges = utils.FakeEventHandlerGroup(self.clientObject)
self.localEventHandlerGroup.AddPropertyChangedEventHandler(
UIA.TreeScope_Ancestors | UIA.TreeScope_Element,
self.baseCacheRequest,
self,
*self.clientObject.IntSafeArrayToNativeArray(localEventHandlerGroupUIAPropertyIds)
)
self.localEventHandlerGroupWithTextChanges.AddPropertyChangedEventHandler(
UIA.TreeScope_Ancestors | UIA.TreeScope_Element,
self.baseCacheRequest,
self,
*self.clientObject.IntSafeArrayToNativeArray(localEventHandlerGroupUIAPropertyIds)
)
for eventId in localEventHandlerGroupUIAEventIds:
self.localEventHandlerGroup.AddAutomationEventHandler(
eventId,
UIA.TreeScope_Ancestors | UIA.TreeScope_Element,
self.baseCacheRequest,
self
)
self.localEventHandlerGroupWithTextChanges.AddAutomationEventHandler(
eventId,
UIA.TreeScope_Ancestors | UIA.TreeScope_Element,
self.baseCacheRequest,
self
)
self.localEventHandlerGroupWithTextChanges.AddAutomationEventHandler(
UIA.UIA_Text_TextChangedEventId,
UIA.TreeScope_Ancestors | UIA.TreeScope_Element,
self.baseCacheRequest,
self
)

def addEventHandlerGroup(self, element, eventHandlerGroup):
if isinstance(eventHandlerGroup, UIA.IUIAutomationEventHandlerGroup):
Expand Down Expand Up @@ -466,7 +506,23 @@ def func():
if not isStillFocus:
return
try:
self.addEventHandlerGroup(element, self.localEventHandlerGroup)
if (
element.currentClassName in textChangeUIAClassNames
or element.CachedAutomationID in textChangeUIAAutomationIDs
):
group = self.localEventHandlerGroupWithTextChanges
logPrefix = "Explicitly"
else:
group = self.localEventHandlerGroup
logPrefix = "Not"

if _isDebug():
log.debugWarning(
f"{logPrefix} registering for textChange events from UIA element "
f"with class name {repr(element.currentClassName)} "
f"and automation ID {repr(element.CachedAutomationID)}"
)
self.addEventHandlerGroup(element, group)
except COMError:
log.error("Could not register for UIA events for element", exc_info=True)
else:
Expand Down Expand Up @@ -500,7 +556,22 @@ def IUIAutomationEventHandler_HandleAutomationEvent(self,sender,eventID):
if _isDebug():
log.debug("HandleAutomationEvent: Ignored MenuOpenedEvent while focus event pending")
return
NVDAEventName=UIAEventIdsToNVDAEventNames.get(eventID,None)
if eventID == UIA.UIA_Text_TextChangedEventId:
if (
sender.currentClassName in textChangeUIAClassNames
or sender.CachedAutomationID in textChangeUIAAutomationIDs
):
NVDAEventName = "textChange"
else:
if _isDebug():
log.debugWarning(
"HandleAutomationEvent: Dropping textChange event from element "
f"with class name {repr(sender.currentClassName)} "
f"and automation ID {repr(sender.CachedAutomationID)}"
)
return
else:
NVDAEventName = UIAEventIdsToNVDAEventNames.get(eventID, None)
if not NVDAEventName:
if _isDebug():
log.debugWarning(f"HandleAutomationEvent: Don't know how to handle event {eventID}")
Expand Down
1 change: 1 addition & 0 deletions user_docs/en/changes.t2t
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ What's New in NVDA
Please refer to [the developer guide https://www.nvaccess.org/files/nvda/documentation/developerGuide.html#API] for information on NVDA's API deprecation and removal process.

- The [NVDA API Announcement mailing list https://groups.google.com/a/nvaccess.org/g/nvda-api/about] was created. (#13999)
- NVDA no longer processes ``textChange`` events for most UI Automation applications due to their extreme negative performance impact. (#11002, #14067)
-


Expand Down

0 comments on commit 6fdabfe

Please sign in to comment.