diff --git a/source/NVDAObjects/UIA/winConsoleUIA.py b/source/NVDAObjects/UIA/winConsoleUIA.py index 2bfbcfe92c6..a987c4105e3 100644 --- a/source/NVDAObjects/UIA/winConsoleUIA.py +++ b/source/NVDAObjects/UIA/winConsoleUIA.py @@ -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.""" @@ -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 @@ -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): @@ -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' '}) @@ -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": diff --git a/source/globalCommands.py b/source/globalCommands.py index 03788c8752c..59df3f7714d 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -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. @@ -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. @@ -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. @@ -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. @@ -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. @@ -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. diff --git a/source/textInfos/__init__.py b/source/textInfos/__init__.py index 32c71bb924c..0dc623ff00b 100755 --- a/source/textInfos/__init__.py +++ b/source/textInfos/__init__.py @@ -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.