Skip to content
Merged
Changes from 11 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: 119 additions & 1 deletion 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 speech
import time
import textInfos
Expand All @@ -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.


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 @@ -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

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.
offset = self._getCurrentOffsetInThisLine()
index = 1 if direction > 0 else 0
start, end = self._getWordOffsetsInThisLine(offset)
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:
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.
offset = self._getCurrentOffsetInThisLine()
start, end = self._getWordOffsetsInThisLine(offset)
wordEndPoints = (
(offset - start) * -1,
end - offset - 1
)
if wordEndPoints[0]:
self._rangeObj.MoveEndpointByUnit(
UIAHandler.TextPatternRangeEndpoint_Start,
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
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):
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

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 _getWordOffsetsInThisLine(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) - 2)
)


class winConsoleUIA(Terminal):
STABILIZE_DELAY = 0.03
Expand Down Expand Up @@ -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