Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI Automation in Windows Console: add "API levels" as a better isImprovedTextRangeAvailable #12660

Merged
merged 4 commits into from
Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions source/NVDAObjects/UIA/winConsoleUIA.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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()
Expand Down
37 changes: 24 additions & 13 deletions source/UIAUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import weakref
from functools import lru_cache
from logHandler import log
from _UIAConstants import WinConsoleAPILevel


def createUIAMultiPropertyCondition(*dicts):
Expand Down Expand Up @@ -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
Expand All @@ -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
24 changes: 23 additions & 1 deletion source/_UIAConstants.py
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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
4 changes: 4 additions & 0 deletions user_docs/en/changes.t2t
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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 =
Expand Down