Skip to content

Commit 1faa8e9

Browse files
authored
Merge cea6aca into b21a299
2 parents b21a299 + cea6aca commit 1faa8e9

File tree

7 files changed

+232
-153
lines changed

7 files changed

+232
-153
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: 122 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# A part of NonVisual Desktop Access (NVDA)
33
# This file is covered by the GNU General Public License.
44
# See the file COPYING for more details.
5-
# Copyright (C) 2019 Bill Dengler
5+
# Copyright (C) 2019-2020 Bill Dengler
66

77
import ctypes
88
import NVDAHelper
@@ -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,62 @@ 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+
def compareEndPoints(self, other, which):
116+
"""Works around a UIA bug on Windows 10 1803 to 2004."""
117+
# Even when a console textRange's start and end have been moved to the
118+
# same position, the console incorrectly reports the end as being
119+
# past the start.
120+
# Compare to the start (not the end) when collapsed.
121+
selfEndPoint, otherEndPoint = which.split("To")
122+
if selfEndPoint == "end" and self._isCollapsed():
123+
selfEndPoint = "start"
124+
if otherEndPoint == "End" and other._isCollapsed():
125+
otherEndPoint = "Start"
126+
which = f"{selfEndPoint}To{otherEndPoint}"
127+
return super().compareEndPoints(other, which=which)
128+
129+
def setEndPoint(self, other, which):
130+
"""Override of L{textInfos.TextInfo.setEndPoint}.
131+
Works around a UIA bug on Windows 10 1803 to 2004 that means we can
132+
not trust the "end" endpoint of a collapsed (empty) text range
133+
for comparisons.
134+
"""
135+
selfEndPoint, otherEndPoint = which.split("To")
136+
# In this case, there is no need to check if self is collapsed
137+
# since the point of this method is to change its text range, modifying the "end" endpoint of a collapsed
138+
# text range is fine.
139+
if otherEndPoint == "End" and other._isCollapsed():
140+
otherEndPoint = "Start"
141+
which = f"{selfEndPoint}To{otherEndPoint}"
142+
return super().setEndPoint(other, which=which)
143+
144+
def expand(self, unit):
145+
if unit == textInfos.UNIT_WORD:
146+
# UIA doesn't implement word movement, so we need to do it manually.
147+
lineInfo = self.copy()
148+
lineInfo.expand(textInfos.UNIT_LINE)
149+
offset = self._getCurrentOffsetInThisLine(lineInfo)
150+
start, end = self._getWordOffsetsInThisLine(offset, lineInfo)
151+
wordEndPoints = (
152+
(offset - start) * -1,
153+
end - offset - 1
154+
)
155+
if wordEndPoints[0]:
156+
self._rangeObj.MoveEndpointByUnit(
157+
UIAHandler.TextPatternRangeEndpoint_Start,
158+
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
159+
wordEndPoints[0]
160+
)
161+
if wordEndPoints[1]:
162+
self._rangeObj.MoveEndpointByUnit(
163+
UIAHandler.TextPatternRangeEndpoint_End,
164+
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
165+
wordEndPoints[1]
166+
)
167+
else:
168+
return super(consoleUIATextInfo, self).expand(unit)
169+
170+
def _move(self, unit, direction, endPoint=None):
72171
if unit == textInfos.UNIT_WORD and direction != 0:
73172
# UIA doesn't implement word movement, so we need to do it manually.
74173
# Relative to the current line, calculate our offset
@@ -128,98 +227,8 @@ def move(self, unit, direction, endPoint=None):
128227
# after moving.
129228
# Therefore manually collapse.
130229
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
152230
return res
153231

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-
223232
def _getCurrentOffsetInThisLine(self, lineInfo):
224233
"""
225234
Given a caret textInfo expanded to line, returns the index into the
@@ -258,18 +267,19 @@ def _getWordOffsetsInThisLine(self, offset, lineInfo):
258267
min(end.value, max(1, lineTextLen - 2))
259268
)
260269

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

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
279+
def _get_isCollapsed(self):
280+
# To decide if the textRange is collapsed,
281+
# Check if it has no text.
282+
return self._isCollapsed()
273283

274284

275285
class consoleUIAWindow(Window):
@@ -302,7 +312,7 @@ def _get_TextInfo(self):
302312
on NVDAObjects.UIA.UIA
303313
consoleUIATextInfo fixes expand/collapse, implements word movement, and
304314
bounds review to the visible text."""
305-
return consoleUIATextInfo
315+
return consoleUIATextInfo if isWin10(2004) else consoleUIATextInfoPre2004
306316

307317
def _getTextLines(self):
308318
# 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-2020 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)