Add multi routing gesture and select with routing command#20028
Conversation
…ccess#20001) `braille.BrailleDisplayGesture.routingIndex` (single int) was too narrow: "routing" excluded non-routing cell-addressed gestures (e.g. Handy Tech Active Tactile Control), and simultaneous routing key presses had unspecified behavior (last index won). Core API (source/braille.py): - New `cellIndexes: list[int]` canonical attribute for any cell-addressed gesture (routing, touch, ATC, ...). - `BrailleDisplayGesture.idForCellCount(n)` helper returning `"routing"` for <=1 cell, `"multiRouting"` for >1. - `_get_routingIndex`/`_set_routingIndex` deprecation shim using the `AutoPropertyObject` pattern: getter returns first cell, setter wraps into a single-element list. Warning gated on `NVDAState._allowDeprecatedAPI()`. Scripts (source/globalCommands.py): - `script_braille_routeTo` and `script_braille_reportFormatting` now read `cellIndexes[0]`. - New `script_braille_selectToCell` (unassigned by default) selects text between first and last pressed routing cells when bound to a `multiRouting` gesture. Driver migration (source/brailleDisplayDrivers/*): all 22 drivers migrated from `self.routingIndex = X` to `self.cellIndexes = [X]`. Multi-routing emission added where the protocol already reports simultaneous presses: - baum: iterate bitmap of routing keys - handyTech: aggregate routing key codes from pressed-key set - albatross: aggregate primary routing range indexes - nlseReaderZoomax: iterate routing keys bitmap - seikantk: emit single gesture with sorted indexes instead of one gesture per key Remote compat (source/_remoteClient/*): - Sender serializes `cellIndexes` plus legacy `routingIndex` (first cell) for old peers. - Receiver normalizes legacy `routingIndex` into `cellIndexes` before attribute assignment, avoiding a deprecation warning. brailleViewer: `brailleViewerInputGesture.py` sets `cellIndexes`. Tests (tests/unit/test_braille/test_brailleDisplayDrivers.py): added coverage for default `cellIndexes`, `idForCellCount`, deprecated `routingIndex` getter/setter round-trip, and `multiRouting` identifier regex validity. Docs (user_docs/en/changes.md): new-feature entry for multi-routing selection, developer change for `cellIndexes`/`multiRouting`/helper, deprecation entry for `routingIndex`.
- cellIndexes default is now None instead of empty list, matching the previous routingIndex = None convention. - Replace #: style comments with docstrings for ID_ROUTING, ID_MULTI_ROUTING, and cellIndexes. - Update deprecated routingIndex setter to return None (not []). - Update tests to expect None default and None on clear. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- idForCellCount now accepts an optional baseName parameter (default "routing"). For count > 1, prepends "multi" with the first character uppercased: "routing" → "multiRouting", "secondRouting" → "multiSecondRouting", "route" → "multiRoute". - Albatross driver: collect all routing ranges into a dict keyed by range name, then merge into a flat cellIndexes list preserving duplicates for cross-row simultaneous presses. - Alva driver: same aggregation pattern for "routing" and "secondRouting" ranges, replacing the previous overwrite behavior. - Added test_idForCellCount_custom_baseName covering secondRouting, route, and upperRouting base names. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
These class constants on BrailleDisplayGesture are superseded by the idForCellCount helper which dynamically builds the gesture id for any routing range name. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…orCellCount - brailliantB: aggregate routing keys into list, support multi-press - hidBrailleStandard: collect routing keys with prefix, support multi-press producing routerSet1_multiRouterKey for simultaneous presses - Switch all drivers from braille.BrailleDisplayGesture.idForCellCount to self.idForCellCount for consistency Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The end position of the selection no longer includes the character at the last routing key, making the selection range exclusive. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Make the deprecated routingIndex getter and the remote client legacy field use max(cellIndexes) consistently, so single-cell behavior is preserved while multi-cell gestures resolve to the highest index. Also guard script_braille_selectToCell against cellIndexes being None. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restore backwards compatibility for callers that pass a single int to InputGestureRouting, normalizing it to a one-element list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Caller (_handleRouting) already passes a fresh sorted list, and the single-int compat path constructs a new list. No need to copy again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds default bindings for the new braille_selectToCell command on drivers that emit a multiRouting gesture: ALVA, Albatross, Baum, Brailliant B, Handy Tech, HID Braille, NLS eReader Zoomax, and Seika Notetaker. Updates the user-facing changelog to list these drivers explicitly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace 'braille cells' with 'braille routing keys' in the script description, error message and changelog entry to match the actual input concept. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Allows subclasses to override the helper if they need a different naming convention while still letting drivers call self.idForCellCount. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, when NVDAState._allowDeprecatedAPI() returned False, the routingIndex getter and setter still returned/applied the value and only suppressed the log message. That defeated the purpose of the flag, which is meant to prove a code path is free of deprecated API usage. Now raise AttributeError instead, mirroring the pattern used elsewhere (e.g. addonHandler._pickleToJsonMigration). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR enhances NVDA’s braille input model to support gestures that address multiple braille cells at once (e.g., multiple routing keys pressed simultaneously), and adds a new built-in command to select text directly from the braille display using multi-routing.
Changes:
- Added
cellIndexes(list of addressed cell indexes) tobraille.BrailleDisplayGesture, deprecatedroutingIndex, and introducedBrailleDisplayGesture.idForCellCount(...)for consistent gesture ids. - Updated in-tree braille display drivers (and Braille Viewer) to populate
cellIndexesand emitmultiRouting(and related) ids where appropriate; boundmultiRoutingto the new selection command on supported drivers. - Added
script_braille_selectToCelland propagatedcellIndexesacross NVDA Remote while retaining legacyroutingIndexcompatibility.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| user_docs/en/changes.md | Documents the new multi-routing gesture, selection command, and the cellIndexes API/deprecation. |
| tests/unit/test_braille/test_brailleDisplayDrivers.py | Adds unit tests for cellIndexes, idForCellCount, and the routingIndex compatibility shim. |
| source/globalCommands.py | Updates routing/formatting scripts to use cellIndexes; adds braille_selectToCell command. |
| source/brailleViewer/brailleViewerInputGesture.py | Migrates Braille Viewer routing gesture to cellIndexes. |
| source/brailleDisplayDrivers/seikantk.py | Aggregates routing indexes into cellIndexes, emits multi ids, and binds multiRouting to selection. |
| source/brailleDisplayDrivers/seika.py | Migrates routing gesture to cellIndexes. |
| source/brailleDisplayDrivers/papenmeier_serial.py | Migrates routing handling to cellIndexes while preserving existing ids. |
| source/brailleDisplayDrivers/papenmeier.py | Migrates routing gesture to cellIndexes. |
| source/brailleDisplayDrivers/nlseReaderZoomax.py | Collects multiple routing keys into cellIndexes, uses idForCellCount, binds selection. |
| source/brailleDisplayDrivers/nattiqbraille.py | Migrates routing gesture to cellIndexes. |
| source/brailleDisplayDrivers/lilli.py | Migrates routing gesture to cellIndexes. |
| source/brailleDisplayDrivers/hims.py | Migrates routing gesture to cellIndexes. |
| source/brailleDisplayDrivers/hidBrailleStandard.py | Aggregates routing keys into cellIndexes, emits routerSet1_multiRouterKey, binds selection. |
| source/brailleDisplayDrivers/hedoProfiLine.py | Migrates routing gesture to cellIndexes. |
| source/brailleDisplayDrivers/hedoMobilLine.py | Migrates routing gesture to cellIndexes. |
| source/brailleDisplayDrivers/handyTech.py | Aggregates routing indexes, uses idForCellCount, binds selection. |
| source/brailleDisplayDrivers/freedomScientific.py | Migrates routing gesture to cellIndexes. |
| source/brailleDisplayDrivers/eurobraille/gestures.py | Migrates routing gesture to cellIndexes. |
| source/brailleDisplayDrivers/ecoBraille.py | Migrates routing gesture to cellIndexes. |
| source/brailleDisplayDrivers/brltty.py | Migrates routing gesture to cellIndexes. |
| source/brailleDisplayDrivers/brailliantB.py | Aggregates routing indexes, uses idForCellCount, binds selection. |
| source/brailleDisplayDrivers/brailleNote.py | Migrates routing gesture to cellIndexes. |
| source/brailleDisplayDrivers/baum.py | Aggregates routing indexes, uses idForCellCount, binds selection. |
| source/brailleDisplayDrivers/alva.py | Supports multiple routing ranges via per-range aggregation and idForCellCount; binds selection. |
| source/brailleDisplayDrivers/albatross/gestures.py | Supports multiple routing ranges via aggregation and idForCellCount; binds selection. |
| source/braille.py | Introduces cellIndexes, adds idForCellCount, and implements deprecated routingIndex shim. |
| source/_remoteClient/session.py | Propagates cellIndexes over Remote while providing legacy routingIndex. |
| source/_remoteClient/input.py | Normalizes legacy routingIndex into cellIndexes to avoid triggering deprecation warnings. |
The previous name suggested a single destination cell, while the command actually selects a range between two routing keys. Update all driver gesture maps and the changelog to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the redundant 'Requires a display that reports simultaneous routing key presses' sentence. The input gestures dialog already shows the bound gesture, and the wording is now consistent with sibling braille script descriptions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The selection is half-open: it includes the cell at the first pressed routing key but stops at (does not include) the cell at the last pressed routing key. 'Between' was misleading. Use 'from the first up to the last' instead, both in the script description and in the changelog entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
BAUM_ROUTING_KEY (0x27) sends a 1-indexed key number per event, not a cumulative bitmask like other commands. The previous numeric comparison `arg < prev` incorrectly treated a lower key number (e.g. pressing key 3 while holding key 4) as a release, breaking single-key routing and making multi-key combos impossible. Normalize each incoming key number to its bitmask bit and OR it into the stored value before the shared press/release comparison. arg=0 (all keys released) passes through unchanged, where `0 < prevBitmask` correctly triggers the release path. This also enables multi-key routing combos on BAUM_ROUTING_KEY devices such as the VarioUltra. Update InputGesture to treat BAUM_ROUTING_KEY identically to BAUM_ROUTING_KEYS, iterating the stored bitmask to populate cellIndexes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Document braille_selectRange (multiRouting) for all drivers that gained the binding in this branch: ALVA, HandyTech, Baum, HumanWare Brailliant, Seika Notetaker, NLS eReader Zoomax, Standard HID Braille, and Albatross. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@cary-rowen The focus is a driver limitation indeed, see #20077. Have you also been able to test with your Seika3Pro? |
|
Thanks @LeonarddeR
Yes, this feature also doesn't work for Seika3Pro. However, I haven't looked at the code at all, and I'd really like to help you debug it to see if it's a limitation of the driver layer or some other reason. |
To make sure, does that device use the Seika Braille Displays driver or the Seika Notetake one?
Thanks. If it is the notetake driver, it would help if you could report back on current behavior with keyboard help on, i.e. does the routing announcement happen at press or release, and what happens if you combine multiple of them? |
|
Hi @LeonarddeR
This device does not use the Seika Notetaker driver; instead, it requires the Seika Braille driver, which must be installed separately. |
|
Note for reviewers: The only untested driver is currently the |
- Wrap deprecated routingIndex getter/setter in if NVDAState._allowDeprecatedAPI() block; warn only on setter
- Move NVDAState import to top level in braille.py
- Convert L{} Epytext to Sphinx :attr:/:class: in BrailleDisplayGesture docstring
- Add type hints for gesture parameter on braille scripts in globalCommands.py
- Move inner _Gesture test fixtures to module-level _RoutingGesture/_MultiRoutingGesture
- Update hims.py to modern copyright header; add type hint on RoutingInputGesture.__init__
- Gate legacy routingIndex in session.py to single-cell presses only
- Fix duplicate deprecation entry in changes.md (was in 2026.2, belongs in 2026.3)
- Split deprecation changelog sentence onto new line
- Add spaces and backtick formatting to new userGuide.md table rows
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes #20077. Follow-up of #20028. Summary of the issue: The Freedom Scientific driver ignored routing button presses entirely (if isPress: return) and fired a single-cell RoutingGesture on each release. It could not signal which routing keys were held together, so the "multi routing" / "select range" infrastructure added in #20028 had no effect on Focus/PAC Mate displays. Description of user facing changes: Pressing multiple routing keys simultaneously on Freedom Scientific Focus/PAC Mate displays is now a single "multi routing" gesture, bound to "select range" by default. Single routing presses (route to cell) and the top routing row (scroll) keep working as before. Description of developer facing changes: brailleDisplayDrivers.freedomScientific.RoutingGesture is deprecated; use KeyGesture, which now carries routing state. Backward compatibility is preserved via a module-level __getattr__ / MovedSymbol. KeyGesture gains routingKeyBits / topRoutingKeyBits parameters, sets cellIndexes, and builds its id via idForCellCount. Description of development approach: Routing presses are now tracked as bitmasks (_routingKeyBits, _topRoutingKeyBits) in _handleRoutingKey; one combined KeyGesture fires on the first release, then the bits clear. This mirrors the existing _handleKeys / _updateKeyBits release model instead of firing per key. KeyGesture merges the cells addressed by every routing range into one sorted cellIndexes list and emits an idForCellCount part per range, mirroring the ALVA and Standard HID Braille drivers. Alongside the fix, the driver was modernized/simplified with no behavior change: Python 3 idioms (super(), f-strings, OSError, list[int] / X | None), full type hints, Sphinx-style docstrings, list-comprehension _translate, sum-based checksum, extracted _executeGesture and _bitmaskToIndexes helpers, and removal of dead code, stale Python 2 comments and pylint pragmas.
Link to issue number:
Closes #20001
Summary of the issue:
NVDA's
BrailleDisplayGestureonly exposed a singleroutingIndex, so braille displays that report multiple simultaneously pressed routing keys could not bind a meaningful "multi routing" gesture. There was also no way for a user to select a range of text directly from the braille display.Description of user facing changes:
script_braille_selectRange). The selection is half-open: it starts at the cell under the first pressed routing key and ends just before the cell under the last pressed routing key. On drivers that emitmultiRoutingnatively, the gesture is bound to this command out of the box; users of other displays can bind it manually if they wish.multiRouting→braille_selectRangebound by default: ALVA, Albatross (bound tohome1+multiRoutingandhome2+multiRoutingdue to device-level limitation), Baum (and compatible), HumanWare Brailliant BI/B series, Handy Tech, NLS eReader Zoomax, Seika Notetaker, Standard HID Braille displays.Description of developer facing changes:
braille.BrailleDisplayGestureexposescellIndexes: list[int] | None. The single-valuedroutingIndexis now a deprecated property that falls back tomax(cellIndexes)for compatibility. WhenNVDAState._allowDeprecatedAPI()returnsFalsethe getter and setter raiseAttributeError.BrailleDisplayGesture.idForCellCount(count, baseName="routing")(classmethod) builds the canonical id ("routing"/"multiRouting"/"multiSecondRouting").cellIndexesis not limited to routing keys; touch-sensitive cells (e.g. Handy Tech ATC) can reuse it._remoteClientpropagatescellIndexeswhile keepingroutingIndexpopulated for older peers.seikantk.InputGestureRouting.__init__now accepts a list of indexes; a single int is still accepted for backwards compatibility.Description of development approach:
cellIndexesand a deprecation shim forroutingIndexonBrailleDisplayGesture.self.routingIndex = xtoself.cellIndexes = [x](or to a collected list where the protocol reports several keys at once).idForCellCountso drivers with multiple routing ranges (e.g. ALVArouting+secondRouting, HIDrouterSet1collections yield ids likemultiRouting,multiSecondRouting,routerSet1_multiRouterKey.script_braille_selectRangeinglobalCommandsusinggetTextInfoForWindowPos+setEndPoint("endToEnd")+updateSelection(). Guarded againstcellIndexesbeingNone/short and againstNotImplementedErrorfrom the focused control.multiRouting→braille_selectRangein the gesture map of every driver whose driver-side implementation now reports simultaneous routing keys.Testing strategy:
Unit tests added in
tests/unit/test_braille/test_brailleDisplayDrivers.pycovering gesture id construction andcellIndexespopulation for multi-key presses.Manual testing performed on the following drivers (all updated to emit
multiRouting):Per-driver multi-routing support matrix
| Driver | Multi routing emitted |
|---|---|---|
| ALVA | Yes (incl.
multiSecondRouting) || Albatross | Only when mixed with other key(s); bound to
home1+multiRoutingandhome2+multiRouting|| Baum | Yes |
| Brailliant B (HumanWare) | Yes |
| Handy Tech | Yes |
| HID Braille (standard) | Yes (
routerSet1_multiRouterKey) || NLS eReader Zoomax | Yes |
| Seika Notetaker | Yes |
| BrailleNote (HumanWare) | No |
| brltty | No |
| EcoBraille | No |
| Eurobraille | No |
| Freedom Scientific | No |
| hedoMobilLine | No |
| hedoProfiLine | No |
| HIMS | No |
| Lilli | No |
| Nattiq | No |
| Papenmeier (USB + serial) | No |
| Seika (non-Notetaker) | No |
| Braille Viewer (built-in) | No |
Known issues with pull request:
multiRoutingbecause their current driver-side implementation does not aggregate simultaneous routing key presses. This is not necessarily a hard protocol limitation. Adding support for any of them is a follow-up that only requires collecting indexes intocellIndexesand usingidForCellCount.home1orhome2). This is a device-level limitation and cannot be worked around in the driver.Code Review Checklist:
cellIndexes,idForCellCount, deprecation entry)routingIndexgetter/setter preserved with deprecation warning under_allowDeprecatedAPI();seikantk.InputGestureRoutingaccepts legacyint.)