Skip to content

Commit

Permalink
winConsoleUIATextInfo: use rangeFromPoint instead of broken getVisibl…
Browse files Browse the repository at this point in the history
…eRanges (#10559)

* WinConsoleUIATextInfo:  As IUIAutomationTextPattern::getVisibleRanges is very broken, use IUIAutomationTextPattern::rangeFromPoint for the top left and bottom right of the console window.

* Fix linting issues.

* WinConsoleUIA: override _getTextLines with a faster implementation that just uses Python string splitlines.

* Fix linting issues.

* Don't use isTextRangeOffscreen, and further improve POSITION_LAST to fix review bounds.

* Fix linting issues.

* WinConsoleUIATextInfo: ensure ranges are collapsed after moves.

* Remove TextInfo._expandCollapseBeforeReview as WinConsoleUIA no longer needs it.

* WinConsoleUIATextInfo: when calculating word / line offsets, grab text directly with IUIAutomationTextRange::getText as  WinConsoleUIATextInfo.text has been changed to return a space for empty ranges.

* Address review actions.

* Update source/NVDAObjects/UIA/winConsoleUIA.py

Co-Authored-By: Leonard de Ruijter <leonardder@users.noreply.github.com>
  • Loading branch information
michaelDCurran and LeonarddeR authored Dec 4, 2019
1 parent 2f7a8fb commit 65b0234
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 61 deletions.
102 changes: 63 additions & 39 deletions source/NVDAObjects/UIA/winConsoleUIA.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,35 @@


class consoleUIATextInfo(UIATextInfo):
#: At least on Windows 10 1903, expanding then collapsing the text info
#: caused review to get stuck, so disable it.
#: There may be no need to disable this anymore, but doing so doesn't seem
#: to do much good either.
_expandCollapseBeforeReview = False

def __init__(self, obj, position, _rangeObj=None):
# We want to limit textInfos to just the visible part of the console.
# Therefore we specifically handle POSITION_FIRST, POSITION_LAST and POSITION_ALL.
# We could use IUIAutomationTextRange::getVisibleRanges, but it seems very broken in consoles
# once more than a few screens worth of content has been written to the console.
# Therefore we resort to using IUIAutomationTextPattern::rangeFromPoint
# for the top left, and bottom right of the console window.
if position is textInfos.POSITION_FIRST:
_rangeObj = self.__class__(obj, obj.location.topLeft)._rangeObj
elif position is textInfos.POSITION_LAST:
# Asking for the range at the bottom right of the window
# Seems to sometimes ignore the x coordinate.
# Therefore use the bottom left, then move to the last character on that line.
tempInfo = self.__class__(obj, obj.location.bottomLeft)
tempInfo.expand(textInfos.UNIT_LINE)
# We must pull back the end by one character otherwise when we collapse to end,
# a console bug results in a textRange covering the entire console buffer!
# Strangely the *very* last character is a special blank point
# so we never seem to miss a real character.
UIATextInfo.move(tempInfo, textInfos.UNIT_CHARACTER, -1, endPoint="end")
tempInfo.setEndPoint(tempInfo, "startToEnd")
_rangeObj = tempInfo._rangeObj
elif position is textInfos.POSITION_ALL:
first = self.__class__(obj, textInfos.POSITION_FIRST)
last = self.__class__(obj, textInfos.POSITION_LAST)
first.setEndPoint(last, "endToEnd")
_rangeObj = first._rangeObj
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 1803 and later."""
Expand All @@ -54,14 +63,12 @@ def collapse(self, end=False):
)

def move(self, unit, direction, endPoint=None):
oldRange = None
oldInfo = None
if self.basePosition != textInfos.POSITION_CARET:
# Insure we haven't gone beyond the visible text.
# UIA adds thousands of blank lines to the end of the console.
visiRanges = self.obj.UIATextPattern.GetVisibleRanges()
visiLength = visiRanges.length
if visiLength > 0:
oldRange = self._rangeObj.clone()
boundingInfo = self.obj.makeTextInfo(textInfos.POSITION_ALL)
oldInfo = self.copy()
if unit == textInfos.UNIT_WORD and direction != 0:
# UIA doesn't implement word movement, so we need to do it manually.
# Relative to the current line, calculate our offset
Expand Down Expand Up @@ -116,16 +123,32 @@ def move(self, unit, direction, endPoint=None):
else: # moving by a unit other than word
res = super(consoleUIATextInfo, self).move(unit, direction,
endPoint)
try:
if (
oldRange
and isTextRangeOffscreen(self._rangeObj, visiRanges)
and not isTextRangeOffscreen(oldRange, visiRanges)
):
self._rangeObj = oldRange
return 0
except (COMError, RuntimeError):
pass
if not endPoint:
# #10191: IUIAutomationTextRange::move in consoles does not correctly produce a collapsed range
# after moving.
# Therefore manually collapse.
self.collapse()
# Console textRanges have access to the entire console buffer.
# However, we want to limit ourselves to onscreen text.
# Therefore, if the textInfo was originally visible,
# but we are now above or below the visible range,
# Restore the original textRange and pretend the move didn't work.
if oldInfo:
try:
if (
(
self.compareEndPoints(boundingInfo, "startToStart") < 0
or self.compareEndPoints(boundingInfo, "startToEnd") >= 0
)
and not (
oldInfo.compareEndPoints(boundingInfo, "startToStart") < 0
or oldInfo.compareEndPoints(boundingInfo, "startToEnd") >= 0
)
):
self._rangeObj = oldInfo._rangeObj
return 0
except (COMError, RuntimeError):
pass
return res

def expand(self, unit):
Expand Down Expand Up @@ -207,12 +230,12 @@ def _getCurrentOffsetInThisLine(self, lineInfo):
# position a textInfo from the start of the line up to the current position.
charInfo = lineInfo.copy()
charInfo.setEndPoint(self, "endToStart")
text = charInfo.text
text = charInfo._rangeObj.getText(-1)
offset = textUtils.WideStringOffsetConverter(text).wideStringLength
return offset

def _getWordOffsetsInThisLine(self, offset, lineInfo):
lineText = lineInfo.text or u" "
lineText = lineInfo._rangeObj.getText(-1)
# Convert NULL and non-breaking space to space to make sure
# that words will break on them
lineText = lineText.translate({0: u' ', 0xa0: u' '})
Expand Down Expand Up @@ -282,11 +305,12 @@ def _get_TextInfo(self):
return consoleUIATextInfo

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

# This override of _getTextLines takes advantage of the fact that
# the console text contains linefeeds for every line
# Thus a simple string splitlines is much faster than splitting by unit line.
ti = self.makeTextInfo(textInfos.POSITION_ALL)
text = ti.text or ""
return text.splitlines()

def findExtraOverlayClasses(obj, clsList):
if obj.UIAElement.cachedAutomationId == "Text Area":
Expand Down
30 changes: 12 additions & 18 deletions source/globalCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1026,9 +1026,8 @@ def script_review_top(self,gesture):

def script_review_previousLine(self,gesture):
info=api.getReviewPosition().copy()
if info._expandCollapseBeforeReview:
info.expand(textInfos.UNIT_LINE)
info.collapse()
info.expand(textInfos.UNIT_LINE)
info.collapse()
res=info.move(textInfos.UNIT_LINE,-1)
if res==0:
# Translators: a message reported when review cursor is at the top line of the current navigator object.
Expand Down Expand Up @@ -1058,9 +1057,8 @@ def script_review_currentLine(self,gesture):

def script_review_nextLine(self,gesture):
info=api.getReviewPosition().copy()
if info._expandCollapseBeforeReview:
info.expand(textInfos.UNIT_LINE)
info.collapse()
info.expand(textInfos.UNIT_LINE)
info.collapse()
res=info.move(textInfos.UNIT_LINE,1)
if res==0:
# Translators: a message reported when review cursor is at the bottom line of the current navigator object.
Expand All @@ -1086,9 +1084,8 @@ def script_review_bottom(self,gesture):

def script_review_previousWord(self,gesture):
info=api.getReviewPosition().copy()
if info._expandCollapseBeforeReview:
info.expand(textInfos.UNIT_WORD)
info.collapse()
info.expand(textInfos.UNIT_WORD)
info.collapse()
res=info.move(textInfos.UNIT_WORD,-1)
if res==0:
# Translators: a message reported when review cursor is at the top line of the current navigator object.
Expand Down Expand Up @@ -1117,9 +1114,8 @@ def script_review_currentWord(self,gesture):

def script_review_nextWord(self,gesture):
info=api.getReviewPosition().copy()
if info._expandCollapseBeforeReview:
info.expand(textInfos.UNIT_WORD)
info.collapse()
info.expand(textInfos.UNIT_WORD)
info.collapse()
res=info.move(textInfos.UNIT_WORD,1)
if res==0:
# Translators: a message reported when review cursor is at the bottom line of the current navigator object.
Expand Down Expand Up @@ -1148,9 +1144,8 @@ def script_review_previousCharacter(self,gesture):
lineInfo=api.getReviewPosition().copy()
lineInfo.expand(textInfos.UNIT_LINE)
charInfo=api.getReviewPosition().copy()
if charInfo._expandCollapseBeforeReview:
charInfo.expand(textInfos.UNIT_CHARACTER)
charInfo.collapse()
charInfo.expand(textInfos.UNIT_CHARACTER)
charInfo.collapse()
res=charInfo.move(textInfos.UNIT_CHARACTER,-1)
if res==0 or charInfo.compareEndPoints(lineInfo,"startToStart")<0:
# Translators: a message reported when review cursor is at the leftmost character of the current navigator object's text.
Expand Down Expand Up @@ -1195,9 +1190,8 @@ def script_review_nextCharacter(self,gesture):
lineInfo=api.getReviewPosition().copy()
lineInfo.expand(textInfos.UNIT_LINE)
charInfo=api.getReviewPosition().copy()
if charInfo._expandCollapseBeforeReview:
charInfo.expand(textInfos.UNIT_CHARACTER)
charInfo.collapse()
charInfo.expand(textInfos.UNIT_CHARACTER)
charInfo.collapse()
res=charInfo.move(textInfos.UNIT_CHARACTER,1)
if res==0 or charInfo.compareEndPoints(lineInfo,"endToEnd")>=0:
# Translators: a message reported when review cursor is at the rightmost character of the current navigator object's text.
Expand Down
4 changes: 0 additions & 4 deletions source/textInfos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,6 @@ class TextInfo(baseObject.AutoPropertyObject):
@type bookmark: L{Bookmark}
"""

#: whether this textInfo should be expanded then collapsed around its enclosing unit before review.
#: This can be problematic for some implementations.
_expandCollapseBeforeReview = True

def __init__(self,obj,position):
"""Constructor.
Subclasses must extend this, calling the superclass method first.
Expand Down

0 comments on commit 65b0234

Please sign in to comment.