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

Conversation

codeofdusk
Copy link
Contributor

Link to issue number:

Split from #9646 (builds on #9614).

Summary of the issue:

Currently, in consoles with UI Automation enabled:

  • In object review, commands for moving to and reporting words move to or report lines instead.
  • A large number (thousands in many cases) of empty lines are appended to the console's object review.

Description of how this pull request fixes the issue:

Word movement and blank line filtering have been implemented in an overridden move method on consoleUIATextInfo. 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 in NVDAObjects.offsets._getWordOffsets. Blank lines are filtered by comparing the textInfo's _rangeObj to the last visible text range (returned by winConsoleUIA.UIATextPattern.GetVisibleRanges).

Testing performed:

Tested text review on Windows 10 versions 1803 and 1903.

Known issues with pull request:

  • While "review previous word" and "review next word" now allow word movement, the cursor is not consistently placed on the first character of the word under review.

See #9646 for all console UIA known issues to date.

Change log entry:

None.

@codeofdusk codeofdusk changed the title UI Automation in Windows Console: only enable UIA when available UI Automation in Windows Console: limit blank lines in review and initial word movement support May 31, 2019
@codeofdusk
Copy link
Contributor Author

@@ -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 (
Copy link
Member

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)
Copy link
Member

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(
Copy link
Member

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(
Copy link
Member

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()
Copy link
Member

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)
Copy link
Member

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):
Copy link
Member

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:
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.

Note: this new approach includes characters after a line break in the current word.
@@ -140,8 +151,8 @@ def _getCurrentWord(self):
lineInfo = self.copy()
Copy link
Member

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]
Copy link
Member

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.

@michaelDCurran
Copy link
Member

when moving by word, the first two characters of the next line are included in the last word on a line.
The reason for this is to do with the fact that an extra two alphanumeric characters are placed at the end of the text with give to calculateWordOffsets to get around a uniscribe bug. However, when returning the word offsets, len(lineText) is used. This should be len(lineText)-2 as we don't want to include the inserted characters.

@michaelDCurran
Copy link
Member

master will need to be merged into this branch, and any conflicts resolved.

@codeofdusk
Copy link
Contributor Author

@michaelDCurran It's almost perfect now (I think the word movement bugs we were talking about earlier are fixed by the new implementation of expand). Only problem now is when you move past the last word on a line, NVDA repeatedly says "blank" (moving to the previous word puts us on the last line though).

@@ -17,10 +19,12 @@

class consoleUIATextInfo(UIATextInfo):
_expandCollapseBeforeReview = False
_isCaret = False
Copy link
Collaborator

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:
Copy link
Collaborator

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:

Suggested change
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:
Copy link
Collaborator

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:

Suggested change
if visiRanges.length > 0:
visibleRangesLength = visiRanges.Length
if visibleRangesLength > 0:

Copy link
Contributor Author

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.

Copy link
Collaborator

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

source/NVDAObjects/UIA/winConsoleUIA.py Show resolved Hide resolved
charInfo = self.copy()
res = 0
chars = None
while True:
Copy link
Collaborator

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?

Suggested change
while True:
while charInfo.compareEndPoints(
lineInfo,
"startToEnd"
) <= 0:

else:
return super(consoleUIATextInfo, self).expand(unit)

def _getCurrentOffsetInThisLine(self):
Copy link
Collaborator

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

Copy link
Member

@michaelDCurran michaelDCurran left a 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" "

@codeofdusk
Copy link
Contributor Author

@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.
@michaelDCurran
Copy link
Member

michaelDCurran commented Jun 5, 2019

I think I have come up with a fix for moving previous word that does not skip the last word of the previous line.
You can find the code in the pr9647_fixMovePreviousWord branch on nvaccess/nvda repository.
I have provided comments through out the code so you can hopefully understand the logic.
It was rather tricky to get right. Essentially it no lonver recursively calls move within the algorithm as in some cases that was happening too many times, plus the steps are now broken down into very small / easy to understand parts.
In short the steps are now:

  • Relative to the current line, calculate our offset and the current word's offsets.
  • If moving in a forward direction, we can just jump to the end offset of the current word and we're done.
  • However, if moving backwards:
    • If we are after the beginning of a word, first move back to the start of the word.
    • Try to move one character back before the start of the word.
    • If we have moved on to a previous line, recalculate our new offset for the new line.
    • Finally using the offset, Calculate the current word offsets and move to the start of this word if we are not already there.

@michaelDCurran
Copy link
Member

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.

@michaelDCurran
Copy link
Member

michaelDCurran commented Jun 5, 2019

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.

@codeofdusk
Copy link
Contributor Author

@michaelDCurran Looks good to me!

@michaelDCurran michaelDCurran merged commit 06539e6 into nvaccess:master Jun 5, 2019
@nvaccessAuto nvaccessAuto added this to the 2019.3 milestone Jun 5, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants