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: backport some UIA console fixes to Python 2 (2019.2.1) #10274

Closed
wants to merge 12 commits into from
55 changes: 39 additions & 16 deletions source/NVDAObjects/UIA/winConsoleUIA.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import textInfos
import UIAHandler

from comtypes import COMError
from scriptHandler import script
from UIAUtils import isTextRangeOffscreen
from winVersion import isWin10
from . import UIATextInfo
from ..behaviors import Terminal
Expand All @@ -26,6 +28,21 @@ class consoleUIATextInfo(UIATextInfo):
#: to do much good either.
_expandCollapseBeforeReview = False

def __init__(self,obj,position,_rangeObj=None):
super(consoleUIATextInfo, self).__init__(obj, position, _rangeObj)
# Re-implement POSITION_FIRST and POSITION_LAST in terms of
# visible ranges to fix review top/bottom scripts.
if position==textInfos.POSITION_FIRST:
visiRanges = self.obj.UIATextPattern.GetVisibleRanges()
firstVisiRange = visiRanges.GetElement(0)
self._rangeObj = firstVisiRange
self.collapse()
elif position==textInfos.POSITION_LAST:
visiRanges = self.obj.UIATextPattern.GetVisibleRanges()
lastVisiRange = visiRanges.GetElement(visiRanges.length - 1)
self._rangeObj = lastVisiRange
self.collapse(True)

def collapse(self,end=False):
"""Works around a UIA bug on Windows 10 1903 and later."""
if not isWin10(1903):
Expand All @@ -50,8 +67,6 @@ def move(self, unit, direction, endPoint=None):
visiRanges = self.obj.UIATextPattern.GetVisibleRanges()
visiLength = visiRanges.length
if visiLength > 0:
firstVisiRange = visiRanges.GetElement(0)
lastVisiRange = visiRanges.GetElement(visiLength - 1)
oldRange = self._rangeObj.clone()
if unit == textInfos.UNIT_WORD and direction != 0:
# UIA doesn't implement word movement, so we need to do it manually.
Expand Down Expand Up @@ -94,7 +109,6 @@ def move(self, unit, direction, endPoint=None):
lineInfo.expand(textInfos.UNIT_LINE)
offset = self._getCurrentOffsetInThisLine(lineInfo)
# Finally using the new offset,

# Calculate the current word offsets and move to the start of
# this word if we are not already there.
start, end = self._getWordOffsetsInThisLine(offset, lineInfo)
Expand All @@ -108,15 +122,16 @@ def move(self, unit, direction, endPoint=None):
else: # moving by a unit other than word
res = super(consoleUIATextInfo, self).move(unit, direction,
endPoint)
if oldRange and (
self._rangeObj.CompareEndPoints(
UIAHandler.TextPatternRangeEndpoint_Start, firstVisiRange,
UIAHandler.TextPatternRangeEndpoint_Start) < 0
or self._rangeObj.CompareEndPoints(
UIAHandler.TextPatternRangeEndpoint_Start, lastVisiRange,
UIAHandler.TextPatternRangeEndpoint_End) >= 0):
self._rangeObj = oldRange
return 0
try:
if (
oldRange
and isTextRangeOffscreen(self._rangeObj, visiRanges)
and not isTextRangeOffscreen(oldRange, visiRanges)
):
self._rangeObj = oldRange
return 0
except COMError, RuntimeError:
pass
return res

def expand(self, unit):
Expand Down Expand Up @@ -229,7 +244,6 @@ class WinConsoleUIA(Terminal):
#: Only process text changes every 30 ms, in case the console is getting
#: a lot of text.
STABILIZE_DELAY = 0.03
_TextInfo = consoleUIATextInfo
#: A queue of typed characters, to be dispatched on C{textChange}.
#: This queue allows NVDA to suppress typed passwords when needed.
_queuedChars = []
Expand All @@ -239,6 +253,12 @@ class WinConsoleUIA(Terminal):
#: the caret in consoles can take a while to move on Windows 10 1903 and later.
_caretMovementTimeoutMultiplier = 1.5

def _get_TextInfo(self):
"""Overriding _get_TextInfo and thus the TextInfo property on NVDAObjects.UIA.UIA
consoleUIATextInfo fixes expand/collapse, implements word movement, and
bounds review to the visible text."""
return consoleUIATextInfo

def _reportNewText(self, line):
# Additional typed character filtering beyond that in LiveText
if len(line.strip()) < max(len(speech.curWordChars) + 1, 3):
Expand Down Expand Up @@ -292,9 +312,12 @@ def script_flush_queuedChars(self, gesture):

def _getTextLines(self):
# Filter out extraneous empty lines from UIA
ptr = self.UIATextPattern.GetVisibleRanges()
res = [ptr.GetElement(i).GetText(-1) for i in range(ptr.length)]
return res
return (
self.makeTextInfo(textInfos.POSITION_ALL)
._rangeObj.getText(-1)
.rstrip()
.split("\r\n")
)

def _calculateNewText(self, newLines, oldLines):
self._hasNewLines = (
Expand Down
17 changes: 17 additions & 0 deletions source/UIAUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,23 @@ def getChildrenWithCacheFromUIATextRange(textRange,cacheRequest):
c=CacheableUIAElementArray(c)
return c

def isTextRangeOffscreen(textRange, visiRanges):
"""Given a UIA text range and a visible textRanges array (returned from obj.UIATextPattern.GetVisibleRanges), determines if the given textRange is not within the visible textRanges."""
visiLength = visiRanges.length
if visiLength > 0:
firstVisiRange = visiRanges.GetElement(0)
lastVisiRange = visiRanges.GetElement(visiLength - 1)
return textRange.CompareEndPoints(
UIAHandler.TextPatternRangeEndpoint_Start, firstVisiRange,
UIAHandler.TextPatternRangeEndpoint_Start
) < 0 or textRange.CompareEndPoints(
UIAHandler.TextPatternRangeEndpoint_Start, lastVisiRange,
UIAHandler.TextPatternRangeEndpoint_End) >= 0
else:
# Visible textRanges not available.
raise RuntimeError("Visible textRanges array is empty or invalid.")


class UIATextRangeAttributeValueFetcher(object):

def __init__(self,textRange):
Expand Down
5 changes: 2 additions & 3 deletions source/globalCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1092,9 +1092,8 @@ def script_review_nextWord(self,gesture):

def script_review_startOfLine(self,gesture):
info=api.getReviewPosition().copy()
if info._expandCollapseBeforeReview:
info.expand(textInfos.UNIT_LINE)
info.collapse()
info.expand(textInfos.UNIT_LINE)
info.collapse()
api.setReviewPosition(info)
info.expand(textInfos.UNIT_CHARACTER)
ui.reviewMessage(_("Left"))
Expand Down