1919 Tuple ,
2020 Union ,
2121 Type ,
22+ Callable ,
2223)
2324from locale import strxfrm
2425
4243 TetherTo ,
4344 ReportTableHeaders ,
4445)
46+ from config .featureFlag import FeatureFlag
4547from config .featureFlagEnums import ReviewRoutingMovesSystemCaretFlag
4648from logHandler import log
4749import controlTypes
5355import re
5456import scriptHandler
5557import collections
58+ from collections import deque
5659import extensionPoints
5760import hwPortUtils
5861import bdDetect
@@ -1569,6 +1572,7 @@ def bufferPosToRegionPos(self, bufferPos):
15691572 raise LookupError ("No such position" )
15701573
15711574 def regionPosToBufferPos (self , region , pos , allowNearest = False ):
1575+ start : int = 0
15721576 for testRegion , start , end in self .regionsWithPositions :
15731577 if region == testRegion :
15741578 if pos < end - start :
@@ -2003,6 +2007,44 @@ def formatCellsForLog(cells: List[int]) -> str:
20032007"""
20042008
20052009
2010+ class UpdateTimer :
2011+ """Repeating timer for keeping display content always up to date."""
2012+
2013+ def __init__ (
2014+ self ,
2015+ interval : float ,
2016+ updateFunction : Callable [[], None ]
2017+ ):
2018+ """Constructor.
2019+ @param interval: Checking frequency
2020+ @param updateFunction: Update display
2021+ """
2022+ self ._interval = interval
2023+ self ._timer = threading .Timer (self ._interval , self ._run )
2024+ self ._updateFunction = updateFunction
2025+ self .is_running = False
2026+ self .start ()
2027+
2028+ def _run (self ):
2029+ self .is_running = False
2030+ self .start ()
2031+ self ._updateFunction ()
2032+
2033+ def start (self ):
2034+ if not self .is_running :
2035+ self ._timer = threading .Timer (self ._interval , self ._run )
2036+ self ._timer .start ()
2037+ self .is_running = True
2038+
2039+ def stop (self ):
2040+ self ._timer .cancel ()
2041+ self .is_running = False
2042+
2043+
2044+ BRAILLE_UPDATE_CHECK_INTERVAL : float = 0.5
2045+ """Timer interval in milliseconds for L{BrailleHandler._enqueueBrailleUpdateCheck}."""
2046+
2047+
20062048class BrailleHandler (baseObject .AutoPropertyObject ):
20072049 # TETHER_AUTO, TETHER_FOCUS, TETHER_REVIEW and tetherValues
20082050 # are deprecated, but remain to retain API backwards compatibility
@@ -2042,6 +2084,17 @@ def __init__(self):
20422084 self ._cursorPos = None
20432085 self ._cursorBlinkUp = True
20442086 self ._cells = []
2087+ self ._oldCells : List [int ] = []
2088+ self ._showSelection : FeatureFlag = config .conf ["braille" ]["showSelection" ]
2089+ self ._showCursor : bool = config .conf ["braille" ]["showCursor" ]
2090+ # Was braille line updated during previous timer cycle.
2091+ self ._alreadyUpdated : bool = False
2092+ self ._handleUpdateQueue = deque (maxlen = 1 )
2093+ self ._updateTimer = UpdateTimer (
2094+ BRAILLE_UPDATE_CHECK_INTERVAL ,
2095+ self ._enqueueBrailleUpdateCheck
2096+ )
2097+ self ._updateTimer .start ()
20452098 self ._cursorBlinkTimer = None
20462099 config .post_configProfileSwitch .register (self .handlePostConfigProfileSwitch )
20472100 if config .conf ["braille" ]["tetherTo" ] == TetherTo .AUTO .value :
@@ -2064,6 +2117,9 @@ def terminate(self):
20642117 if self ._cursorBlinkTimer :
20652118 self ._cursorBlinkTimer .Stop ()
20662119 self ._cursorBlinkTimer = None
2120+ if self ._updateTimer :
2121+ self ._updateTimer .stop ()
2122+ self ._updateTimer = None
20672123 config .post_configProfileSwitch .unregister (self .handlePostConfigProfileSwitch )
20682124 if self .display :
20692125 self .display .terminate ()
@@ -2341,6 +2397,10 @@ def update(self):
23412397 self ._cells = cells + [0 ] * (self .displaySize - len (cells ))
23422398 self ._cursorPos = self .buffer .cursorWindowPos
23432399 self ._updateDisplay ()
2400+ self ._oldCells = self .buffer .windowBrailleCells .copy ()
2401+ self ._showSelection = config .conf ["braille" ]["showSelection" ]
2402+ self ._showCursor = config .conf ["braille" ]["showCursor" ]
2403+ self ._alreadyUpdated = True
23442404
23452405 def scrollForward (self ):
23462406 self .buffer .scrollForward ()
@@ -2468,6 +2528,12 @@ def handleCaretMove(
24682528 if shouldAutoTether :
24692529 self .setTether (TetherTo .FOCUS .value , auto = True )
24702530 if self ._tether != TetherTo .FOCUS .value :
2531+ # Braille display content is updated in case where:
2532+ # braille is tethered to review, review cursor does not follow system caret,
2533+ # and focus object is navigator object.
2534+ if not config .conf ["reviewCursor" ]["followCaret" ]:
2535+ if obj == api .getNavigatorObject ():
2536+ self .handleUpdate (obj )
24712537 return
24722538 region = self .mainBuffer .regions [- 1 ] if self .mainBuffer .regions else None
24732539 if region and region .obj == obj :
@@ -2552,6 +2618,8 @@ def handleUpdate(self, obj: "NVDAObject") -> None:
25522618 region .update ()
25532619 self .mainBuffer .update ()
25542620 self .mainBuffer .restoreWindow ()
2621+ if self ._oldCells == self .buffer .windowBrailleCells :
2622+ return
25552623 if self .buffer is self .mainBuffer :
25562624 self .update ()
25572625 elif self .buffer is self .messageBuffer and keyboardHandler .keyCounter > self ._keyCountForLastMessage :
@@ -2686,6 +2754,35 @@ def _ackTimeoutResetter(self, param: int):
26862754 self .display ._awaitingAck = False
26872755 self ._writeCellsInBackground ()
26882756
2757+ def _brailleUpdateCheck (self ) -> None :
2758+ """Braille may need update when show cursor or show selection state change or when in terminal window."""
2759+ if self .buffer is not self .mainBuffer :
2760+ return
2761+ if self ._alreadyUpdated :
2762+ self ._alreadyUpdated = False
2763+ return
2764+ obj : NVDAObject
2765+ if api .isObjectInActiveTreeInterceptor (api .getNavigatorObject ()):
2766+ obj = api .getCaretObject ()
2767+ elif self .getTether () == TetherTo .FOCUS .value :
2768+ obj = api .getFocusObject ()
2769+ else :
2770+ obj = api .getNavigatorObject ()
2771+ # Handles updates in terminal windows, and also "show selection" state
2772+ # change in all appropriate windows.
2773+ if (
2774+ hasattr (obj , "role" ) and obj .role == controlTypes .Role .TERMINAL
2775+ or self ._showSelection != config .conf ["braille" ]["showSelection" ]
2776+ ):
2777+ self .handleUpdate (obj )
2778+ # Toggles braille cursor in other appropriate windows.
2779+ if self ._showCursor != config .conf ["braille" ]["showCursor" ]:
2780+ self .update ()
2781+
2782+ def _enqueueBrailleUpdateCheck (self ) -> None :
2783+ """Enques braille update check."""
2784+ self ._handleUpdateQueue .append (self ._brailleUpdateCheck )
2785+
26892786
26902787# Maps old braille display driver names to new drivers that supersede old drivers.
26912788# Ensure that if a user has set a preferred driver which has changed name, the new
@@ -2720,14 +2817,17 @@ def initialize():
27202817 handler .setDisplayByName (config .conf ["braille" ]["display" ])
27212818
27222819def pumpAll ():
2723- """Runs tasks at the end of each core cycle. For now just caret updates."""
2820+ """Runs tasks at the end of each core cycle."""
2821+ if len (handler ._handleUpdateQueue ):
2822+ handler ._handleUpdateQueue .popleft ()()
27242823 handler .handlePendingCaretUpdate ()
27252824
27262825def terminate ():
27272826 global handler
27282827 handler .terminate ()
27292828 handler = None
27302829
2830+
27312831class BrailleDisplayDriver (driverHandler .Driver ):
27322832 """Abstract base braille display driver.
27332833 Each braille display driver should be a separate Python module in the root brailleDisplayDrivers directory
0 commit comments