-
-
Notifications
You must be signed in to change notification settings - Fork 634
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
UI Automation in Windows Console: limit blank lines in review and initial word movement support #9647
Changes from 12 commits
41d4816
53ecca9
a9575ba
1cf0563
6083ded
768fe0f
9a8835c
bc55777
6f52524
c2386b9
c016ab1
6557f04
4e9bbe0
b12cbc9
e4e96ef
d00a15b
1df93fa
9fef531
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,8 @@ | |
# See the file COPYING for more details. | ||
# Copyright (C) 2019 Bill Dengler | ||
|
||
import ctypes | ||
import NVDAHelper | ||
import speech | ||
import time | ||
import textInfos | ||
|
@@ -20,16 +22,133 @@ 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() | ||
if visiRanges.length > 0: | ||
firstVisiRange = visiRanges.GetElement(0) | ||
lastVisiRange = visiRanges.GetElement(visiRanges.length - 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. | ||
lineInfo = self.copy() | ||
lineInfo.expand(textInfos.UNIT_LINE) | ||
offset = self._getCurrentOffsetInThisLine(lineInfo) | ||
index = 1 if direction > 0 else 0 | ||
start, end = self._getWordOffsetsInThisLine(offset, lineInfo) | ||
wordMoveDirections = ( | ||
(offset - start) * -1, | ||
end - offset | ||
) | ||
res = self.move( | ||
textInfos.UNIT_CHARACTER, | ||
wordMoveDirections[index], | ||
endPoint=endPoint | ||
) | ||
if res != 0: | ||
return direction | ||
else: | ||
if self.move(textInfos.UNIT_CHARACTER, -1): # Reset word boundaries to move to the previous word | ||
return self.move(unit, direction, endPoint=endPoint) | ||
else: | ||
return res | ||
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Expand should use similar code to move. E.g.: |
||
# 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 | ||
# 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, len(lineText) - 2) | ||
) | ||
|
||
|
||
class winConsoleUIA(Terminal): | ||
|
@@ -72,7 +191,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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how caching works here, but it might make sense to do something like this:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What exactly is the advantage of doing this? We only use the value once per function call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're also getting the length for lastVisiRange