Skip to content

Commit 74f272c

Browse files
authored
Merge da686b8 into 6dd9002
2 parents 6dd9002 + da686b8 commit 74f272c

10 files changed

Lines changed: 257 additions & 96 deletions

File tree

source/braille.py

Lines changed: 120 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# A part of NonVisual Desktop Access (NVDA)
22
# This file is covered by the GNU General Public License.
33
# See the file COPYING for more details.
4-
# Copyright (C) 2008-2022 NV Access Limited, Joseph Lee, Babbage B.V., Davy Kager, Bram Duvigneau,
4+
# Copyright (C) 2008-2023 NV Access Limited, Joseph Lee, Babbage B.V., Davy Kager, Bram Duvigneau,
55
# Leonard de Ruijter
66

77
import itertools
@@ -304,6 +304,11 @@
304304
)
305305
SELECTION_SHAPE = 0xC0 #: Dots 7 and 8
306306

307+
#: The braille shape shown on a braille display when
308+
#: the number of cells used by the braille handler is lower than the actual number of cells.
309+
#: The 0 based position of the shape is equal to the number of cells used by the braille handler.
310+
END_OF_BRAILLE_OUTPUT_SHAPE = 0xFF # All dots
311+
307312
#: Unicode braille indicator at the start of untranslated braille input.
308313
INPUT_START_IND = u"⣏"
309314
#: Unicode braille indicator at the end of untranslated braille input.
@@ -1871,11 +1876,53 @@ class BrailleHandler(baseObject.AutoPropertyObject):
18711876
queuedWriteLock: threading.Lock
18721877
ackTimerHandle: int
18731878

1879+
#: Notifies when cells are about to be written to a braille display.
1880+
#: This allows components and add-ons to perform an action.
1881+
#: For example, when a system is controlled by a braille enabled remote system,
1882+
#: the remote system should know what cells to show on its display.
1883+
#: @param cells: The list of braille cells.
1884+
#: @type cells: [int]
1885+
#: @param rawText: The raw text that corresponds with the cells.
1886+
#: @type rawText: str
1887+
#: @param currentCellCount: The current number of cells
1888+
#: @type currentCellCount: bool
1889+
pre_writeCells: extensionPoints.Action
1890+
#: Filter that allows components or add-ons to change the display size used for braille output.
1891+
#: For example, when a system is controlled by a remote system while having a 80 cells display connected,
1892+
#: the display size should be lowered to 40 whenever the remote system has a 40 cells display connected.
1893+
#: @param value: the number of cells of the current display.
1894+
#: @type value: int
1895+
filter_displaySize: extensionPoints.Filter
1896+
#: Action that allows components or add-ons to be notified of display size changes.
1897+
#: For example, when a system is controlled by a remote system and the remote system swaps displays,
1898+
#: The local system should be notified about display size changes at the remote system.
1899+
#: @param displaySize: The current display size used by the braille handler.
1900+
#: @type displaySize: int
1901+
displaySizeChanged: extensionPoints.Action
1902+
#: Action that allows components or add-ons to be notified of braille display changes.
1903+
#: For example, when a system is controlled by a remote system and the remote system swaps displays,
1904+
#: The local system should be notified about display parameters at the remote system,
1905+
#: e.g. name and cellcount.
1906+
#: @param display: The new braille display driver
1907+
#: @type display: L{BrailleDisplayDriver}
1908+
#: @param isFallback: Whether the display is set as fallback display due to another display's failure
1909+
#: @type isFallback: bool
1910+
#: @param detected: If the display was set by auto detection, the device match that matched the driver
1911+
#: @type detected: bdDetect.DeviceMatch or C{None}
1912+
displayChanged: extensionPoints.Action
1913+
#: Allows components or add-ons to decide whether the braille handler should be forcefully disabled.
1914+
#: For example, when a system is controlling a remote system with braille,
1915+
#: the local braille handler should be disabled as long as the system is in control of the remote system.
1916+
#: Handlers are called without arguments.
1917+
decide_enabled: extensionPoints.Decider
1918+
18741919
def __init__(self):
18751920
louisHelper.initialize()
18761921
self.display: Optional[BrailleDisplayDriver] = None
1877-
#: Number of cells the connected device (or if no device connected, what braille viewer has)
1878-
#: Zero cells disables braille. See L{_get_enabled}
1922+
#: Internal cache for the displaySize property.
1923+
#: This attribute is used to compare the displaySize output by l{filterDisplaySize}
1924+
#: with its previous output.
1925+
#: If the value differs, L{displaySizeChanged} is notified.
18791926
self._displaySize: int = 0
18801927
self.mainBuffer = BrailleBuffer(self)
18811928
self.messageBuffer = BrailleBuffer(self)
@@ -1901,6 +1948,12 @@ def __init__(self):
19011948

