-
-
Notifications
You must be signed in to change notification settings - Fork 766
New braille IPC APIs: BrailleMirror, DirectBrailleWindow, and named-pipe server #19856
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
2e3d9ae
13072dc
c64bb83
06e9a5c
6c7056e
b6deb2f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,8 +1,8 @@ | ||||||
| # A part of NonVisual Desktop Access (NVDA) | ||||||
| # This file is covered by the GNU General Public License. | ||||||
| # See the file COPYING for more details. | ||||||
| # Copyright (C) 2008-2025 NV Access Limited, Joseph Lee, Babbage B.V., Davy Kager, Bram Duvigneau, | ||||||
| # Leonard de Ruijter, Burman's Computer and Education Ltd., Julien Cochuyt | ||||||
| # Copyright (C) 2008-2026 NV Access Limited, Joseph Lee, Babbage B.V., Davy Kager, Bram Duvigneau, | ||||||
| # Leonard de Ruijter, Burman's Computer and Education Ltd., Julien Cochuyt, Pneuma Solutions | ||||||
|
|
||||||
| from enum import StrEnum | ||||||
| import itertools | ||||||
|
|
@@ -67,6 +67,7 @@ | |||||
| import hwPortUtils | ||||||
| import bdDetect | ||||||
| import queueHandler | ||||||
| import winUser | ||||||
| import brailleViewer | ||||||
| from autoSettingsUtils.driverSetting import BooleanDriverSetting, NumericDriverSetting | ||||||
| from utils.security import objectBelowLockScreenAndWindowsIsLocked, post_sessionLockStateChanged | ||||||
|
|
@@ -3434,6 +3435,9 @@ def initialize(): | |||||
| handler = BrailleHandler() | ||||||
| handler.handlePostConfigProfileSwitch() | ||||||
| config.post_configProfileSwitch.register(handler.handlePostConfigProfileSwitch) | ||||||
| import braillePipeServer | ||||||
|
|
||||||
| braillePipeServer.initialize() | ||||||
|
|
||||||
|
|
||||||
| def pumpAll(): | ||||||
|
|
@@ -3443,6 +3447,9 @@ def pumpAll(): | |||||
|
|
||||||
| def terminate(): | ||||||
| global handler | ||||||
| import braillePipeServer | ||||||
|
|
||||||
| braillePipeServer.terminate() | ||||||
| handler.terminate() | ||||||
| handler = None | ||||||
|
|
||||||
|
|
@@ -3969,6 +3976,177 @@ def getDisplayTextForIdentifier(cls, identifier): | |||||
| inputCore.registerGestureSource("br", BrailleDisplayGesture) | ||||||
|
|
||||||
|
|
||||||
| class BrailleMirror: | ||||||
| """Abstract base class for a braille mirror. | ||||||
|
|
||||||
| A mirror intercepts every braille display update and can optionally influence the negotiated display width. | ||||||
| Both the physical display and all registered mirrors receive the same cells simultaneously; the mirror does **not** suppress the local display. | ||||||
| Register an instance with :func:`registerMirror` and remove it with :func:`unregisterMirror`. To inject a gesture back into NVDA (e.g. a routing key received over a remote channel) use :func:`injectGesture`. | ||||||
| """ | ||||||
|
|
||||||
| def display(self, cells: List[int]) -> None: | ||||||
| """Called with the full cell array on every display update. | ||||||
|
|
||||||
| :param cells: The braille cells written to the display. | ||||||
| """ | ||||||
|
|
||||||
| def numCells(self) -> int: | ||||||
| """Return the number of cells this mirror can show. | ||||||
|
|
||||||
| Return 0 (the default) to have no effect on the negotiated display | ||||||
| width. A positive value caps the display width used by | ||||||
| :data:`filter_displayDimensions` to the smallest value across all registered mirrors and the physical display. | ||||||
| """ | ||||||
| return 0 | ||||||
|
|
||||||
|
|
||||||
| _registeredMirrors: List["BrailleMirror"] = [] | ||||||
|
|
||||||
|
|
||||||
| def _mirrorPreWriteCells(cells: List[int], **kwargs) -> None: | ||||||
| for mirror in _registeredMirrors: | ||||||
| mirror.display(cells) | ||||||
|
Comment on lines
+4006
to
+4008
|
||||||
|
|
||||||
|
|
||||||
| def _mirrorFilterDisplayDimensions(value: DisplayDimensions) -> DisplayDimensions: | ||||||
| sizes = [m.numCells() for m in _registeredMirrors if m.numCells() > 0] | ||||||
| if not sizes: | ||||||
| return value | ||||||
| cap = min(sizes) | ||||||
| if cap >= value.numCols: | ||||||
| return value | ||||||
| return value._replace(numCols=cap) | ||||||
|
|
||||||
|
|
||||||
| def registerMirror(mirror: BrailleMirror) -> None: | ||||||
| """Register *mirror* to receive braille display updates. | ||||||
|
|
||||||
| :meth:`BrailleMirror.display` will be called on the main thread for every subsequent :meth:`BrailleHandler._writeCells` call. If *mirror* returns a positive value from :meth:`BrailleMirror.numCells`, it will also participate in display-width negotiation via :data:`filter_displayDimensions`. | ||||||
| """ | ||||||
| if not _registeredMirrors: | ||||||
| pre_writeCells.register(_mirrorPreWriteCells) | ||||||
| filter_displayDimensions.register(_mirrorFilterDisplayDimensions) | ||||||
| _registeredMirrors.append(mirror) | ||||||
| if handler: | ||||||
| handler._refreshEnabled(block=True) | ||||||
|
|
||||||
|
|
||||||
| def unregisterMirror(mirror: BrailleMirror) -> None: | ||||||
| """Remove a previously registered mirror. | ||||||
|
|
||||||
| Safe to call even if *mirror* is not currently registered. | ||||||
| """ | ||||||
| try: | ||||||
| _registeredMirrors.remove(mirror) | ||||||
| except ValueError: | ||||||
| return | ||||||
| if not _registeredMirrors: | ||||||
| pre_writeCells.unregister(_mirrorPreWriteCells) | ||||||
| filter_displayDimensions.unregister(_mirrorFilterDisplayDimensions) | ||||||
| if handler: | ||||||
| handler._refreshEnabled(block=True) | ||||||
|
|
||||||
|
|
||||||
| def injectGesture(gesture: BrailleDisplayGesture) -> None: | ||||||
| """Inject *gesture* into NVDA's input pipeline. | ||||||
|
|
||||||
| This is a thin wrapper around :func:`inputCore.manager.executeGesture` that silently swallows :class:`inputCore.NoInputGestureAction` so callers do not need to handle the common case where no script is bound. | ||||||
|
|
||||||
| Thread safety: must be called on the main thread, or scheduled via ``wx.CallAfter`` from a background thread. | ||||||
| """ | ||||||
| try: | ||||||
| inputCore.manager.executeGesture(gesture) | ||||||
| except inputCore.NoInputGestureAction: | ||||||
| pass | ||||||
|
|
||||||
|
|
||||||
| class DirectBrailleWindow: | ||||||
| """Take over braille output and input while a specific window has focus. | ||||||
|
|
||||||
| When the window identified by *hwnd* is the foreground window, NVDA's own braille rendering is suppressed. The application drives what appears on the physical display by calling :meth:`display`, and all braille gestures are forwarded to :meth:`onGesture` instead of being processed by NVDA. | ||||||
| When the window loses focus, normal NVDA rendering resumes automatically. | ||||||
|
|
||||||
| :param hwnd: The HWND of the window that triggers direct braille mode. | ||||||
| :param numCells: Advertised display width; 0 means use whatever NVDA provides. A positive value caps the negotiated display width via :data:`filter_displayDimensions`. | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self, hwnd: int, numCells: int = 0) -> None: | ||||||
| self._hwnd = hwnd | ||||||
| self._numCells = numCells | ||||||
| self._active = False | ||||||
|
|
||||||
| def _isForeground(self) -> bool: | ||||||
| """Return True if our registered window is currently in the foreground.""" | ||||||
| fg = winUser.getForegroundWindow() | ||||||
| return fg == self._hwnd or winUser.isDescendantWindow(self._hwnd, fg) | ||||||
|
||||||
| return fg == self._hwnd or winUser.isDescendantWindow(self._hwnd, fg) | |
| return fg == self._hwnd or winUser.isDescendantWindow(fg, self._hwnd) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should be avoiding adding substantial new code to
source/braille.py(See #12772)Could you please move the added classes and functions to a new file (e.g. source/_brailleMirror.py)?