-
-
Notifications
You must be signed in to change notification settings - Fork 651
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
Conversation
Note: there are issues when reaching the bottom of the review (cursor gets stuck).
This should mostly restore caret movement support.
…xtInfo._getCurrentOffset. The reverse option has been removed.
6a3f7e3
to
768fe0f
Compare
@@ -39,7 +153,10 @@ class winConsoleUIA(Terminal): | |||
|
|||
def _reportNewText(self, line): | |||
# Additional typed character filtering beyond that in LiveText | |||
if self._isTyping and time.time() - self._lastCharTime <= self._TYPING_TIMEOUT: | |||
if ( |
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.
Even if this is just fixing up code formatting, I don't think this is directly related to this pr, so I'd prefer to see it in one of the other ones that deals directly with speakTypedWords etc.
# 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() | ||
lastVisiRange = visiRanges.GetElement(visiRanges.length - 1) |
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.
It may be possible that visiRanges has a length of 0. It would probably be a bug, but best to check anyway. Only do the next lot of code if length>0.
# UIA adds thousands of blank lines to the end of the console. | ||
visiRanges = self.obj.UIATextPattern.GetVisibleRanges() | ||
lastVisiRange = visiRanges.GetElement(visiRanges.length - 1) | ||
if self._rangeObj.CompareEndPoints( |
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 about also checking that we are not before the first visible range? I.e. we should not allow someone to move off the top of the screen up into previous content.
# UIA adds thousands of blank lines to the end of the console. | ||
visiRanges = self.obj.UIATextPattern.GetVisibleRanges() | ||
lastVisiRange = visiRanges.GetElement(visiRanges.length - 1) | ||
if self._rangeObj.CompareEndPoints( |
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 think I would prefer to do this check after the call to super, and then restore the textRange if we have moved too far. If my logic is correct, I'm pretty sure that this current code will still allow moving to the first line after the last visible range? Because when on the last line, compareEndPoints textInfo's start with lastVisiRange's end would not yet be >=0.
So before super, I'd save off a copy of _rangeObj with something like:
oldRange=self._rangeObj.clone()
Then after super, do these checks, and if we have moved out of range, then replace self._rangeObj with oldRange, and return 0.
return 0 | ||
if unit == textInfos.UNIT_WORD and direction != 0: | ||
# UIA doesn't implement word movement, so we need to do it manually. | ||
offset = self._getCurrentOffset() |
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'd like to see this method better named to reflect that this is talking about within the current line. Something like _getCurrentOffsetInCurrentLine.
# UIA doesn't implement word movement, so we need to do it manually. | ||
offset = self._getCurrentOffset() | ||
index = 1 if direction > 0 else 0 | ||
start, end = self._getWordOffsets(offset) |
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.
Again: perhaps call it _getWordOffsetsInCurrentLine
if res != 0: | ||
return direction | ||
else: | ||
if self.move(textInfos.UNIT_CHARACTER, -1): |
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.
Can you clarify what this does exactly and why it is necessary?
return super(consoleUIATextInfo, self).move(unit, direction, endPoint) | ||
|
||
def expand(self, unit): | ||
if unit == textInfos.UNIT_WORD: |
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.
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.
Note: this new approach includes characters after a line break in the current word.
@@ -140,8 +151,8 @@ def _getCurrentWord(self): | |||
lineInfo = self.copy() |
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 think _getCurrentWord can be removed now as it is no longer used?
self._rangeObj.MoveEndpointByUnit( | ||
UIAHandler.TextPatternRangeEndpoint_Start, | ||
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER], | ||
wordEndPoints[0] |
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 happens if wordEndPoints[0] or wordEndPoints[1] is 0? I.e. you don't ned to move? perhaps these moveByUnit calls should be within an if conndition checking that wordEndPoints[x] is not 0.
when moving by word, the first two characters of the next line are included in the last word on a line. |
master will need to be merged into this branch, and any conflicts resolved. |
@michaelDCurran It's almost perfect now (I think the word movement bugs we were talking about earlier are fixed by the new implementation of |
@@ -17,10 +19,12 @@ | |||
|
|||
class consoleUIATextInfo(UIATextInfo): | |||
_expandCollapseBeforeReview = False | |||
_isCaret = False |
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 think this is necessary, see below.
@@ -31,6 +35,121 @@ def __init__(self, obj, position, _rangeObj=None): | |||
1 | |||
) | |||
|
|||
def move(self, unit, direction, endPoint=None): | |||
oldRange=None | |||
if not self._isCaret: |
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.
Note that every TextInfo instance has an attribute basePosition
that should be textInfos.POSITION_CARET. Therefore, I think I'd prefer this:
if not self._isCaret: | |
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: |
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:
if visiRanges.length > 0: | |
visibleRangesLength = visiRanges.Length | |
if visibleRangesLength > 0: |
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
charInfo = self.copy() | ||
res = 0 | ||
chars = None | ||
while True: |
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.
Recent experiences with Notepad++ made me a bit worried about endless while loops. Could you limit this somehow?
Would something like this work?
while True: | |
while charInfo.compareEndPoints( | |
lineInfo, | |
"startToEnd" | |
) <= 0: |
else: | ||
return super(consoleUIATextInfo, self).expand(unit) | ||
|
||
def _getCurrentOffsetInThisLine(self): |
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.
In most if not all cases, you're calling _getCurrentOffsetInThisLine and then _getWordOffsetsInThisLine, which both copy self and expand to a line. May be lineInfo could be an argument to both functions to avoid this? Of course, it should still only be used when self is collapsed within that line
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.
When moving forward by word and you land on a blank line, moving forward by word again gets stuck on the blank line. The reason for this is that although UIA textRange's treat a blank line as one character unit, the text it gives back for this unit is of 0 length. Therefore getWordOffsetsInThisLine ends up giving back 0,0.
When fetching the line text in getWordOffsetInThisLine, set lineText to u" " if lineText is of 0 length. Easiest way is to do:
lineText=info.text or u" "
@michaelDCurran Thanks. Now, when moving forward by word, we can land (and pass over) the blank, but moving back by word skips over it. I think this is pretty minor though and this PR is ready for another review/merge. |
…nger jump over the last word on the previous line.
I think I have come up with a fix for moving previous word that does not skip the last word of the previous line.
|
If you are happy with the code, and can follow the logic okay, please merge it into this pr and I will then approve and merge this pr to master. |
This would have been a lot easier to write just using expand and collapse calls within move, but Microsoft's UIA implementation for consoles seems to not handle collapsing properly, as you found with the review cursor scripts previously. |
… cmduia2-textinfos
@michaelDCurran Looks good to me! |
Link to issue number:
Split from #9646 (builds on #9614).
Summary of the issue:
Currently, in consoles with UI Automation enabled:
Description of how this pull request fixes the issue:
Word movement and blank line filtering have been implemented in an overridden
move
method onconsoleUIATextInfo
. Since the console's UIA implementation does not provide word movement, we must use an offset-based word movement algorithm which uses the Uniscribe API to find word boundaries. The algorithm used for this implementation is based on that inNVDAObjects.offsets._getWordOffsets
. Blank lines are filtered by comparing thetextInfo
's_rangeObj
to the last visible text range (returned bywinConsoleUIA.UIATextPattern.GetVisibleRanges
).Testing performed:
Tested text review on Windows 10 versions 1803 and 1903.
Known issues with pull request:
See #9646 for all console UIA known issues to date.
Change log entry:
None.