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
77import ctypes
88import NVDAHelper
1818
1919
2020class consoleUIATextInfo (UIATextInfo ):
21-
2221 def __init__ (self , obj , position , _rangeObj = None ):
2322 # We want to limit textInfos to just the visible part of the console.
2423 # Therefore we specifically handle POSITION_FIRST, POSITION_LAST and POSITION_ALL.
@@ -48,6 +47,56 @@ def __init__(self, obj, position, _rangeObj=None):
4847 _rangeObj = first ._rangeObj
4948 super (consoleUIATextInfo , self ).__init__ (obj , position , _rangeObj )
5049
50+ def move (self , unit , direction , endPoint = None ):
51+ oldInfo = None
52+ if self .basePosition != textInfos .POSITION_CARET :
53+ # Insure we haven't gone beyond the visible text.
54+ # UIA adds thousands of blank lines to the end of the console.
55+ boundingInfo = self .obj .makeTextInfo (textInfos .POSITION_ALL )
56+ oldInfo = self .copy ()
57+ res = self ._move (unit , direction , endPoint )
58+ # Console textRanges have access to the entire console buffer.
59+ # However, we want to limit ourselves to onscreen text.
60+ # Therefore, if the textInfo was originally visible,
61+ # but we are now above or below the visible range,
62+ # Restore the original textRange and pretend the move didn't work.
63+ if oldInfo :
64+ try :
65+ if (
66+ (
67+ self .compareEndPoints (boundingInfo , "startToStart" ) < 0
68+ or self .compareEndPoints (boundingInfo , "startToEnd" ) >= 0
69+ )
70+ and not (
71+ oldInfo .compareEndPoints (boundingInfo , "startToStart" ) < 0
72+ or oldInfo .compareEndPoints (boundingInfo , "startToEnd" ) >= 0
73+ )
74+ ):
75+ self ._rangeObj = oldInfo ._rangeObj
76+ return 0
77+ except (COMError , RuntimeError ):
78+ pass
79+ return res
80+
81+ def _move (self , unit , direction , endPoint = None ):
82+ "Perform a move without respect to bounding."
83+ return super (consoleUIATextInfo , self ).move (unit , direction , endPoint )
84+
85+ def __ne__ (self , other ):
86+ """Support more accurate caret move detection."""
87+ return not self == other
88+
89+ def _get_text (self ):
90+ # #10036: return a space if the text range is empty.
91+ # Consoles don't actually store spaces, the character is merely left blank.
92+ res = super (consoleUIATextInfo , self )._get_text ()
93+ if not res :
94+ return ' '
95+ else :
96+ return res
97+
98+
99+ class consoleUIATextInfoEndInclusive (consoleUIATextInfo ):
51100 def collapse (self , end = False ):
52101 """Works around a UIA bug on Windows 10 1803 and later."""
53102 # When collapsing, consoles seem to incorrectly push the start of the
@@ -62,13 +111,62 @@ def collapse(self, end=False):
62111 UIAHandler .TextPatternRangeEndpoint_Start
63112 )
64113
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 ()
114+ def compareEndPoints (self , other , which ):
115+ """Works around a UIA bug on Windows 10 1803 and later."""
116+ # Even when a console textRange's start and end have been moved to the
117+ # same position, the console incorrectly reports the end as being
118+ # past the start.
119+ # Compare to the start (not the end) when collapsed.
120+ selfEndPoint , otherEndPoint = which .split ("To" )
121+ if selfEndPoint == "end" and self ._isCollapsed ():
122+ selfEndPoint = "start"
123+ if otherEndPoint == "End" and other ._isCollapsed ():
124+ otherEndPoint = "Start"
125+ which = f"{ selfEndPoint } To{ otherEndPoint } "
126+ return super ().compareEndPoints (other , which = which )
127+
128+ def setEndPoint (self , other , which ):
129+ """Override of L{textInfos.TextInfo.setEndPoint}.
130+ Works around a UIA bug on Windows 10 1803 and later that means we can
131+ not trust the "end" endpoint of a collapsed (empty) text range
132+ for comparisons.
133+ """
134+ selfEndPoint , otherEndPoint = which .split ("To" )
135+ # In this case, there is no need to check if self is collapsed
136+ # since the point of this method is to change its text range, modifying the "end" endpoint of a collapsed
137+ # text range is fine.
138+ if otherEndPoint == "End" and other ._isCollapsed ():
139+ otherEndPoint = "Start"
140+ which = f"{ selfEndPoint } To{ otherEndPoint } "
141+ return super ().setEndPoint (other , which = which )
142+
143+ def expand (self , unit ):
144+ if unit == textInfos .UNIT_WORD :
145+ # UIA doesn't implement word movement, so we need to do it manually.
146+ lineInfo = self .copy ()
147+ lineInfo .expand (textInfos .UNIT_LINE )
148+ offset = self ._getCurrentOffsetInThisLine (lineInfo )
149+ start , end = self ._getWordOffsetsInThisLine (offset , lineInfo )
150+ wordEndPoints = (
151+ (offset - start ) * - 1 ,
152+ end - offset - 1
153+ )
154+ if wordEndPoints [0 ]:
155+ self ._rangeObj .MoveEndpointByUnit (
156+ UIAHandler .TextPatternRangeEndpoint_Start ,
157+ UIAHandler .NVDAUnitsToUIAUnits [textInfos .UNIT_CHARACTER ],
158+ wordEndPoints [0 ]
159+ )
160+ if wordEndPoints [1 ]:
161+ self ._rangeObj .MoveEndpointByUnit (
162+ UIAHandler .TextPatternRangeEndpoint_End ,
163+ UIAHandler .NVDAUnitsToUIAUnits [textInfos .UNIT_CHARACTER ],
164+ wordEndPoints [1 ]
165+ )
166+ else :
167+ return super (consoleUIATextInfo , self ).expand (unit )
168+
169+ def _move (self , unit , direction , endPoint = None ):
72170 if unit == textInfos .UNIT_WORD and direction != 0 :
73171 # UIA doesn't implement word movement, so we need to do it manually.
74172 # Relative to the current line, calculate our offset
@@ -128,98 +226,8 @@ def move(self, unit, direction, endPoint=None):
128226 # after moving.
129227 # Therefore manually collapse.
130228 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
152229 return res
153230
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-
223231 def _getCurrentOffsetInThisLine (self , lineInfo ):
224232 """
225233 Given a caret textInfo expanded to line, returns the index into the
@@ -258,18 +266,19 @@ def _getWordOffsetsInThisLine(self, offset, lineInfo):
258266 min (end .value , max (1 , lineTextLen - 2 ))
259267 )
260268
261- def __ne__ (self , other ):
262- """Support more accurate caret move detection."""
263- return not self == other
269+ def _isCollapsed (self ):
270+ """Works around a UIA bug on Windows 10 1803 and later that means we
271+ cannot trust the "end" endpoint of a collapsed (empty) text range
272+ for comparisons.
273+ Instead we check to see if we can get the first character from the
274+ text range. A collapsed range will not have any characters
275+ and will return an empty string."""
276+ return not bool (self ._rangeObj .getText (1 ))
264277
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
278+ def _get_isCollapsed (self ):
279+ # To decide if the textRange is collapsed,
280+ # Check if it has no text.
281+ return self ._isCollapsed ()
273282
274283
275284class consoleUIAWindow (Window ):
@@ -302,7 +311,7 @@ def _get_TextInfo(self):
302311 on NVDAObjects.UIA.UIA
303312 consoleUIATextInfo fixes expand/collapse, implements word movement, and
304313 bounds review to the visible text."""
305- return consoleUIATextInfo
314+ return consoleUIATextInfoEndInclusive
306315
307316 def _getTextLines (self ):
308317 # This override of _getTextLines takes advantage of the fact that
@@ -317,3 +326,8 @@ def findExtraOverlayClasses(obj, clsList):
317326 clsList .append (WinConsoleUIA )
318327 elif obj .UIAElement .cachedAutomationId == "Console Window" :
319328 clsList .append (consoleUIAWindow )
329+
330+
331+ class WinTerminalUIA (KeyboardHandlerBasedTypedCharSupport ):
332+ def _get_TextInfo (self ):
333+ return consoleUIATextInfo
0 commit comments