diff --git a/source/NVDAObjects/UIA/winConsoleUIA.py b/source/NVDAObjects/UIA/winConsoleUIA.py index ba725d1ca3f..0fedbdfaaa6 100644 --- a/source/NVDAObjects/UIA/winConsoleUIA.py +++ b/source/NVDAObjects/UIA/winConsoleUIA.py @@ -21,19 +21,25 @@ class consoleUIATextInfo(UIATextInfo): #: At least on Windows 10 1903, expanding then collapsing the text info - #: causes review to get stuck, so disable it. + #: caused review to get stuck, so disable it. + #: There may be no need to disable this anymore, but doing so doesn't seem + #: to do much good either. _expandCollapseBeforeReview = False - def __init__(self, obj, position, _rangeObj=None): - super(consoleUIATextInfo, self).__init__(obj, position, _rangeObj) - 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. - self._rangeObj.MoveEndpointByUnit( + def collapse(self,end=False): + """Works around a UIA bug on Windows 10 1903 and later.""" + if not isWin10(1903): + return super(consoleUIATextInfo, self).collapse(end=end) + # When collapsing, consoles seem to incorrectly push the start of the + # textRange back one character. + # Correct this by bringing the start back up to where the end is. + oldInfo=self.copy() + super(consoleUIATextInfo,self).collapse() + if not end: + self._rangeObj.MoveEndpointByRange( UIAHandler.TextPatternRangeEndpoint_Start, - UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER], - 1 + oldInfo._rangeObj, + UIAHandler.TextPatternRangeEndpoint_Start ) def move(self, unit, direction, endPoint=None): @@ -139,6 +145,17 @@ def expand(self, unit): else: return super(consoleUIATextInfo, self).expand(unit) + def _get_isCollapsed(self): + """Works around a UIA bug on Windows 10 1903 and later.""" + if not isWin10(1903): + return super(consoleUIATextInfo, self)._get_isCollapsed() + # Even when a console textRange's start and end have been moved to the + # same position, the console incorrectly reports the end as being + # past the start. + # Therefore to decide if the textRange is collapsed, + # Check if it has no text. + return not bool(self._rangeObj.getText(1)) + def _getCurrentOffsetInThisLine(self, lineInfo): """ Given a caret textInfo expanded to line, returns the index into the @@ -187,6 +204,9 @@ def _getWordOffsetsInThisLine(self, offset, lineInfo): min(end.value, max(1, len(lineText) - 2)) ) + def __ne__(self,other): + return not self==other + class consoleUIAWindow(Window): def _get_focusRedirect(self): @@ -215,6 +235,8 @@ class WinConsoleUIA(Terminal): #: Whether the console got new text lines in its last update. #: Used to determine if typed character/word buffers should be flushed. _hasNewLines = False + #: the caret in consoles can take a while to move on Windows 10 1903 and later. + _caretMovementTimeoutMultiplier = 2 def _reportNewText(self, line): # Additional typed character filtering beyond that in LiveText diff --git a/source/_UIAHandler.py b/source/_UIAHandler.py index 91209981952..f45b907fadc 100644 --- a/source/_UIAHandler.py +++ b/source/_UIAHandler.py @@ -151,7 +151,10 @@ } if winVersion.isWin10(): - UIAEventIdsToNVDAEventNames[UIA_Text_TextChangedEventId] = "textChange" + UIAEventIdsToNVDAEventNames.update({ + UIA_Text_TextChangedEventId: "textChange", + UIA_Text_TextSelectionChangedEventId:"caret" + }) ignoreWinEventsMap = { UIA_AutomationPropertyChangedEventId: list(UIAPropertyIdsToNVDAEventNames.keys()), diff --git a/source/editableText.py b/source/editableText.py index a8e5fd5e518..db948ce88bc 100755 --- a/source/editableText.py +++ b/source/editableText.py @@ -48,6 +48,8 @@ class EditableText(TextContainerObject,ScriptableObject): _hasCaretMoved_minWordTimeoutMs=30 #: The minimum amount of time that should elapse before checking if the word under the caret has changed + _caretMovementTimeoutMultiplier = 1 + def _hasCaretMoved(self, bookmark, retryInterval=0.01, timeout=None, origWord=None): """ Waits for the caret to move, for a timeout to elapse, or for a new focus event or script to be queued. @@ -69,6 +71,7 @@ def _hasCaretMoved(self, bookmark, retryInterval=0.01, timeout=None, origWord=No else: # This function's arguments are in seconds, but we want ms. timeoutMs = timeout * 1000 + timeoutMs *= self._caretMovementTimeoutMultiplier # time.sleep accepts seconds, so retryInterval is in seconds. # Convert to integer ms to avoid floating point precision errors when adding to elapsed. retryMs = int(retryInterval * 1000) @@ -81,6 +84,10 @@ def _hasCaretMoved(self, bookmark, retryInterval=0.01, timeout=None, origWord=No if eventHandler.isPendingEvents("gainFocus"): log.debug("Focus event. Elapsed: %d ms" % elapsed) return (True,None) + # Some controls implement a caret event + if eventHandler.isPendingEvents("caret"): + newInfo = self.makeTextInfo(textInfos.POSITION_CARET) + return (True, newInfo) # If the focus changes after this point, fetching the caret may fail, # but we still want to stay in this loop. try: diff --git a/source/textInfos/__init__.py b/source/textInfos/__init__.py index 4497e183436..16d3946fdd5 100755 --- a/source/textInfos/__init__.py +++ b/source/textInfos/__init__.py @@ -216,8 +216,6 @@ def __eq__(self,other): if isinstance(other,Bookmark) and self.infoClass==other.infoClass and self.data==other.data: return True - def __ne__(self,other): - return not self==other #Unit constants UNIT_CHARACTER="character"