From 540795475c91f4fbe0d00d295d270ce8b43eb70f Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Wed, 21 Jul 2021 22:31:01 -0400 Subject: [PATCH] UI Automation in Windows Console: add "API levels" as a better isImprovedTextRangeAvailable (#12660) Summary of the issue: The Windows 11 inbox console will ship with most, but not all, fixes required to make UIA by default a good experience. However, a new conhost version will be delivered "out-of-box" including additional functionality and bug fixes. Therefore, isImprovedTextRangeAvailable (a boolean value) is now insufficient to express the range of possible UIA console versions now in the wild. Description of how this pull request fixes the issue: Adds an enumeration, WinConsoleAPILevel, with comments describing the meaning of each member. Refactors NVDA's current code to use the new enum without changing current functionality. Functionality changes to follow in subsequent PRs. Expose apiLevel in dev info to ease debugging. Deprecate isImprovedTextRangeAvailable, but refactor it for now to use apiLevel. Co-authored-by: Leonard de Ruijter Co-authored-by: buddsean --- source/NVDAObjects/UIA/winConsoleUIA.py | 38 +++++++++++++++++-------- source/UIAUtils.py | 37 +++++++++++++++--------- source/_UIAConstants.py | 24 +++++++++++++++- user_docs/en/changes.t2t | 4 +++ 4 files changed, 77 insertions(+), 26 deletions(-) diff --git a/source/NVDAObjects/UIA/winConsoleUIA.py b/source/NVDAObjects/UIA/winConsoleUIA.py index bc17a75c3cb..f4e8571d787 100644 --- a/source/NVDAObjects/UIA/winConsoleUIA.py +++ b/source/NVDAObjects/UIA/winConsoleUIA.py @@ -11,7 +11,8 @@ from comtypes import COMError from logHandler import log -from UIAUtils import _isImprovedConhostTextRangeAvailable +from UIAUtils import _getConhostAPILevel +from _UIAConstants import WinConsoleAPILevel from . import UIATextInfo from ..behaviors import EnhancedTermTypedCharSupport, KeyboardHandlerBasedTypedCharSupport from ..window import Window @@ -335,9 +336,21 @@ class WinConsoleUIA(KeyboardHandlerBasedTypedCharSupport): #: a lot of text. STABILIZE_DELAY = 0.03 + def _get_apiLevel(self) -> WinConsoleAPILevel: + """ + This property shows which of several console UIA workarounds are + needed in a given conhost instance. + See the comments on the WinConsoleAPILevel enum for details. + """ + self.apiLevel = _getConhostAPILevel(self.windowHandle) + return self.apiLevel + def _get__caretMovementTimeoutMultiplier(self): "On older consoles, the caret can take a while to move." - return 1 if self.isImprovedTextRangeAvailable else 1.5 + return ( + 1 if self.apiLevel >= WinConsoleAPILevel.IMPROVED + else 1.5 + ) def _get_windowThreadID(self): # #10113: Windows forces the thread of console windows to match the thread of the first attached process. @@ -351,15 +364,11 @@ def _get_windowThreadID(self): return threadID def _get_isImprovedTextRangeAvailable(self): - """This property determines whether microsoft/terminal#4495 - and by extension microsoft/terminal#4018 are present in this conhost. - In consoles before these PRs, a number of workarounds were needed - in our UIA implementation. However, these do not fix all bugs and are - problematic on newer console releases. This property is therefore used - internally to determine whether to activate workarounds and as a - convenience when debugging. - """ - return _isImprovedConhostTextRangeAvailable(self.windowHandle) + log.warning( + "winConsole.isImprovedTextRangeAvailable is deprecated and will be " + "removed in NVDA 2022.1. Please use apiLevel instead." + ) + return self.apiLevel >= WinConsoleAPILevel.IMPROVED def _get_TextInfo(self): """Overriding _get_ConsoleUIATextInfo and thus the ConsoleUIATextInfo property @@ -369,10 +378,15 @@ def _get_TextInfo(self): word movement.""" return ( ConsoleUIATextInfo - if self.isImprovedTextRangeAvailable + if self.apiLevel >= WinConsoleAPILevel.IMPROVED else ConsoleUIATextInfoWorkaroundEndInclusive ) + def _get_devInfo(self): + info = super().devInfo + info.append(f"API level: {self.apiLevel.name}") + return info + def detectPossibleSelectionChange(self): try: return super().detectPossibleSelectionChange() diff --git a/source/UIAUtils.py b/source/UIAUtils.py index 533da15ec10..4ba917bf794 100644 --- a/source/UIAUtils.py +++ b/source/UIAUtils.py @@ -11,6 +11,7 @@ import weakref from functools import lru_cache from logHandler import log +from _UIAConstants import WinConsoleAPILevel def createUIAMultiPropertyCondition(*dicts): @@ -298,22 +299,20 @@ def _shouldUseUIAConsole(hwnd: int) -> bool: # #7497: the UIA implementation in old conhost is incomplete, therefore we # should ignore it. # When the UIA implementation is improved, the below line will be replaced - # with a call to _isImprovedConhostTextRangeAvailable. + # with a check that _getConhostAPILevel >= FORMATTED. return False @lru_cache(maxsize=10) -def _isImprovedConhostTextRangeAvailable(hwnd: int) -> bool: - """This function determines whether microsoft/terminal#4495 and by extension - microsoft/terminal#4018 are present in this conhost. - In consoles before these PRs, a number of workarounds were needed - in our UIA implementation. However, these do not fix all bugs and are - problematic on newer console releases. This function is therefore used - to determine whether this console's UIA implementation is good enough to - use by default.""" - # microsoft/terminal#4495: In newer consoles, +def _getConhostAPILevel(hwnd: int) -> WinConsoleAPILevel: + """ + This function determines which of several console UIA workarounds are + needed in a given conhost instance. + See the comments on the WinConsoleAPILevel enum for details. + """ + # microsoft/terminal#4495: In IMPROVED consoles, # IUIAutomationTextRange::getVisibleRanges returns one visible range. - # Therefore, if exactly one range is returned, it is almost definitely a newer console. + # Therefore, if exactly one range is returned, it is almost definitely an IMPROVED console. try: UIAElement = UIAHandler.handler.clientObject.ElementFromHandleBuildCache( hwnd, UIAHandler.handler.baseCacheRequest @@ -330,7 +329,19 @@ def _isImprovedConhostTextRangeAvailable(hwnd: int) -> bool: UIATextPattern = textArea.GetCurrentPattern( UIAHandler.UIA_TextPatternId ).QueryInterface(UIAHandler.IUIAutomationTextPattern) - return UIATextPattern.GetVisibleRanges().length == 1 + visiRanges = UIATextPattern.GetVisibleRanges() + if visiRanges.length == 1: + # Microsoft/terminal#2161: FORMATTED consoles expose text formatting + # information to UIA. + if isinstance( + visiRanges.GetElement(0).GetAttributeValue(UIAHandler.UIA_FontNameAttributeId), + str + ): + return WinConsoleAPILevel.FORMATTED + else: + return WinConsoleAPILevel.IMPROVED + else: + return WinConsoleAPILevel.END_INCLUSIVE except (COMError, ValueError): log.exception() - return False + return WinConsoleAPILevel.END_INCLUSIVE diff --git a/source/_UIAConstants.py b/source/_UIAConstants.py index f458e8b33bb..1b58347de43 100644 --- a/source/_UIAConstants.py +++ b/source/_UIAConstants.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2021 NV Access Limited +# Copyright (C) 2021 NV Access Limited, Bill Dengler # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -60,3 +60,25 @@ class UIAutomationType(enum.IntEnum): OUT_POINT_ARRAY = 28 OUT_RECT_ARRAY = 29 OUT_ELEMENT_ARRAY = 30 + + +class WinConsoleAPILevel(enum.IntEnum): + """ + Defines actively used Windows Console versions and the levels of custom code required + for each. + """ + # Represents a console before microsoft/terminal#4018 was merged. + # These consoles do not support UIA word navigation and require a number + # of text range workarounds. + END_INCLUSIVE = 0 + # Represents a console with microsoft/terminal#4018, but without + # resolution of microsoft/terminal#2161 (text formatting) + # or microsoft/terminal#6986 (extraneous empty lines). + # This is a significant improvement over END_INCLUSIVE, so fewer workarounds + # are required. However, these consoles lack some information + # (such as text formatting) and require bounding, so are unsuitable for + # usage by default. + IMPROVED = 1 + # Represents an IMPROVED console that exposes text formatting and a + # buffer that does not contain extraneous empty lines. + FORMATTED = 2 diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index f5c6c0dfeaa..7308a68e2b5 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -19,6 +19,10 @@ What's New in NVDA == Changes for Developers == +- ``NVDAObjects.UIA.winConsoleUIA.WinConsoleUIA.isImprovedTextRangeAvailable`` has been deprecated for removal in 2022.1. (#12660) + - Instead use ``apiLevel`` (see the comments at ``_UIAConstants.WinConsoleAPILevel`` for details). + - +- = 2021.2 =