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
77import itertools
304304)
305305SELECTION_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.
308313INPUT_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 )
0 commit comments