19021949
brailleViewer.postBrailleViewerToolToggledAction.register(self._onBrailleViewerChangedState)
19031950

1951+
self.pre_writeCells = extensionPoints.Action()
1952+
self.filter_displaySize = extensionPoints.Filter()
1953+
self.displaySizeChanged = extensionPoints.Action()
1954+
self.displayChanged = extensionPoints.Action()
1955+
self.decide_enabled = extensionPoints.Decider()
1956+
19041957
def terminate(self):
19051958
self._disableDetection()
19061959
if self._messageCallLater:
@@ -1937,22 +1990,49 @@ def _get_shouldAutoTether(self) -> bool:
19371990
return self.enabled and config.conf["braille"]["tetherTo"] == TetherTo.AUTO.value
19381991

19391992
displaySize: int
1993+
_cache_displaySize = True
19401994

19411995
def _get_displaySize(self):
1942-
if self._displaySize == 0 and brailleViewer.isBrailleViewerActive():
1943-
return brailleViewer.DEFAULT_NUM_CELLS
1944-
return self._displaySize
1945-
1946-
def _set_displaySize(self, numCells):
1947-
"""The display size can be changed while a display is connected, for instance
1948-
see L{brailleDisplayDrivers.alva.BrailleDisplayDriver} split point feature.
1996+
"""Returns the display size to use for braille output.
1997+
Handlers can register themselves to L{filter_displaySize} to change this value on the fly.
1998+
Therefore, this is a read only property and can't be set.
1999+
"""
2000+
numCells = self.display.numCells if self.display else 0
2001+
currentDisplaySize = self.filter_displaySize.apply(numCells)
2002+
if self._displaySize != currentDisplaySize:
2003+
self.displaySizeChanged.notify(displaySize=currentDisplaySize)
2004+
self._displaySize = currentDisplaySize
2005+
return currentDisplaySize
2006+
2007+
def _set_displaySize(self, value):
2008+
"""While the display size can be changed while a display is connected
2009+
(for instance see L{brailleDisplayDrivers.alva.BrailleDisplayDriver} split point feature),
2010+
it is not possible to override the display size using this property.
2011+
Consider registering a handler to L{filter_displaySize} instead.
19492012
"""
1950-
self._displaySize = numCells
2013+
raise AttributeError(
2014+
f"Can't set displaySize to {value}, consider registering a handler to filter_displaySize"
2015+
)
19512016

19522017
enabled: bool
2018+
_cache_enabled = True
19532019

19542020
def _get_enabled(self):
1955-
return bool(self.displaySize)
2021+
"""Returns whether braille is enabled.
2022+
Handlers can register themselves to L{decide_enabled} and return C{False}
2023+
to forcefully disable the braille handler.
2024+
If components need to change the state from disabled to enabled instead,
2025+
they should register to L{filter_displaySize}.
2026+
By default, the enabled/disabled state is based on the boolean value of L{displaySize},
2027+
and thus is C{True} when the display size is greater than 0.
2028+
This is a read only property and can't be set.
2029+
"""
2030+
return bool(self.displaySize) and self.decide_enabled.decide()
2031+
2032+
def _set_enabled(self, value):
2033+
raise AttributeError(
2034+
f"Can't set enabled to {value}, consider registering a handler to decide_enabled or filter_displaySize"
2035+
)
19562036

19572037
_lastRequestedDisplayName = None
19582038
"""The name of the last requested braille display driver with setDisplayByName,
@@ -1996,7 +2076,8 @@ def setDisplayByName( # noqa: C901
19962076
oldDisplay = self.display
19972077
if detected and bdDetect._isDebug():
19982078
log.debug("Possibly detected display '%s'" % newDisplay.description)
1999-
if newDisplay == oldDisplay.__class__:
2079+
sameDisplayReinit = newDisplay == oldDisplay.__class__
2080+
if sameDisplayReinit:
20002081
# This is the same driver as was already set, so just re-initialise it.
20012082
log.debug("Reinitializing %s braille display"%name)
20022083
oldDisplay.terminate()
@@ -2021,7 +2102,6 @@ def setDisplayByName( # noqa: C901
20212102
log.error("Error terminating previous display driver", exc_info=True)
20222103
self.display = newDisplay
20232104
newDisplay.initSettings()
2024-
self._displaySize = newDisplay.numCells
20252105
if isFallback:
20262106
if self._detectionEnabled and not self._detector:
20272107
# As this is the fallback display, which is usually noBraille,
@@ -2036,6 +2116,12 @@ def setDisplayByName( # noqa: C901
20362116
queueHandler.queueFunction(queueHandler.eventQueue, self.initialDisplay)
20372117
if detected and 'bluetoothName' in detected.deviceInfo:
20382118
self._enableDetection(bluetooth=False, keepCurrentDisplay=True, limitToDevices=[name])
2119+
# #14503: optimization, avoid notifications of unnecessary re-initialization
2120+
# of the noBraille display
2121+
# When setDisplayByName is refactored, ensure that braille display detection no longer triggers
2122+
# an unnecessary reinit of noBraille.
2123+
if not (sameDisplayReinit and newDisplay.name == "noBraille"):
2124+
self.displayChanged.notify(display=newDisplay, isFallback=isFallback, detected=detected)
20392125
return True
20402126
except:
20412127
# For auto display detection, logging an error for every failure is too obnoxious.
@@ -2068,7 +2154,26 @@ def _updateDisplay(self):
20682154
wx.CallAfter(self._cursorBlinkTimer.Start,blinkRate)
20692155

20702156
def _writeCells(self, cells: List[int]):
2071-
brailleViewer.update(cells, self._rawText)
2157+
handlerCellCount = self.displaySize
2158+
self.pre_writeCells.notify(cells=cells, rawText=self._rawText, currentCellCount=handlerCellCount)
2159+
displayCellCount = self.display.numCells
2160+
if not displayCellCount:
2161+
# No physical display to write to
2162+
return
2163+
# Braille displays expect cells to be padded up to displayCellCount.
2164+
# However, the braille handler uses handlerCellCount to calculate the number of cells.
2165+
cellCountDif = displayCellCount - len(cells)
2166+
if cellCountDif < 0:
2167+
# There are more cells than the connected display could take.
2168+
log.warning(
2169+
f"Connected display {self.display.name!r} has {displayCellCount} cells, "
2170+
f"while braille handler is using {handlerCellCount} cells"
2171+
)
2172+
cells = cells[:displayCellCount]
2173+
elif cellCountDif > 0:
2174+
# The connected display could take more cells than the braille handler produces.
2175+
# Displays expect cells to be padded up to the number of cells.
2176+
cells += [END_OF_BRAILLE_OUTPUT_SHAPE] + [0] * (cellCountDif - 1)
20722177
if not self.display.isThreadSafe:
20732178
try:
20742179
self.display.display(cells)

source/brailleDisplayDrivers/alva.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ def _get_model(self):
124124
return self.model
125125

126126
def _updateSettings(self):
127-
oldNumCells = self.numCells
128127
if self.isHid:
129128
displaySettings = self._dev.getFeature(ALVA_DISPLAY_SETTINGS_REPORT)
130129
if displaySettings[ALVA_DISPLAY_SETTINGS_STATUS_CELL_SIDE_POS] > 1:
@@ -152,9 +151,6 @@ def _updateSettings(self):
152151
self._ser6SendMessage(b"H", b"?")
153152
# Get HID keyboard input state
154153
self._ser6SendMessage(b"r", b"?")
155-
if oldNumCells not in (0, self.numCells):
156-
# In case of splitpoint changes, we need to update the braille handler as well
157-
braille.handler.displaySize = self.numCells
158154

159155
def __init__(self, port="auto"):
160156
super(BrailleDisplayDriver,self).__init__()

source/brailleViewer/__init__.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
# brailleViewer.py
21
# A part of NonVisual Desktop Access (NVDA)
3-
# Copyright (C) 2014-2019 NV Access Limited
2+
# Copyright (C) 2014-2023 NV Access Limited, Leonard de Ruijter
43
# This file is covered by the GNU General Public License.
54
# See the file COPYING for more details.
65
from typing import Optional, List
@@ -63,20 +62,16 @@ def isBrailleViewerActive() -> bool:
6362
return bool(_brailleGui)
6463

6564

66-
def update(cells: List[int], rawText: str):
67-
if _brailleGui:
68-
_brailleGui.updateBrailleDisplayed(
69-
cells,
70-
rawText,
71-
_getDisplaySize()
72-
)
73-
74-
7565
def destroyBrailleViewer():
7666
global _brailleGui
7767
d: Optional[BrailleViewerFrame] = _brailleGui
7868
_brailleGui = None # protect against re-entrance
7969
if d and not d.isDestroyed:
70+
import braille # imported late to avoid a circular import.
71+
updateBrailleDisplayedUnregistered = braille.handler.pre_writeCells.unregister(d.updateBrailleDisplayed)
72+
assert updateBrailleDisplayedUnregistered
73+
getDisplaySizeUnregistered = braille.handler.filter_displaySize.unregister(_getDisplaySize)
74+
assert getDisplaySizeUnregistered
8075
d.saveInfoAndDestroy()
8176

8277

@@ -91,9 +86,7 @@ def _onGuiDestroyed():
9186
postBrailleViewerToolToggledAction.notify(created=False)
9287

9388

94-
def _getDisplaySize():
95-
import braille # imported late to avoid a circular import.
96-
numCells = braille.handler.displaySize
89+
def _getDisplaySize(numCells: int):
9790
return numCells if numCells > 0 else DEFAULT_NUM_CELLS
9891

9992

@@ -105,13 +98,15 @@ def createBrailleViewerTool():
10598
if not braille.handler:
10699
raise RuntimeError("Can not initialise the BrailleViewerGui: braille.handler not yet initialised")
107100

101+
braille.handler.filter_displaySize.register(_getDisplaySize)
102+
108103
global _brailleGui
109104
if _brailleGui:
110105
destroyBrailleViewer()
111106

112107
_brailleGui = BrailleViewerFrame(
113-
_getDisplaySize(),
108+
braille.handler.displaySize,
114109
_onGuiDestroyed
115110
)
116-
111+
braille.handler.pre_writeCells.register(_brailleGui.updateBrailleDisplayed)
117112
postBrailleViewerToolToggledAction.notify(created=True)

source/extensionPoints/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class Filter(HandlerRegistrar):
8080
>>> messageFilter.register(filterMessage)
8181
8282
When filtering is desired, all registered handlers are called to filter the data, see L{util.callWithSupportedKwargs}
83-
for how args passed to notify are mapped to the handler:
83+
for how args passed to apply are mapped to the handler:
8484
8585
>>> messageFilter.apply("This is a message", someArg=42)
8686
'This is a message which has been filtered'
@@ -125,7 +125,7 @@ class Decider(HandlerRegistrar):
125125
126126
When the decision is to be made, registered handlers are called until
127127
a handler returns False, see L{util.callWithSupportedKwargs}
128-
for how args passed to notify are mapped to the handler:
128+
for how args passed to decide are mapped to the handler:
129129
130130
>>> doSomething.decide(someArg=42)
131131
False
@@ -175,9 +175,9 @@ class AccumulatingDecider(HandlerRegistrar):
175175
...
176176
>>> doSomething.register(shouldDoSomething)
177177
178-
When the decision is to be made registered handlers are called and they return values are collected,
178+
When the decision is to be made registered handlers are called and their return values are collected,
179179
see L{util.callWithSupportedKwargs}
180-
for how args passed to notify are mapped to the handler:
180+
for how args passed to decide are mapped to the handler:
181181
182182
>>> doSomething.decide(someArg=42)
183183
False

source/inputCore.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import languageHandler
4141
import controlTypes
4242
import winKernel
43+
import extensionPoints
4344

4445

4546
InputGestureBindingClassT = TypeVar("InputGestureBindingClassT")
@@ -253,19 +254,21 @@ def clear(self):
253254
self._map.clear()
254255
self.lastUpdateContainedError = False
255256

256-
def add(self, gesture, module, className, script,replace=False):
257+
def add(
258+
self,
259+
gesture: str,
260+
module: str,
261+
className: str,
262+
script: Optional[str],
263+
replace: bool = False
264+
):
257265
"""Add a gesture mapping.
258266
@param gesture: The gesture identifier.
259-
@type gesture: str
260267
@param module: The name of the Python module containing the target script.
261-
@type module: str
262268
@param className: The name of the class in L{module} containing the target script.
263-
@type className: str
264269
@param script: The name of the target script
265270
or C{None} to unbind the gesture for this class.
266-
@type script: str
267271
@param replace: if true replaces all existing bindings for this gesture with the given script, otherwise only appends this binding.
268-
@type replace: boolean
269272
"""
270273
gesture = normalizeGestureIdentifier(gesture)
271274
try:
@@ -449,6 +452,15 @@ def __init__(self):
449452
self.loadUserGestureMap()
450453
self._lastInputTime = None
451454

455+
#: Notifies when a gesture is about to be executed,
456+
#: and allows components or add-ons to decide whether or not to execute a gesture.
457+
#: For example, when controlling a remote system with a connected local braille display,
458+
#: braille display gestures should not be executed locally.
459+
#: Handlers are called with one argument:
460+
#: @param gesture: The gesture that is about to be executed.
461+
#: @type gesture: L{InputGesture}
462+
self.decide_executeGesture = extensionPoints.Decider()
463+
452464
def executeGesture(self, gesture):
453465
"""Perform the action associated with a gesture.
454466
@param gesture: The gesture to execute.
@@ -461,6 +473,15 @@ def executeGesture(self, gesture):
461473
# as well as stopping a flood of actions when the core revives.
462474
raise NoInputGestureAction
463475

476+
if not self.decide_executeGesture.decide(gesture=gesture):
477+
# A registered handler decided that this gesture shouldn't be executed.
478+
# Purposely do not raise a NoInputGestureAction here, as that could
479+
# lead to unexpected behavior for gesture emulation.
480+
log.debug(
481+
"Gesture execution canceled by handler registered to decide_executeGesture extension point"
482+
)
483+
return
484+
464485
script = gesture.script
465486
focus = api.getFocusObject()
466487
if focus.sleepMode is focus.SLEEP_FULL or (focus.sleepMode and not getattr(script, 'allowInSleepMode', False)):

0 commit comments

Comments
 (0)