Skip to content

Commit ef8163f

Browse files
authored
Merge 84be35d into 72545d7
2 parents 72545d7 + 84be35d commit ef8163f

File tree

1 file changed

+101
-1
lines changed

1 file changed

+101
-1
lines changed

source/braille.py

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
Tuple,
2020
Union,
2121
Type,
22+
Callable,
2223
)
2324
from locale import strxfrm
2425

@@ -42,6 +43,7 @@
4243
TetherTo,
4344
ReportTableHeaders,
4445
)
46+
from config.featureFlag import FeatureFlag
4547
from config.featureFlagEnums import ReviewRoutingMovesSystemCaretFlag
4648
from logHandler import log
4749
import controlTypes
@@ -53,6 +55,7 @@
5355
import re
5456
import scriptHandler
5557
import collections
58+
from collections import deque
5659
import extensionPoints
5760
import hwPortUtils
5861
import 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+
20062048
class 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

27222819
def 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

27262825
def terminate():
27272826
global handler
27282827
handler.terminate()
27292828
handler = None
27302829

2830+
27312831
class 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

Comments
 (0)