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: Remove "Text area", replace isAtLeastWin10, and code cleanup #9761

Merged
merged 4 commits into from
Jun 17, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
70 changes: 44 additions & 26 deletions source/NVDAObjects/UIA/winConsoleUIA.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@
import UIAHandler

from scriptHandler import script
from winVersion import isAtLeastWin10
from winVersion import isWin10
from . import UIATextInfo
from ..behaviors import Terminal
from ..window import Window


class consoleUIATextInfo(UIATextInfo):
#: At least on Windows 10 1903, expanding then collapsing the text info
#: causes review to get stuck, so disable it.
_expandCollapseBeforeReview = False

def __init__(self, obj, position, _rangeObj=None):
super(consoleUIATextInfo, self).__init__(obj, position, _rangeObj)
if position == textInfos.POSITION_CARET and isAtLeastWin10(1903):
if position == textInfos.POSITION_CARET and isWin10(1903, atLeast=False):
# The UIA implementation in 1903 causes the caret to be
# off-by-one, so move it one position to the right
# to compensate.
Expand Down Expand Up @@ -98,19 +100,15 @@ def move(self, unit, direction, endPoint=None):
endPoint=endPoint
)
else: # moving by a unit other than word
res = super(consoleUIATextInfo, self).move(unit, direction, endPoint)
res = super(consoleUIATextInfo, self).move(unit, direction,
endPoint)
if oldRange and (
self._rangeObj.CompareEndPoints(
UIAHandler.TextPatternRangeEndpoint_Start,
firstVisiRange,
UIAHandler.TextPatternRangeEndpoint_Start
) < 0
UIAHandler.TextPatternRangeEndpoint_Start, firstVisiRange,
UIAHandler.TextPatternRangeEndpoint_Start) < 0
or self._rangeObj.CompareEndPoints(
UIAHandler.TextPatternRangeEndpoint_Start,
lastVisiRange,
UIAHandler.TextPatternRangeEndpoint_End
) >= 0
):
UIAHandler.TextPatternRangeEndpoint_Start, lastVisiRange,
UIAHandler.TextPatternRangeEndpoint_End) >= 0):
self._rangeObj = oldRange
return 0
return res
Expand Down Expand Up @@ -142,6 +140,12 @@ def expand(self, unit):
return super(consoleUIATextInfo, self).expand(unit)

def _getCurrentOffsetInThisLine(self, lineInfo):
"""
Given a caret textInfo expanded to line, returns the index into the
line where the caret is located.
This is necessary since Uniscribe requires indices into the text to
find word boundaries, but UIA only allows for relative movement.
"""
charInfo = self.copy()
res = 0
chars = None
Expand Down Expand Up @@ -187,21 +191,29 @@ def _getWordOffsetsInThisLine(self, offset, lineInfo):
class consoleUIAWindow(Window):
def _get_focusRedirect(self):
"""
Sometimes, attempting to interact with the console too quickly after
focusing the window can make NVDA unable to get any caret or review
information or receive new text events.
To work around this, we must redirect focus to the console text area.
Sometimes, attempting to interact with the console too quickly after
focusing the window can make NVDA unable to get any caret or review
information or receive new text events.
To work around this, we must redirect focus to the console text area.
"""
for child in self.children:
if isinstance(child, winConsoleUIA):
if isinstance(child, WinConsoleUIA):
return child
return None


class winConsoleUIA(Terminal):
class WinConsoleUIA(Terminal):
#: Disable the name as it won't be localized
name = ""
#: Only process text changes every 30 ms, in case the console is getting
#: a lot of text.
STABILIZE_DELAY = 0.03
_TextInfo = consoleUIATextInfo
#: A queue of typed characters, to be dispatched on C{textChange}.
#: This queue allows NVDA to suppress typed passwords when needed.
_queuedChars = []
#: Whether the console got new text lines in its last update.
#: Used to determine if typed character/word buffers should be flushed.
_hasNewLines = False

def _reportNewText(self, line):
Expand All @@ -213,7 +225,7 @@ def _reportNewText(self, line):
# This will need to be changed once #8110 is merged.
speech.curWordChars = []
self._queuedChars = []
super(winConsoleUIA, self)._reportNewText(line)
super(WinConsoleUIA, self)._reportNewText(line)

def event_typedCharacter(self, ch):
if ch == '\t':
Expand All @@ -229,13 +241,13 @@ def event_typedCharacter(self, ch):
):
self._queuedChars.append(ch)
else:
super(winConsoleUIA, self).event_typedCharacter(ch)
super(WinConsoleUIA, self).event_typedCharacter(ch)

