Skip to content

Commit c24a561

Browse files
authored
Merge 03e8f37 into b53692a
2 parents b53692a + 03e8f37 commit c24a561

8 files changed

Lines changed: 194 additions & 48 deletions

File tree

source/braille.py

Lines changed: 79 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
@@ -300,6 +300,11 @@
300300
)
301301
SELECTION_SHAPE = 0xC0 #: Dots 7 and 8
302302

303+
#: The braille shape shown on a braille display when
304+
#: the number of cells used by the braille handler is lower than the actual number of cells.
305+
#: The 0 based position of the shape is equal to the number of cells used by the braille handler.
306+
END_OF_BRAILLE_OUTPUT_SHAPE = 0xFF # All dots
307+
303308
#: Unicode braille indicator at the start of untranslated braille input.
304309
INPUT_START_IND = u"⣏"
305310
#: Unicode braille indicator at the end of untranslated braille input.
@@ -1779,9 +1784,6 @@ class BrailleHandler(baseObject.AutoPropertyObject):
17791784
def __init__(self):
17801785
louisHelper.initialize()
17811786
self.display: Optional[BrailleDisplayDriver] = None
1782-
#: Number of cells the connected device (or if no device connected, what braille viewer has)
1783-
#: Zero cells disables braille. See L{_get_enabled}
1784-
self._displaySize: int = 0
17851787
self.mainBuffer = BrailleBuffer(self)
17861788
self.messageBuffer = BrailleBuffer(self)
17871789
self._messageCallLater = None
@@ -1806,6 +1808,29 @@ def __init__(self):
18061808

18071809
brailleViewer.postBrailleViewerToolToggledAction.register(self._onBrailleViewerChangedState)
18081810

1811+
#: Notifies when cells are about to be written to a braille display.
1812+
#: This allows components and add-ons to perform an action.
1813+
#: For example, when a system is controlled by a braille enabled remote system,
1814+
#: the remote system should know what cells to show on its display.
1815+
#: @param cells: The list of braille cells.
1816+
#: @type cells: [int]
1817+
#: @param rawText: The raw text that corresponds with the cells.
1818+
#: @type rawText: str
1819+
self.pre_writeCells = extensionPoints.Action()
1820+
1821+
#: Filter that allows components or add-ons to change the display size used for braille output.
1822+
#: For example, when a system is controlled by a remote system while having a 80 cells display connected,
1823+
#: the display size should be lowered to 40 whenever the remote system has a 40 cells display connected.
1824+
#: @param value: the number of cells of the current display.
1825+
#: @type value: int
1826+
self.filter_displaySize = extensionPoints.Filter()
1827+
1828+
#: Allows components or add-ons to decide whether the braille handler should be forcefully disabled.
1829+
#: For example, when a system is controlling a remote system with braille,
1830+
#: the local braille handler should be disabled as long as the system is in control of the remote system.
1831+
#: Handlers are called without arguments.
1832+
self.decide_enabled = extensionPoints.Decider()
1833+
18091834
def terminate(self):
18101835
self._disableDetection()
18111836
if self._messageCallLater:
@@ -1844,20 +1869,41 @@ def _get_shouldAutoTether(self) -> bool:
18441869
displaySize: int
18451870

