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: limit blank lines in review and initial word movement support #9647

Merged
merged 18 commits into from
Jun 5, 2019
Merged
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
163 changes: 156 additions & 7 deletions source/NVDAObjects/UIA/winConsoleUIA.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# Copyright (C) 2019 Bill Dengler

import config
import ctypes
import NVDAHelper
import speech
import time
import textInfos
Expand All @@ -21,16 +23,164 @@ class consoleUIATextInfo(UIATextInfo):

def __init__(self, obj, position, _rangeObj=None):
super(consoleUIATextInfo, self).__init__(obj, position, _rangeObj)
if position == textInfos.POSITION_CARET:
if isAtLeastWin10(1903):
# The UIA implementation in 1903 causes the caret to be
# off-by-one, so move it one position to the right
# to compensate.
if position == textInfos.POSITION_CARET and isAtLeastWin10(1903):
# The UIA implementation in 1903 causes the caret to be
# off-by-one, so move it one position to the right
# to compensate.
self._rangeObj.MoveEndpointByUnit(
UIAHandler.TextPatternRangeEndpoint_Start,
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
1
)

def move(self, unit, direction, endPoint=None):
oldRange = 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:
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.
# Relative to the current line, calculate our offset
# and the current word's offsets.
lineInfo = self.copy()
lineInfo.expand(textInfos.UNIT_LINE)
offset = self._getCurrentOffsetInThisLine(lineInfo)
start, end = self._getWordOffsetsInThisLine(offset, lineInfo)
if direction > 0:
# Moving in a forward direction, we can just jump to the
# end offset of the current word and we're done.
res = self.move(
textInfos.UNIT_CHARACTER,
end - offset,
endPoint=endPoint
)
else:
# Moving backwards
wordStartDistance = (offset - start) * -1
if wordStartDistance < 0:
# We are after the beginning of a word.
# So first move back to the start of the word.
self.move(
textInfos.UNIT_CHARACTER,
wordStartDistance,
endPoint=endPoint
)
offset += wordStartDistance
# Try to move one character back before the start of the word.
res = self.move(textInfos.UNIT_CHARACTER, -1, endPoint=endPoint)
if res == 0:
return 0
offset -= 1
# We are now positioned within the previous word.
if offset < 0:
# We've moved on to the previous line.
# Recalculate the current offset based on the new line we are now on.
lineInfo = self.copy()
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)
wordStartDistance = (offset - start) * -1
if wordStartDistance < 0:
self.move(
textInfos.UNIT_CHARACTER,
wordStartDistance,
endPoint=endPoint
)
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
return res

def expand(self, unit):
if unit == textInfos.UNIT_WORD:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expand should use similar code to move. E.g.:
If unit is word, then use _getCurrentOffset and _getWordOffsets and manually move the start and end of the range to have it correctly bound the current word.
Then, there is no need to change what text is exposed as the textRange will be correct.
The advantage of this is there is less to track, it is easier to read, and most importantly, later comparisons or movements of this textInfo won't behave like it is expanded to line even though the text returned is word.
There may be a question around performance: but I'd like to see the code changed to my proposed way before we then consider if optimisations are needed at all.

# UIA doesn't implement word movement, so we need to do it manually.
lineInfo = self.copy()
lineInfo.expand(textInfos.UNIT_LINE)
offset = self._getCurrentOffsetInThisLine(lineInfo)
start, end = self._getWordOffsetsInThisLine(offset, lineInfo)
wordEndPoints = (
LeonarddeR marked this conversation as resolved.
Show resolved Hide resolved
(offset - start) * -1,
end - offset - 1
)
if wordEndPoints[0]:
self._rangeObj.MoveEndpointByUnit(
UIAHandler.TextPatternRangeEndpoint_Start,
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
1
wordEndPoints[0]
)
if wordEndPoints[1]:
self._rangeObj.MoveEndpointByUnit(
UIAHandler.TextPatternRangeEndpoint_End,
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
wordEndPoints[1]
)
else:
return super(consoleUIATextInfo, self).expand(unit)

def _getCurrentOffsetInThisLine(self, lineInfo):
charInfo = self.copy()
res = 0
chars = None
while charInfo.compareEndPoints(
lineInfo,
"startToEnd"
) <= 0:
charInfo.expand(textInfos.UNIT_CHARACTER)
chars = charInfo.move(textInfos.UNIT_CHARACTER, -1) * -1
if chars != 0 and charInfo.compareEndPoints(
lineInfo,
"startToStart"
) >= 0:
res += chars
else:
break
return res

def _getWordOffsetsInThisLine(self, offset, lineInfo):
lineText = lineInfo.text or u" "
# Convert NULL and non-breaking space to space to make sure
# that words will break on them
lineText = lineText.translate({0: u' ', 0xa0: u' '})
start = ctypes.c_int()
end = ctypes.c_int()
# Uniscribe does some strange things when you give it a string with
# not more than two alphanumeric chars in a row.
# Inject two alphanumeric characters at the end to fix this.
lineText += "xx"
NVDAHelper.localLib.calculateWordOffsets(
lineText,
len(lineText),
offset,
ctypes.byref(start),
ctypes.byref(end)
)
return (
start.value,
min(end.value, max(1, len(lineText) - 2))
)


class winConsoleUIA(Terminal):
Expand Down Expand Up @@ -90,7 +240,6 @@ def script_clear_isTyping(self, gesture):

def _getTextLines(self):
# Filter out extraneous empty lines from UIA
# Todo: do this (also) somewhere else so they aren't in document review either
ptr = self.UIATextPattern.GetVisibleRanges()
res = [ptr.GetElement(i).GetText(-1) for i in range(ptr.length)]
return res