Skip to content

Commit 828d40f

Browse files
authored
Merge 8d4a2b3 into b21a299
2 parents b21a299 + 8d4a2b3 commit 828d40f

File tree

7 files changed

+241
-163
lines changed

7 files changed

+241
-163
lines changed

source/NVDAObjects/UIA/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import textInfos
2727
from logHandler import log
2828
from UIAUtils import *
29+
from UIAUtils import shouldUseUIAConsole
2930
from NVDAObjects.window import Window
3031
from NVDAObjects import NVDAObjectTextInfo, InvalidNVDAObject
3132
from NVDAObjects.behaviors import (
@@ -929,7 +930,7 @@ def findOverlayClasses(self,clsList):
929930
# Support Windows Console's UIA interface
930931
if (
931932
self.windowClassName == "ConsoleWindowClass"
932-
and config.conf['UIA']['winConsoleImplementation'] == "UIA"
933+
and shouldUseUIAConsole()
933934
):
934935
from . import winConsoleUIA
935936
winConsoleUIA.findExtraOverlayClasses(self, clsList)

source/NVDAObjects/UIA/winConsoleUIA.py

Lines changed: 123 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212

1313
from comtypes import COMError
1414
from UIAUtils import isTextRangeOffscreen
15+
from winVersion import isWin10
1516
from . import UIATextInfo
1617
from ..behaviors import KeyboardHandlerBasedTypedCharSupport
1718
from ..window import Window
1819

1920

2021
class consoleUIATextInfo(UIATextInfo):
21-
2222
def __init__(self, obj, position, _rangeObj=None):
2323
# We want to limit textInfos to just the visible part of the console.
2424
# Therefore we specifically handle POSITION_FIRST, POSITION_LAST and POSITION_ALL.
@@ -48,8 +48,58 @@ def __init__(self, obj, position, _rangeObj=None):
4848
_rangeObj = first._rangeObj
4949
super(consoleUIATextInfo, self).__init__(obj, position, _rangeObj)
5050

51+
def move(self, unit, direction, endPoint=None):
52+
oldInfo = None
53+
if self.basePosition != textInfos.POSITION_CARET:
54+
# Insure we haven't gone beyond the visible text.
55+
# UIA adds thousands of blank lines to the end of the console.
56+
boundingInfo = self.obj.makeTextInfo(textInfos.POSITION_ALL)
57+
oldInfo = self.copy()
58+
res = self._move(unit, direction, endPoint)
59+
# Console textRanges have access to the entire console buffer.
60+
# However, we want to limit ourselves to onscreen text.
61+
# Therefore, if the textInfo was originally visible,
62+
# but we are now above or below the visible range,
63+
# Restore the original textRange and pretend the move didn't work.
64+
if oldInfo:
65+
try:
66+
if (
67+
(
68+
self.compareEndPoints(boundingInfo, "startToStart") < 0
69+
or self.compareEndPoints(boundingInfo, "startToEnd") >= 0
70+
)
71+
and not (
72+
oldInfo.compareEndPoints(boundingInfo, "startToStart") < 0
73+
or oldInfo.compareEndPoints(boundingInfo, "startToEnd") >= 0
74+
)
75+
):
76+
self._rangeObj = oldInfo._rangeObj
77+
return 0
78+
except (COMError, RuntimeError):
79+
pass
80+
return res
81+
82+
def _move(self, unit, direction, endPoint=None):
83+
"Perform a move without respect to bounding."
84+
return super(consoleUIATextInfo, self).move(unit, direction, endPoint)
85+
86+
def __ne__(self, other):
87+
"""Support more accurate caret move detection."""
88+
return not self == other
89+
90+
def _get_text(self):
91+
# #10036: return a space if the text range is empty.
92+
# Consoles don't actually store spaces, the character is merely left blank.
93+
res = super(consoleUIATextInfo, self)._get_text()
94+
if not res:
95+
return ' '
96+
else:
97+
return res
98+
99+
100+
class consoleUIATextInfoPre2004(consoleUIATextInfo):
51101
def collapse(self, end=False):
52-
"""Works around a UIA bug on Windows 10 1803 and later."""
102+
"""Works around a UIA bug on Windows 10 1803 to 2004."""
53103
# When collapsing, consoles seem to incorrectly push the start of the
54104
# textRange back one character.
55105
# Correct this by bringing the start back up to where the end is.
@@ -62,13 +112,63 @@ def collapse(self, end=False):
62112
UIAHandler.TextPatternRangeEndpoint_Start
63113
)
64114