18461871
def _get_displaySize(self):
1847-
if self._displaySize == 0 and brailleViewer.isBrailleViewerActive():
1848-
return brailleViewer.DEFAULT_NUM_CELLS
1849-
return self._displaySize
1850-
1851-
def _set_displaySize(self, numCells):
1852-
"""The display size can be changed while a display is connected, for instance
1853-
see L{brailleDisplayDrivers.alva.BrailleDisplayDriver} split point feature.
1872+
"""Returns the display size to use for braille output.
1873+
Handlers can register themselves to L{filter_displaySize} to change this value on the fly.
1874+
Therefore, this is a read only property and can't be set.
18541875
"""
1855-
self._displaySize = numCells
1876+
numCells = self.display.numCells if self.display else 0
1877+
return self.filter_displaySize.apply(numCells)
1878+
1879+
def _set_displaySize(self, value):
1880+
"""While the display size can be changed while a display is connected
1881+
(for instance see L{brailleDisplayDrivers.alva.BrailleDisplayDriver} split point feature),
1882+
it is not possible to override the display size using this property.
1883+
Consider registering a handler to L{filter_displaySize} instead.
1884+
"""
1885+
raise AttributeError(
1886+
f"Can't set displaySize to {value}, consider registering a handler to filter_displaySize"
1887+
)
18561888

18571889
enabled: bool
18581890

18591891
def _get_enabled(self):
1860-
return bool(self.displaySize)
1892+
"""Returns whether braille is enabled.
1893+
Handlers can register themselves to L{decide_enabled} and return C{False}
1894+
to forcefully disable the braille handler.
1895+
If components need to change the state from disabled to enabled instead,
1896+
they should register to L{filter_displaySize}.
1897+
By default, the enabled/disabled state is based on the boolean value of L{displaySize},
1898+
and thus is C{True} when the display size is greater than 0.
1899+
This is a read only property and can't be set.
1900+
"""
1901+
return bool(self.displaySize) and self.decide_enabled.decide()
1902+
1903+
def _set_enabled(self, value):
1904+
raise AttributeError(
1905+
f"Can't set enabled to {value}, consider registering a handler to decide_enabled or filter_displaySize"
1906+
)
18611907

18621908
_lastRequestedDisplayName = None
18631909
"""The name of the last requested braille display driver with setDisplayByName,
@@ -1926,7 +1972,6 @@ def setDisplayByName( # noqa: C901
19261972
log.error("Error terminating previous display driver", exc_info=True)
19271973
self.display = newDisplay
19281974
newDisplay.initSettings()
1929-
self._displaySize = newDisplay.numCells
19301975
if isFallback:
19311976
if self._detectionEnabled and not self._detector:
19321977
# As this is the fallback display, which is usually noBraille,
@@ -1973,7 +2018,26 @@ def _updateDisplay(self):
19732018
wx.CallAfter(self._cursorBlinkTimer.Start,blinkRate)
19742019

19752020
def _writeCells(self, cells: List[int]):
1976-
brailleViewer.update(cells, self._rawText)
2021+
self.pre_writeCells.notify(cells=cells, rawText=self._rawText)
2022+
displayCellCount = self.display.numCells
2023+
handlerCellCount = self.displaySize
2024+
if not displayCellCount:
2025+
# No physical display to write to
2026+
return
2027+
# Braille displays expect cells to be padded up to displayCellCount.
2028+
# However, the braille handler uses handlerCellCount to calculate the number of cells.
2029+
cellCountDif = displayCellCount - len(cells)
2030+
if cellCountDif < 0:
2031+
# There are more cells than the connected display could take.
2032+
log.warning(
2033+
f"Connected display {self.display.name!r} has {displayCellCount} cells, "
2034+
f"while braille handler is using {handlerCellCount} cells"
2035+
)
2036+
cells = cells[:displayCellCount]
2037+
elif cellCountDif > 0:
2038+
# The connected display could take more cells than the braille handler produces.
2039+
# Displays expect cells to be padded up to the number of cells.
2040+
cells += [END_OF_BRAILLE_OUTPUT_SHAPE] + [0] * (cellCountDif - 1)
19772041
if not self.display.isThreadSafe:
19782042
try:
19792043
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 & 7 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
@@ -65,10 +64,11 @@ def isBrailleViewerActive() -> bool:
6564

6665
def update(cells: List[int], rawText: str):
6766
if _brailleGui:
67+
import braille # imported late to avoid a circular import.
6868
_brailleGui.updateBrailleDisplayed(
6969
cells,
7070
rawText,
71-
_getDisplaySize()
71+
braille.handler.displaySize
7272
)
7373

7474

@@ -77,6 +77,9 @@ def destroyBrailleViewer():
7777
d: Optional[BrailleViewerFrame] = _brailleGui
7878
_brailleGui = None # protect against re-entrance
7979
if d and not d.isDestroyed:
80+
import braille # imported late to avoid a circular import.
81+
braille.handler.pre_writeCells.unregister(update)
82+
braille.handler.filter_displaySize.unregister(_getDisplaySize)
8083
d.saveInfoAndDestroy()
8184

8285

@@ -91,9 +94,7 @@ def _onGuiDestroyed():
9194
postBrailleViewerToolToggledAction.notify(created=False)
9295

9396

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

99100

@@ -105,12 +106,15 @@ def createBrailleViewerTool():
105106
if not braille.handler:
106107
raise RuntimeError("Can not initialise the BrailleViewerGui: braille.handler not yet initialised")
107108

109+
braille.handler.filter_displaySize.register(_getDisplaySize)
110+
braille.handler.pre_writeCells.register(update)
111+
108112
global _brailleGui
109113
if _brailleGui:
110114
destroyBrailleViewer()
111115

112116
_brailleGui = BrailleViewerFrame(
113-
_getDisplaySize(),
117+
braille.handler.displaySize,
114118
_onGuiDestroyed
115119
)
116120

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)):

