1212
1313from comtypes import COMError
1414from UIAUtils import isTextRangeOffscreen
15+ from winVersion import isWin10
1516from . import UIATextInfo
1617from ..behaviors import KeyboardHandlerBasedTypedCharSupport
1718from ..window import Window
1819
1920
2021class 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
275287class 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
0 commit comments