Skip to content

Commit ce602b7

Browse files
authored
Merge d4b37ed into 54a4ec5
2 parents 54a4ec5 + d4b37ed commit ce602b7

16 files changed

Lines changed: 528 additions & 159 deletions

source/NVDAObjects/__init__.py

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -202,13 +202,13 @@ class NVDAObject(documentBase.TextContainerObject, baseObject.ScriptableObject,
202202
Events for the widget are handled by special event methods on the object.
203203
Commands triggered by input from the user can also be handled by special methods called scripts.
204204
See L{ScriptableObject} for more details.
205-
205+
206206
The only attribute that absolutely must be provided is L{processID}.
207207
However, subclasses should provide at least the L{name} and L{role} attributes in order for the object to be meaningful to the user.
208208
Attributes such as L{parent}, L{firstChild}, L{next} and L{previous} link an instance to other NVDAObjects in the hierarchy.
209209
In order to facilitate access to text exposed by a widget which supports text content (e.g. an editable text control),
210210
a L{textInfos.TextInfo} should be implemented and the L{TextInfo} attribute should specify this class.
211-
211+
212212
There are two main types of NVDAObject classes:
213213
* API classes, which provide the core functionality to work with objects exposed using a particular API (e.g. MSAA/IAccessible).
214214
* Overlay classes, which supplement the core functionality provided by an API class to handle a specific widget or type of widget.
@@ -247,7 +247,7 @@ def findBestAPIClass(cls,kwargs,relation=None):
247247
newAPIClass=cls
248248
if 'getPossibleAPIClasses' in newAPIClass.__dict__:
249249
for possibleAPIClass in newAPIClass.getPossibleAPIClasses(kwargs,relation=relation):
250-
if 'kwargsFromSuper' not in possibleAPIClass.__dict__:
250+
if 'kwargsFromSuper' not in possibleAPIClass.__dict__:
251251
log.error("possible API class %s does not implement kwargsFromSuper"%possibleAPIClass)
252252
continue
253253
if possibleAPIClass.kwargsFromSuper(kwargs,relation=relation):
@@ -364,7 +364,7 @@ def _isEqual(self,other):
364364
@rtype: boolean
365365
"""
366366
return True
367-
367+
368368
def __eq__(self,other):
369369
"""Compaires the objects' memory addresses, their type, and uses L{NVDAObject._isEqual} to see if they are equal.
370370
"""
@@ -373,7 +373,7 @@ def __eq__(self,other):
373373
if type(self) is not type(other):
374374
return False
375375
return self._isEqual(other)
376-
376+
377377
# As __eq__ was defined on this class, we must provide __hash__ to remain hashable.
378378
# The default hash implementation is fine for our purposes.
379379
def __hash__(self):
@@ -415,7 +415,7 @@ def _get_treeInterceptor(self) -> typing.Optional[TreeInterceptor]:
415415
If a treeInterceptor has not been specifically set,
416416
the L{treeInterceptorHandler} is asked if it can find a treeInterceptor containing this object.
417417
@return: the treeInterceptor
418-
"""
418+
"""
419419
if hasattr(self,'_treeInterceptor'):
420420
ti=self._treeInterceptor
421421
if isinstance(ti,weakref.ref):
@@ -468,7 +468,7 @@ def _get_name(self) -> str:
468468

469469
def _get_role(self) -> controlTypes.Role:
470470
"""The role or type of control this object represents (example: button, list, dialog).
471-
"""
471+
"""
472472
return controlTypes.Role.UNKNOWN
473473

474474
#: Type definition for auto prop '_get_roleText'
@@ -500,7 +500,7 @@ def _get_roleTextBraille(self):
500500
def _get_value(self) -> str:
501501
"""The value of this object
502502
(example: the current percentage of a scrollbar, the selected option in a combo box).
503-
"""
503+
"""
504504
return ""
505505

506506
#: Typing information for auto property _get_description
@@ -583,7 +583,7 @@ def getActionName(self,index=None):
583583
@rtype: str
584584
"""
585585
raise NotImplementedError
586-
586+
587587
def doAction(self,index=None):
588588
"""Performs an action supported by this object.
589589
If index is not given then the default action will be used if it exists.
@@ -729,15 +729,15 @@ def _get_rowNumber(self):
729729

730730
def _get_presentationalRowNumber(self):
731731
"""
732-
An optional version of the rowNumber property
732+
An optional version of the rowNumber property
733733
used purely for speech and braille presentation if implemented.
734734
This is never used for navigational logic.
735735
This property should be implemented if the table has virtual content which may not all be loaded at one time.
736-
For example, a table with 1000 rows and 1000 columns,
736+
For example, a table with 1000 rows and 1000 columns,
737737
yet the table only shows perhaps 10 rows by 10 columns at a time.
738-
Although the rowNumber might be row 2 of 10,
738+
Although the rowNumber might be row 2 of 10,
739739
the user needs to be told it is perhaps row 500 (taking all virtual rows into account).
740-
If the underlying APIs do not distinguish between virtual and physical cell coordinates,
740+
If the underlying APIs do not distinguish between virtual and physical cell coordinates,
741741
then this property should not be implemented.
742742
@rtype: int
743743
"""
@@ -751,15 +751,15 @@ def _get_columnNumber(self):
751751

752752
def _get_presentationalColumnNumber(self):
753753
"""
754-
An optional version of the columnNumber property
754+
An optional version of the columnNumber property
755755
used purely for speech and braille presentation if implemented.
756756
This is never used for navigational logic.
757757
This property should be implemented if the table has virtual content which may not all be loaded at one time.
758-
For example, a table with 1000 rows and 1000 columns,
758+
For example, a table with 1000 rows and 1000 columns,
759759
yet the table only shows perhaps 10 rows by 10 columns at a time.
760-
Although the columnNumber might be column 2 of 10,
760+
Although the columnNumber might be column 2 of 10,
761761
the user needs to be told it is perhaps column 500 (taking all virtual columns into account).
762-
If the underlying APIs do not distinguish between virtual and physical cell coordinates,
762+
If the underlying APIs do not distinguish between virtual and physical cell coordinates,
763763
then this property should not be implemented.
764764
@rtype: int
765765
"""
@@ -783,15 +783,15 @@ def _get_rowCount(self):
783783

784784
def _get_presentationalRowCount(self):
785785
"""
786-
An optional version of the rowCount property
786+
An optional version of the rowCount property
787787
used purely for speech and braille presentation if implemented.
788788
This is never used for navigational logic.
789789
This property should be implemented if the table has virtual content which may not all be loaded at one time.
790-
For example, a table with 1000 rows and 1000 columns,
790+
For example, a table with 1000 rows and 1000 columns,
791791
yet the table only shows perhaps 10 rows by 10 columns at a time.
792-
Although the rowCount might be 10,
793-
the user needs to be told the table really has 1000 rows.
794-
If the underlying APIs do not distinguish between virtual and physical cell coordinates,
792+
Although the rowCount might be 10,
793+
the user needs to be told the table really has 1000 rows.
794+
If the underlying APIs do not distinguish between virtual and physical cell coordinates,
795795
then this property should not be implemented.
796796
@rtype: int
797797
"""
@@ -805,15 +805,15 @@ def _get_columnCount(self):
805805

806806
def _get_presentationalColumnCount(self):
807807
"""
808-
An optional version of the columnCount property
808+
An optional version of the columnCount property
809809
used purely for speech and braille presentation if implemented.
810810
This is never used for navigational logic.
811811
This property should be implemented if the table has virtual content which may not all be loaded at one time.
812-
For example, a table with 1000 rows and 1000 columns,
812+
For example, a table with 1000 rows and 1000 columns,
813813
yet the table only shows perhaps 10 rows by 10 columns at a time.
814-
Although the columnCount might be 10,
815-
the user needs to be told the table really has 1000 columns.
816-
If the underlying APIs do not distinguish between virtual and physical cell coordinates,
814+
Although the columnCount might be 10,
815+
the user needs to be told the table really has 1000 columns.
816+
If the underlying APIs do not distinguish between virtual and physical cell coordinates,
817817
then this property should not be implemented.
818818
@rtype: int
819819
"""
@@ -857,7 +857,7 @@ def _get_tableID(self):
857857
even if the user moves to a cell in the same row/column.
858858
"""
859859
raise NotImplementedError
860-
860+
861861
def _get_recursiveDescendants(self):
862862
"""Recursively traverse and return the descendants of this object.
863863
This is a depth-first forward traversal.
@@ -1008,12 +1008,18 @@ def _get_activeChild(self):
10081008
"""
10091009
return None
10101010

1011+
#: Type definition for auto prop '_get_isFocusable'
1012+
isFocusable: bool
1013+
10111014
def _get_isFocusable(self):
10121015
"""Whether this object is focusable.
10131016
@rtype: bool
10141017
"""
10151018
return controlTypes.State.FOCUSABLE in self.states
10161019

1020+
#: Type definition for auto prop '_get_hasFocus'
1021+
hasFocus: bool
1022+
10171023
def _get_hasFocus(self):
10181024
"""Whether this object has focus.
10191025
@rtype: bool
@@ -1034,7 +1040,7 @@ def scrollIntoView(self):
10341040
def _get_labeledBy(self):
10351041
"""Retrieves the object that this object is labeled by (example: the static text label beside an edit field).
10361042
@return: the label object if it has one else None.
1037-
@rtype: L{NVDAObject} or None
1043+
@rtype: L{NVDAObject} or None
10381044
"""
10391045
return None
10401046

@@ -1062,7 +1068,8 @@ def _get_isProtected(self):
10621068
isProtected=(controlTypes.State.PROTECTED in self.states or self.role==controlTypes.Role.PASSWORDEDIT)
10631069
# #7908: If this object is currently protected, keep it protected for the rest of its lifetime.
10641070
# The most likely reason it would lose its protected state is because the object is dying.
1065-
# In this case it is much more secure to assume it is still protected, thus the end of PIN codes will not be accidentally reported.
1071+
# In this case it is much more secure to assume it is still protected, thus the end of PIN codes
1072+
# will not be accidentally reported.
10661073
if isProtected:
10671074
self.isProtected=isProtected
10681075
return isProtected
@@ -1114,7 +1121,7 @@ def _get_statusBar(self) -> Optional["NVDAObject"]:
11141121
isCurrent: controlTypes.IsCurrent #: type info for auto property _get_isCurrent
11151122

11161123
def _get_isCurrent(self) -> controlTypes.IsCurrent:
1117-
"""Gets the value that indicates whether this object is the current element in a set of related
1124+
"""Gets the value that indicates whether this object is the current element in a set of related
11181125
elements. This maps to aria-current.
11191126
"""
11201127
return controlTypes.IsCurrent.NO
@@ -1284,7 +1291,7 @@ def event_gainFocus(self):
12841291
vision.handler.handleGainFocus(self)
12851292

12861293
def event_loseFocus(self):
1287-
# Forget the word currently being typed as focus is moving to a new control.
1294+
# Forget the word currently being typed as focus is moving to a new control.
12881295
speech.clearTypedWordBuffer()
12891296

12901297
def event_focusExited(self):

source/braille.py

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
TetherTo,
4343
ReportTableHeaders,
4444
)
45+
from config.featureFlagEnums import ReviewRoutingMovesSystemCaretFlag
4546
from logHandler import log
4647
import controlTypes
4748
import api
@@ -755,6 +756,19 @@ def routeTo(self, braillePos):
755756
pass
756757