65-
def move(self, unit, direction, endPoint=None):
66-
oldInfo = None
67-
if self.basePosition != textInfos.POSITION_CARET:
68-
# Insure we haven't gone beyond the visible text.
69-
# UIA adds thousands of blank lines to the end of the console.
70-
boundingInfo = self.obj.makeTextInfo(textInfos.POSITION_ALL)
71-
oldInfo = self.copy()
115+
116+
def compareEndPoints(self, other, which):
117+
"""Works around a UIA bug on Windows 10 1803 to 2004."""
118+
# Even when a console textRange's start and end have been moved to the
119+
# same position, the console incorrectly reports the end as being
120+
# past the start.
121+
# Compare to the start (not the end) when collapsed.
122+
selfEndPoint, otherEndPoint = which.split("To")
123+
if selfEndPoint == "end" and self._isCollapsed():
124+
selfEndPoint = "start"
125+
if otherEndPoint == "End" and other._isCollapsed():
126+
otherEndPoint = "Start"
127+
which = f"{selfEndPoint}To{otherEndPoint}"
128+
return super().compareEndPoints(other, which=which)
129+
130+
def setEndPoint(self, other, which):
131+
"""Override of L{textInfos.TextInfo.setEndPoint}.
132+
Works around a UIA bug on Windows 10 1803 to 2004 that means we can
133+
not trust the "end" endpoint of a collapsed (empty) text range
134+
for comparisons.
135+
"""
136+
selfEndPoint, otherEndPoint = which.split("To")
137+
# In this case, there is no need to check if self is collapsed
138+
# since the point of this method is to change its text range, modifying the "end" endpoint of a collapsed
139+
# text range is fine.
140+
if otherEndPoint == "End" and other._isCollapsed():
141+
otherEndPoint = "Start"
142+
which = f"{selfEndPoint}To{otherEndPoint}"
143+
return super().setEndPoint(other, which=which)
144+
145+
def expand(self, unit):
146+
if unit == textInfos.UNIT_WORD:
147+
# UIA doesn't implement word movement, so we need to do it manually.
148+
lineInfo = self.copy()
149+
lineInfo.expand(textInfos.UNIT_LINE)
150+
offset = self._getCurrentOffsetInThisLine(lineInfo)
151+
start, end = self._getWordOffsetsInThisLine(offset, lineInfo)
152+
wordEndPoints = (
153+
(offset - start) * -1,
154+
end - offset - 1
155+
)
156+
if wordEndPoints[0]:
157+
self._rangeObj.MoveEndpointByUnit(
158+
UIAHandler.TextPatternRangeEndpoint_Start,
159+
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
160+
wordEndPoints[0]
161+
)
162+
if wordEndPoints[1]:
163+
self._rangeObj.MoveEndpointByUnit(
164+
UIAHandler.TextPatternRangeEndpoint_End,
165+
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
166+
wordEndPoints[1]
167+
)
168+
else:
169+
return super(consoleUIATextInfo, self).expand(unit)
170+
171+
def _move(self, unit, direction, endPoint=None):
72172
if unit == textInfos.UNIT_WORD and direction != 0:
73173
# UIA doesn't implement word movement, so we need to do it manually.
74174
# Relative to the current line, calculate our offset
@@ -128,98 +228,8 @@ def move(self, unit, direction, endPoint=None):
128228
# after moving.
129229
# Therefore manually collapse.
130230
self.collapse()
131-
# Console textRanges have access to the entire console buffer.
132-
# However, we want to limit ourselves to onscreen text.
133-
# Therefore, if the textInfo was originally visible,
134-
# but we are now above or below the visible range,
135-
# Restore the original textRange and pretend the move didn't work.
136-
if oldInfo:
137-
try:
138-
if (
139-
(
140-
self.compareEndPoints(boundingInfo, "startToStart") < 0
141-
or self.compareEndPoints(boundingInfo, "startToEnd") >= 0
142-
)
143-
and not (
144-
oldInfo.compareEndPoints(boundingInfo, "startToStart") < 0
145-
or oldInfo.compareEndPoints(boundingInfo, "startToEnd") >= 0
146-
)
147-
):
148-
self._rangeObj = oldInfo._rangeObj
149-
return 0
150-
except (COMError, RuntimeError):
151-
pass
152231
return res
153232