def event_textChange(self):
while self._queuedChars:
ch = self._queuedChars.pop(0)
super(winConsoleUIA, self).event_typedCharacter(ch)
super(winConsoleUIA, self).event_textChange()
super(WinConsoleUIA, self).event_typedCharacter(ch)
super(WinConsoleUIA, self).event_textChange()

@script(gestures=[
"kb:enter",
Expand All @@ -247,11 +259,13 @@ def event_textChange(self):
])
def script_flush_queuedChars(self, gesture):
"""
Flushes the queue of typedCharacter events if present.
This is necessary to avoid speaking of passwords in the console if disabled.
Flushes the typed word buffer and queue of typedCharacter events if present.
Since these gestures clear the current word/line, we should flush the
queue to avoid erroneously reporting these chars.
"""
gesture.send()
self._queuedChars = []
speech.curWordChars = []

def _getTextLines(self):
# Filter out extraneous empty lines from UIA
Expand All @@ -264,13 +278,17 @@ def _calculateNewText(self, newLines, oldLines):
self._findNonBlankIndices(newLines)
!= self._findNonBlankIndices(oldLines)
)
return super(winConsoleUIA, self)._calculateNewText(newLines, oldLines)
return super(WinConsoleUIA, self)._calculateNewText(newLines, oldLines)

def _findNonBlankIndices(self, lines):
"""
Given a list of strings, returns a list of indices where the strings
are not empty.
"""
return [index for index, line in enumerate(lines) if line]

def findExtraOverlayClasses(obj, clsList):
if obj.UIAElement.cachedAutomationId == "Text Area":
clsList.append(winConsoleUIA)
clsList.append(WinConsoleUIA)
elif obj.UIAElement.cachedAutomationId == "Console Window":
clsList.append(consoleUIAWindow)
2 changes: 1 addition & 1 deletion source/_UIAHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
UIA_SystemAlertEventId:"UIA_systemAlert",
}

if winVersion.isAtLeastWin10():
if winVersion.isWin10():
UIAEventIdsToNVDAEventNames[UIA_Text_TextChangedEventId] = "textChange"

ignoreWinEventsMap = {
Expand Down
6 changes: 3 additions & 3 deletions source/keyboardHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,10 @@ def internal_keyDownEvent(vkCode,scanCode,extended,injected):
# #6017: handle typed characters in Win10 RS2 and above where we can't detect typed characters in-process
# This code must be in the 'finally' block as code above returns in several places yet we still want to execute this particular code.
focus=api.getFocusObject()
from NVDAObjects.UIA.winConsoleUIA import winConsoleUIA
from NVDAObjects.UIA.winConsoleUIA import WinConsoleUIA
if (
# This is only possible in Windows 10 RS2 and above
winVersion.isAtLeastWin10(1703)
winVersion.isWin10(1703)
# And we only want to do this if the gesture did not result in an executed action
and not gestureExecuted
# and not if this gesture is a modifier key
Expand All @@ -212,7 +212,7 @@ def internal_keyDownEvent(vkCode,scanCode,extended,injected):
# or the focus is within a UWP app, where WM_CHAR never gets sent
or focus.windowClassName.startswith('Windows.UI.Core')
#Or this is a UIA console window, where WM_CHAR messages are doubled
or isinstance(focus, winConsoleUIA)
or isinstance(focus, WinConsoleUIA)
)
):
keyStates=(ctypes.c_byte*256)()
Expand Down
16 changes: 11 additions & 5 deletions source/winVersion.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ def canRunVc2010Builds():
def isUwpOcrAvailable():
return os.path.isdir(UWP_OCR_DATA_PATH)

def isAtLeastWin10(version=1507):
def isWin10(version=1507, atLeast=True):
"""
Returns True if NVDA is running on at least the supplied release version of Windows 10. If no argument is supplied, returns True for all public Windows 10 releases.
Note: this function will always return False for source copies of NVDA due to a Python bug.
Returns True if NVDA is running on the supplied release version of Windows 10. If no argument is supplied, returns True for all public Windows 10 releases.
@note: this function will always return False for source copies of NVDA due to a Python bug.
@param version: a release version of Windows 10 (such as 1903).
@param atLeast: return True if NVDA is running on at least this Windows 10 build (i.e. this version or higher).
"""
from logHandler import log
win10VersionsToBuilds={
Expand All @@ -44,10 +45,15 @@ def isAtLeastWin10(version=1507):
1809: 17763,
1903: 18362
}
if winVersion.major < 10:
if atLeast and winVersion.major < 10:
return False
elif not atLeast and winVersion.major != 10:
return False
try:
return winVersion.build >= win10VersionsToBuilds[version]
if atLeast:
return winVersion.build >= win10VersionsToBuilds[version]
else:
return winVersion.build == win10VersionsToBuilds[version]
except KeyError:
log.error("Unknown Windows 10 version {}".format(version))
return False