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,16 @@ def __init__(self):
20422084 self ._cursorPos = None
20432085 self ._cursorBlinkUp = True
20442086 self ._cells = []
2087+ self ._showSelection : FeatureFlag = config .conf ["braille" ]["showSelection" ]
2088+ self ._showCursor : bool = config .conf ["braille" ]["showCursor" ]
2089+ # Was braille line updated during previous timer cycle.
2090+ self ._alreadyUpdated : bool = False
2091+ self ._handleUpdateQueue = deque (maxlen = 1 )
2092+ self ._updateTimer = UpdateTimer (
2093+ BRAILLE_UPDATE_CHECK_INTERVAL ,
2094+ self ._enqueueBrailleUpdateCheck
2095+ )
2096+ self ._updateTimer .start ()
20452097 self ._cursorBlinkTimer = None
20462098 config .post_configProfileSwitch .register (self .handlePostConfigProfileSwitch )
20472099 if config .conf ["braille" ]["tetherTo" ] == TetherTo .AUTO .value :
@@ -2064,6 +2116,9 @@ def terminate(self):
20642116 if self ._cursorBlinkTimer :
20652117 self ._cursorBlinkTimer .Stop ()
20662118 self ._cursorBlinkTimer = None
2119+ if self ._updateTimer :
2120+ self ._updateTimer .stop ()
2121+ self ._updateTimer = None
20672122 config .post_configProfileSwitch .unregister (self .handlePostConfigProfileSwitch )
20682123 if self .display :
20692124 self .display .terminate ()
@@ -2332,6 +2387,7 @@ def _blink(self):
23322387 self ._displayWithCursor ()
23332388
23342389 def update (self ):
2390+ self ._alreadyUpdated = True
23352391 cells = self .buffer .windowBrailleCells
23362392 self ._rawText = self .buffer .windowRawText
23372393 if log .isEnabledFor (log .IO ):
@@ -2341,6 +2397,8 @@ def update(self):
23412397 self ._cells = cells + [0 ] * (self .displaySize - len (cells ))
23422398 self ._cursorPos = self .buffer .cursorWindowPos
23432399 self ._updateDisplay ()
2400+ self ._showSelection = config .conf ["braille" ]["showSelection" ]
2401+ self ._showCursor = config .conf ["braille" ]["showCursor" ]
23442402
23452403 def scrollForward (self ):
23462404 self .buffer .scrollForward ()
@@ -2468,6 +2526,12 @@ def handleCaretMove(
24682526 if shouldAutoTether :
24692527 self .setTether (TetherTo .FOCUS .value , auto = True )
24702528 if self ._tether != TetherTo .FOCUS .value :
2529+ # Braille display content is updated in case where:
2530+ # braille is tethered to review, review cursor does not follow system caret,
2531+ # and focus object is navigator object.
2532+ if not config .conf ["reviewCursor" ]["followCaret" ]:
2533+ if obj == api .getNavigatorObject ():
2534+ self .handleUpdate (obj )
24712535 return
24722536 region = self .mainBuffer .regions [- 1 ] if self .mainBuffer .regions else None
24732537 if region and region .obj == obj :
@@ -2686,6 +2750,32 @@ def _ackTimeoutResetter(self, param: int):
26862750 self .display ._awaitingAck = False
26872751 self ._writeCellsInBackground ()
26882752
2753+ def _brailleUpdateCheck (self ) -> None :
2754+ """Braille may need update when show cursor or show selection state change or when in terminal window."""
2755+ if self .buffer is not self .mainBuffer :
2756+ return
2757+ if self ._alreadyUpdated :
2758+ self ._alreadyUpdated = False
2759+ return
2760+ obj : NVDAObject
2761+ if api .isObjectInActiveTreeInterceptor (api .getNavigatorObject ()):
2762+ obj = api .getCaretObject ()
2763+ elif self .getTether () == TetherTo .FOCUS .value :
2764+ obj = api .getFocusObject ()
2765+ else :
2766+ obj = api .getNavigatorObject ()
2767+ if (
2768+ hasattr (obj , "role" ) and obj .role == controlTypes .Role .TERMINAL
2769+ or self ._showSelection != config .conf ["braille" ]["showSelection" ]
2770+ ):
2771+ self .handleUpdate (obj )
2772+ elif self ._showCursor != config .conf ["braille" ]["showCursor" ]:
2773+ self .update ()
2774+
2775+ def _enqueueBrailleUpdateCheck (self ) -> None :
2776+ """Enques braille update check."""
2777+ self ._handleUpdateQueue .append (self ._brailleUpdateCheck )
2778+
26892779
26902780# Maps old braille display driver names to new drivers that supersede old drivers.
26912781# Ensure that if a user has set a preferred driver which has changed name, the new
@@ -2720,14 +2810,17 @@ def initialize():
27202810 handler .setDisplayByName (config .conf ["braille" ]["display" ])
27212811
27222812def pumpAll ():
2723- """Runs tasks at the end of each core cycle. For now just caret updates."""
2813+ """Runs tasks at the end of each core cycle."""
2814+ if len (handler ._handleUpdateQueue ):
2815+ handler ._handleUpdateQueue .popleft ()()
27242816 handler .handlePendingCaretUpdate ()
27252817
27262818def terminate ():
27272819 global handler
27282820 handler .terminate ()
27292821 handler = None
27302822
2823+
27312824class BrailleDisplayDriver (driverHandler .Driver ):
27322825 """Abstract base braille display driver.
27332826 Each braille display driver should be a separate Python module in the root brailleDisplayDrivers directory
0 commit comments