154-
def expand(self, unit):
155-
if unit == textInfos.UNIT_WORD:
156-
# UIA doesn't implement word movement, so we need to do it manually.
157-
lineInfo = self.copy()
158-
lineInfo.expand(textInfos.UNIT_LINE)
159-
offset = self._getCurrentOffsetInThisLine(lineInfo)
160-
start, end = self._getWordOffsetsInThisLine(offset, lineInfo)
161-
wordEndPoints = (
162-
(offset - start) * -1,
163-
end - offset - 1
164-
)
165-
if wordEndPoints[0]:
166-
self._rangeObj.MoveEndpointByUnit(
167-
UIAHandler.TextPatternRangeEndpoint_Start,
168-
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
169-
wordEndPoints[0]
170-
)
171-
if wordEndPoints[1]:
172-
self._rangeObj.MoveEndpointByUnit(
173-
UIAHandler.TextPatternRangeEndpoint_End,
174-
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
175-
wordEndPoints[1]
176-
)
177-
else:
178-
return super(consoleUIATextInfo, self).expand(unit)
179-
180-
def compareEndPoints(self, other, which):
181-
"""Works around a UIA bug on Windows 10 1803 and later."""
182-
# Even when a console textRange's start and end have been moved to the
183-
# same position, the console incorrectly reports the end as being
184-
# past the start.
185-
# Compare to the start (not the end) when collapsed.
186-
selfEndPoint, otherEndPoint = which.split("To")
187-
if selfEndPoint == "end" and self._isCollapsed():
188-
selfEndPoint = "start"
189-
if otherEndPoint == "End" and other._isCollapsed():
190-
otherEndPoint = "Start"
191-
which = f"{selfEndPoint}To{otherEndPoint}"
192-
return super().compareEndPoints(other, which=which)
193-
194-
def setEndPoint(self, other, which):
195-
"""Override of L{textInfos.TextInfo.setEndPoint}.
196-
Works around a UIA bug on Windows 10 1803 and later that means we can
197-
not trust the "end" endpoint of a collapsed (empty) text range
198-
for comparisons.
199-
"""
200-
selfEndPoint, otherEndPoint = which.split("To")
201-
# In this case, there is no need to check if self is collapsed
202-
# since the point of this method is to change its text range, modifying the "end" endpoint of a collapsed
203-
# text range is fine.
204-
if otherEndPoint == "End" and other._isCollapsed():
205-
otherEndPoint = "Start"
206-
which = f"{selfEndPoint}To{otherEndPoint}"
207-
return super().setEndPoint(other, which=which)
208-
209-
def _isCollapsed(self):
210-
"""Works around a UIA bug on Windows 10 1803 and later that means we
211-
cannot trust the "end" endpoint of a collapsed (empty) text range
212-
for comparisons.
213-
Instead we check to see if we can get the first character from the
214-
text range. A collapsed range will not have any characters
215-
and will return an empty string."""
216-
return not bool(self._rangeObj.getText(1))
217-
218-
def _get_isCollapsed(self):
219-
# To decide if the textRange is collapsed,
220-
# Check if it has no text.
221-
return self._isCollapsed()
222-
223233
def _getCurrentOffsetInThisLine(self, lineInfo):
224234
"""
225235
Given a caret textInfo expanded to line, returns the index into the
@@ -258,18 +268,20 @@ def _getWordOffsetsInThisLine(self, offset, lineInfo):
258268
min(end.value, max(1, lineTextLen - 2))
259269
)
260270

261-
def __ne__(self, other):
262-
"""Support more accurate caret move detection."""
263-
return not self == other
271+
def _isCollapsed(self):
272+
"""Works around a UIA bug on Windows 10 1803 to 2004 that means we
273+
cannot trust the "end" endpoint of a collapsed (empty) text range
274+
for comparisons.
275+
Instead we check to see if we can get the first character from the
276+
text range. A collapsed range will not have any characters
277+
and will return an empty string."""
278+
return not bool(self._rangeObj.getText(1))
264279

265-
def _get_text(self):
266-
# #10036: return a space if the text range is empty.
267-
# Consoles don't actually store spaces, the character is merely left blank.
268-
res = super(consoleUIATextInfo, self)._get_text()
269-
if not res:
270-
return ' '
271-
else:
272-
return res
280+
281+
def _get_isCollapsed(self):
282+
# To decide if the textRange is collapsed,
283+
# Check if it has no text.
284+
return self._isCollapsed()
273285

274286

275287
class consoleUIAWindow(Window):
@@ -302,7 +314,7 @@ def _get_TextInfo(self):
302314
on NVDAObjects.UIA.UIA
303315
consoleUIATextInfo fixes expand/collapse, implements word movement, and
304316
bounds review to the visible text."""
305-
return consoleUIATextInfo
317+
return consoleUIATextInfo if isWin10(2004) else consoleUIATextInfoPre2004
306318