757758

759+
class ReviewNVDAObjectRegion(NVDAObjectRegion):
760+
"""A region to provide a braille representation of an NVDAObject when braille is tethered to review.
761+
This region behaves very similar to its base class.
762+
However, when the move system caret when Routing review cursor braille setting is active,
763+
pressing a routing key will first focus the object before executing the default action.
764+
"""
765+
766+
def routeTo(self, braillePos):
767+
if _routingShouldMoveSystemCaret() and self.obj.isFocusable and not self.obj.hasFocus:
768+
self.obj.setFocus()
769+
super().routeTo(braillePos)
770+
771+
758772
def getControlFieldBraille(
759773
info: textInfos.TextInfo,
760774
field: textInfos.Field,
@@ -1068,10 +1082,9 @@ def _getSelection(self):
10681082
except:
10691083
return self.obj.makeTextInfo(textInfos.POSITION_FIRST)
10701084

1071-
def _setCursor(self, info):
1085+
def _setCursor(self, info: textInfos.TextInfo):
10721086
"""Set the cursor.
10731087
@param info: The range to which the cursor should be moved.
1074-
@type info: L{textInfos.TextInfo}
10751088
"""
10761089
try:
10771090
info.updateCaret()
@@ -1332,7 +1345,7 @@ def getTextInfoForBraillePos(self, braillePos):
13321345
dest.move(textInfos.UNIT_CHARACTER, pos)
13331346
return dest
13341347

1335-
def routeTo(self, braillePos):
1348+
def routeTo(self, braillePos: int):
13361349
if self._brailleInputIndStart is not None and self._brailleInputIndStart <= braillePos < self._brailleInputIndEnd:
13371350
# The user is moving within untranslated braille input.
13381351
if braillePos < self._brailleInputStart:
@@ -1349,21 +1362,24 @@ def routeTo(self, braillePos):
13491362
return
13501363

13511364
dest = self.getTextInfoForBraillePos(braillePos)
1365+
self._routeToTextInfo(dest)
1366+
1367+
def _routeToTextInfo(self, info: textInfos.TextInfo):
13521368
# When there is a selection, brailleCursorPos will be None
13531369
# Don't activate, but move the cursor to the new cell (dropping the
13541370
# selection). An alternative behavior may be to activate on the selection.
13551371
# Moving the cursor was considered more intuitive.
13561372
if self.brailleCursorPos is not None:
13571373
cursor = self.getTextInfoForBraillePos(self.brailleCursorPos)
1358-
if dest.compareEndPoints(cursor, "startToStart") == 0:
1374+
if info.compareEndPoints(cursor, "startToStart") == 0:
13591375
# The cursor is already at this position,
13601376
# so activate the position.
13611377
try:
13621378
self._getSelection().activate()
13631379
except NotImplementedError:
13641380
pass
13651381
return
1366-
self._setCursor(dest)
1382+
self._setCursor(info)
13671383

13681384
def nextLine(self):
13691385
dest = self._readingInfo.copy()
@@ -1404,6 +1420,7 @@ def previousLine(self, start=False):
14041420
dest.collapse()
14051421
self._setCursor(dest)
14061422

1423+
14071424
class CursorManagerRegion(TextInfoRegion):
14081425

14091426
def _isMultiline(self):
@@ -1412,19 +1429,65 @@ def _isMultiline(self):
14121429
def _getSelection(self):
14131430
return self.obj.selection
14141431

1415-
def _setCursor(self, info):
1432+
def _setCursor(self, info: textInfos.TextInfo):
14161433
self.obj.selection = info
14171434

1435+
14181436
class ReviewTextInfoRegion(TextInfoRegion):
14191437

14201438
allowPageTurns=False
14211439

14221440
def _getSelection(self):
14231441
return api.getReviewPosition().copy()
14241442

1425-
def _setCursor(self, info):
1443+
def _routeToTextInfo(self, info: textInfos.TextInfo):
1444+
super()._routeToTextInfo(info)
1445+
if not _routingShouldMoveSystemCaret():
1446+
return
1447+
from displayModel import DisplayModelTextInfo, EditableTextDisplayModelTextInfo
1448+
if (
1449+
isinstance(info, DisplayModelTextInfo)
1450+
and not isinstance(info, EditableTextDisplayModelTextInfo)
1451+
):
1452+
# This region either reviews the screen or an object that has
1453+
# DisplayModelTextInfo without a caret, e.g. IAccessible.ContentGenericClient.
1454+
# In this case, we can at least emulate a kind of caret
1455+
# by trying to focus the object at start of the range.
1456+
obj = info.NVDAObjectAtStart
1457+
if (
1458+
not objectBelowLockScreenAndWindowsIsLocked(obj)
1459+
and obj.isFocusable
1460+
and not obj.hasFocus
1461+
):
1462+
obj.setFocus()
1463+
else:
1464+
# Update the physical caret using the super class.
1465+
super()._setCursor(info)
1466+
1467+
def _setCursor(self, info: textInfos.TextInfo):
14261468
api.setReviewPosition(info)
14271469

1470+
1471+
class ReviewCursorManagerRegion(ReviewTextInfoRegion, CursorManagerRegion):
1472+
...
1473+
1474+
1475+
def _routingShouldMoveSystemCaret() -> bool:
1476+
"""Returns whether pressing a braille routing key should move the system caret.
1477+
"""
1478+
reviewRoutingMovesSystemCaret = config.conf["braille"]["reviewRoutingMovesSystemCaret"].calculated()
1479+
configuredTether = config.conf["braille"]["tetherTo"]
1480+
shouldMoveCaretTetheredReview = (
1481+
configuredTether == TetherTo.REVIEW.value
1482+
and reviewRoutingMovesSystemCaret == ReviewRoutingMovesSystemCaretFlag.ALWAYS
1483+
)
1484+
shouldMoveCaretTetheredAuto = (
1485+
configuredTether == TetherTo.AUTO.value
1486+
and reviewRoutingMovesSystemCaret != ReviewRoutingMovesSystemCaretFlag.NEVER
1487+
)
1488+
return shouldMoveCaretTetheredAuto or shouldMoveCaretTetheredReview
1489+
1490+
14281491
def rindex(seq, item, start, end):
14291492
for index in range(end - 1, start - 1, -1):
14301493
if seq[index] == item:
@@ -1845,7 +1908,7 @@ def getFocusRegions(
18451908
from cursorManager import CursorManager
18461909
from NVDAObjects import NVDAObject
18471910
if isinstance(obj, CursorManager):
1848-
region2 = (ReviewTextInfoRegion if review else CursorManagerRegion)(obj)
1911+
region2 = (ReviewCursorManagerRegion if review else CursorManagerRegion)(obj)
18491912
elif (
18501913
isinstance(obj, DocumentTreeInterceptor)
18511914
or (
@@ -1858,7 +1921,10 @@ def getFocusRegions(
18581921
region2 = None
18591922
if isinstance(obj, TreeInterceptor):
18601923
obj = obj.rootNVDAObject
1861-
region = NVDAObjectRegion(obj, appendText=TEXT_SEPARATOR if region2 else "")
1924+
region = (ReviewNVDAObjectRegion if review else NVDAObjectRegion)(
1925+
obj,
1926+
appendText=TEXT_SEPARATOR if region2 else ""
1927+
)
18621928
region.update()
18631929
yield region
18641930
if region2:

0 commit comments

Comments
 (0)