source/nvwave.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2007-2021 NV Access Limited, Aleksey Sadovoy, Cyrille Bougot, Peter Vágner
2+
# Copyright (C) 2007-2021 NV Access Limited, Aleksey Sadovoy, Cyrille Bougot, Peter Vágner, Babbage B.V.,
3+
# Leonard de Ruijter
34
# This file is covered by the GNU General Public License.
45
# See the file COPYING for more details.
56

@@ -37,16 +38,30 @@
3738
import config
3839
from logHandler import log
3940
import os.path
41+
import extensionPoints
42+
4043

4144
__all__ = (
42-
"WavePlayer", "getOutputDeviceNames", "outputDeviceIDToName", "outputDeviceNameToID",
45+
"WavePlayer",
46+
"getOutputDeviceNames",
47+
"outputDeviceIDToName",
48+
"outputDeviceNameToID",
49+
"decide_playWaveFile",
4350
)
4451

4552
winmm = windll.winmm
4653

4754
HWAVEOUT = HANDLE
4855
LPHWAVEOUT = POINTER(HWAVEOUT)
4956

57+
#: Notifies when a wave file is about to be played,
58+
#: and allows components or add-ons to decide whether the wave file should be played.
59+
#: For example, when controlling a remote system,
60+
#: the remote system must be notified of sounds played on the local system.
61+
#: Also, registrars should be able to suppress playing sounds if desired.
62+
#: Handlers are called with the same arguments as L{playWaveFile} as keyword arguments.
63+
decide_playWaveFile = extensionPoints.Decider()
64+
5065
class WAVEFORMATEX(Structure):
5166
_fields_ = [
5267
("wFormatTag", WORD),
@@ -627,16 +642,31 @@ def outputDeviceNameToID(name: str, useDefaultIfInvalid=False) -> int:
627642
fileWavePlayerThread = None
628643

629644

630-
def playWaveFile(fileName, asynchronous=True):
645+
def playWaveFile(
646+
fileName: str,
647+
asynchronous: bool = True,
648+
isSPeechWaveFileCommand: bool = False
649+
):
631650
"""plays a specified wave file.
651+
@param fileName: the path to the wave file, usually absolute.
632652
@param asynchronous: whether the wave file should be played asynchronously
633-
@type asynchronous: bool
653+
If C{False}, the calling thread is blocked until the wave has finished playing.
654+
@param isSPeechWaveFileCommand: whether this wave is played as part of a speech sequence.
634655
"""
635656
global fileWavePlayer, fileWavePlayerThread
636657
f = wave.open(fileName,"r")
637658
if f is None: raise RuntimeError("can not open file %s"%fileName)
638659
if fileWavePlayer is not None:
639660
fileWavePlayer.stop()
661+
if not decide_playWaveFile.decide(
662+
fileName=fileName,
663+
asynchronous=asynchronous,
664+
isSPeechWaveFileCommand=isSPeechWaveFileCommand
665+
):
666+
log.debug(
667+
"Playing wave file canceled by handler registered to decide_playWaveFile extension point"
668+
)
669+
return
640670
fileWavePlayer = WavePlayer(
641671
channels=f.getnchannels(),
642672
samplesPerSec=f.getframerate(),

0 commit comments

Comments
 (0)