307319
def _getTextLines(self):
308320
# This override of _getTextLines takes advantage of the fact that

source/UIAUtils.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2015-2016 NV Access Limited
2+
# Copyright (C) 2015-2019 NV Access Limited, Bill Dengler
33
# This file is covered by the GNU General Public License.
44
# See the file COPYING for more details.
55

66
import operator
77
from comtypes import COMError
8+
import config
89
import ctypes
910
import UIAHandler
11+
from winVersion import isWin10
1012

1113
def createUIAMultiPropertyCondition(*dicts):
1214
"""
@@ -224,3 +226,22 @@ def getValue(self,ID,ignoreMixedValues=False):
224226
if not ignoreMixedValues and val==UIAHandler.handler.ReservedMixedAttributeValue:
225227
raise UIAMixedAttributeError
226228
return val
229+
230+
231+
def shouldUseUIAConsole(setting=None):
232+
"""Determines whether to use UIA in the Windows Console.
233+
@param setting: the config value to base this check on (if not provided,
234+
it is retrieved from config).
235+
"""
236+
if not setting:
237+
setting = config.conf['UIA']['winConsoleImplementation']
238+
if setting == "legacy":
239+
return False
240+
elif setting == "UIA":
241+
return True
242+
# #7497: Windows 10 Fall Creators Update has an incomplete UIA
243+
# implementation for console windows, therefore for now we should
244+
# ignore it.
245+
# It does not implement caret/selection, and probably has no
246+
# new text events.
247+
return isWin10(2004)

source/_UIAHandler.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -500,12 +500,10 @@ def IUIAutomationNotificationEventHandler_HandleNotificationEvent(
500500

501501
def _isBadUIAWindowClassName(self, windowClass):
502502
"Given a windowClassName, returns True if this is a known problematic UIA implementation."
503-
# #7497: Windows 10 Fall Creators Update has an incomplete UIA
504-
# implementation for console windows, therefore for now we should
505-
# ignore it.
506-
# It does not implement caret/selection, and probably has no new text
507-
# events.
508-
if windowClass == "ConsoleWindowClass" and config.conf['UIA']['winConsoleImplementation'] != "UIA":
503+
if (
504+
windowClass == "ConsoleWindowClass"
505+
and not UIAUtils.shouldUseUIAConsole()
506+
):
509507
return True
510508
return windowClass in badUIAWindowClassNames
511509

0 commit comments

Comments
 (0)