Skip to content
Merged
Changes from 7 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
120 changes: 118 additions & 2 deletions source/NVDAObjects/UIA/winConsoleUIA.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# See the file COPYING for more details.
# Copyright (C) 2019 Bill Dengler

import ctypes
import NVDAHelper
import time
import textInfos
import UIAHandler
Expand All @@ -16,10 +18,13 @@

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.

_expandedToWord = False

def __init__(self, obj, position, _rangeObj=None):
super(consoleUIATextInfo, self).__init__(obj, position, _rangeObj)
if position == textInfos.POSITION_CARET:
self._isCaret = True
if isAtLeastWin10(1903):
# The UIA implementation in 1903 causes the caret to be
# off-by-one, so move it one position to the right
Expand All @@ -30,6 +35,115 @@ def __init__(self, obj, position, _rangeObj=None):
1
)

def move(self, unit, direction, endPoint=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()
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.

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.

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.

UIAHandler.TextPatternRangeEndpoint_Start,
lastVisiRange,
UIAHandler.TextPatternRangeEndpoint_End
) >= 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.

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

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):
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 self.move(unit, direction, endPoint=endPoint)
else:
return res
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.

self._expandedToWord = True
else:
self._expandedToWord = False
return super(consoleUIATextInfo, self).expand(unit)

def collapse(self):
self._expandedToWord = False
return super(consoleUIATextInfo, self).collapse()

def getTextWithFields(self, formatConfig=None):
if self._expandedToWord:
return [self.text]
return super(consoleUIATextInfo, self).getTextWithFields(
formatConfig=formatConfig
)

def _get_text(self):
if self._expandedToWord:
return self._getCurrentWord()
return super(consoleUIATextInfo, self)._get_text()

def _getCurrentOffset(self):
lineInfo = self.copy()
lineInfo.expand(textInfos.UNIT_LINE)
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:

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 _getWordOffsets(self, offset):
lineInfo = self.copy()
lineInfo.expand(textInfos.UNIT_LINE)
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))
)

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?

lineInfo.expand(textInfos.UNIT_LINE)
lineText = lineInfo.text
offset = self._getCurrentOffset()
start, end = self._getWordOffsets(offset)
return lineText[start:end]


class winConsoleUIA(Terminal):
_TextInfo = consoleUIATextInfo
Expand All @@ -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.

self._isTyping
and time.time() - self._lastCharTime <= self._TYPING_TIMEOUT
):
return
super(winConsoleUIA, self)._reportNewText(line)

Expand All @@ -56,7 +173,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