Skip to content
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ea08bd0
Add cellIndexes to BrailleDisplayGesture, deprecate routingIndex (#20…
LeonarddeR Apr 22, 2026
40de144
Use None default for cellIndexes and switch to docstring comments
LeonarddeR Apr 23, 2026
b276be9
Generalize idForCellCount for multi-row routing ranges
LeonarddeR Apr 23, 2026
1cc6d8e
Remove unused ID_ROUTING and ID_MULTI_ROUTING constants
LeonarddeR Apr 23, 2026
8e0db96
Add multi-routing to brailliantB and hidBrailleStandard, use self.idF…
LeonarddeR Apr 23, 2026
56b4c10
Make braille multi-routing selection exclusive
LeonarddeR Apr 24, 2026
7330906
Use max(cellIndexes) for routingIndex compat and harden selectToCell
LeonarddeR Apr 28, 2026
08ceb30
seikantk: keep single-int compat for InputGestureRouting
LeonarddeR Apr 28, 2026
48029fa
seikantk: drop redundant list() wrapping of cellIndexes
LeonarddeR Apr 28, 2026
3d84779
Bind multiRouting to braille_selectToCell in supporting drivers
LeonarddeR Apr 28, 2026
2d1c59b
Reword braille_selectToCell user-facing strings to use 'routing keys'
LeonarddeR Apr 28, 2026
e6fff6b
Make BrailleDisplayGesture.idForCellCount a classmethod
LeonarddeR Apr 28, 2026
8953afe
Raise AttributeError on routingIndex when deprecated APIs are disabled
LeonarddeR Apr 28, 2026
7cacaac
Rename script_braille_selectToCell to script_braille_selectRange
LeonarddeR Apr 28, 2026
9f7ace0
Pre-commit auto-fix
pre-commit-ci[bot] Apr 28, 2026
19a9a1b
Trim braille_selectRange script description
LeonarddeR Apr 28, 2026
7f54d8f
Reword braille_selectRange description to reflect exclusive end
LeonarddeR Apr 28, 2026
eef3764
Update tests/unit/test_braille/test_brailleDisplayDrivers.py
LeonarddeR Apr 28, 2026
c7bae13
Fix HID
LeonarddeR Apr 28, 2026
16d781f
Merge branch 'cellIndexes' of https://github.com/leonardder/nvda into…
LeonarddeR Apr 28, 2026
87b1a58
Update description
LeonarddeR Apr 28, 2026
59b37a3
Merge branch 'master' into cellIndexes
LeonarddeR Apr 30, 2026
4d98ff4
Merge remote-tracking branch 'origin/master' into cellIndexes
LeonarddeR May 5, 2026
aa484f8
No longer mark Albatross as supported for multi routing
LeonarddeR May 7, 2026
b4efb8d
Merge remote-tracking branch 'origin/master' into cellIndexes
LeonarddeR May 7, 2026
218ae35
Restore driver level multiRouting but don't assign by default as per …
LeonarddeR May 8, 2026
e542736
Merge remote-tracking branch 'origin/master' into cellIndexes
LeonarddeR May 13, 2026
325de5b
Update changelog
LeonarddeR May 13, 2026
ed6ecf1
Merge remote-tracking branch 'origin/master' into cellIndexes
LeonarddeR May 15, 2026
a9cf47d
Merge remote-tracking branch 'origin/master' into cellIndexes
LeonarddeR May 18, 2026
d78b3da
Fix BAUM_ROUTING_KEY press/release detection and multi-key combos
LeonarddeR May 18, 2026
6a2047e
Add better display names for routing
LeonarddeR May 18, 2026
859f829
Merge remote-tracking branch 'origin/master' into cellIndexes
LeonarddeR May 25, 2026
33a30bb
feat(albatross): bind braille_selectRange to home1/2+multiRouting
LeonarddeR May 25, 2026
d3cc500
docs(changelog): add Albatross to multi routing driver list
LeonarddeR May 25, 2026
5f2ccd8
Merge remote-tracking branch 'origin/master' into cellIndexes
LeonarddeR May 27, 2026
dfdd42e
Fix identifiers
LeonarddeR May 27, 2026
0373057
Fix identifier order
LeonarddeR May 27, 2026
f6893e0
Update headers
LeonarddeR May 27, 2026
bb15450
Merge remote-tracking branch 'origin/master' into cellIndexes
LeonarddeR May 28, 2026
99072af
Address code review feedback
LeonarddeR May 28, 2026
6c01d51
Extend tests
LeonarddeR May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions source/_remoteClient/input.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2015-2025 NV Access Limited, Christopher Toth, Tyler Spivey, Babbage B.V., David Sexton and others.
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2015-2026 NV Access Limited, Christopher Toth, Tyler Spivey, Babbage B.V., David Sexton,
# Leonard de Ruijter and others.
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

import ctypes
from enum import IntEnum
Expand Down Expand Up @@ -30,6 +31,11 @@ class VKMapType(IntEnum):
class BrailleInputGesture(braille.BrailleDisplayGesture, brailleInput.BrailleInputGesture):
def __init__(self, **kwargs):
super().__init__()
# Normalize legacy routingIndex field into cellIndexes before assignment
# to avoid triggering the deprecation warning on the setter.
legacyRoutingIndex = kwargs.pop("routingIndex", None)
if "cellIndexes" not in kwargs and legacyRoutingIndex is not None:
kwargs["cellIndexes"] = [legacyRoutingIndex]
for key, value in kwargs.items():
setattr(self, key, value)
self.source = f"remote{self.source.capitalize()}"
Expand Down
13 changes: 8 additions & 5 deletions source/_remoteClient/session.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2015-2025 NV Access Limited, Christopher Toth, Tyler Spivey, Babbage B.V., David Sexton and others.
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2015-2026 NV Access Limited, Christopher Toth, Tyler Spivey, Babbage B.V., David Sexton,
# Leonard de Ruijter and others.
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

"""NVDA Remote session management and message routing.

Expand Down Expand Up @@ -662,8 +663,10 @@ def handleDecideExecuteGesture(
dict["dots"] = gesture.dots
if hasattr(gesture, "space") and "space" not in dict:
dict["space"] = gesture.space
if hasattr(gesture, "routingIndex") and "routingIndex" not in dict:
dict["routingIndex"] = gesture.routingIndex
if hasattr(gesture, "cellIndexes") and "cellIndexes" not in dict and gesture.cellIndexes:
dict["cellIndexes"] = gesture.cellIndexes
# Legacy field for older peers that only know routingIndex.
dict.setdefault("routingIndex", max(gesture.cellIndexes))
Comment thread
LeonarddeR marked this conversation as resolved.
Resolved
self.localMachine._dismissLocalBrailleMessage()
self.transport.send(type=RemoteMessageType.BRAILLE_INPUT, **dict)
return False
Expand Down
83 changes: 74 additions & 9 deletions source/braille.py
Original file line number Diff line number Diff line change
Expand Up @@ -3939,7 +3939,8 @@ class BrailleDisplayGesture(inputCore.InputGesture):
"""A button, wheel or other control pressed on a braille display.
Subclasses must provide L{source} and L{id}.
Optionally, L{model} can be provided to facilitate model specific gestures.
L{routingIndex} should be provided for routing buttons.
L{cellIndexes} should be provided for gestures addressed to specific braille cells,
Comment thread
LeonarddeR marked this conversation as resolved.
Resolved
such as routing keys or touch-sensitive cells (e.g. Handy Tech Active Tactile Control).
Subclasses can also inherit from L{brailleInput.BrailleInputGesture} if the display has a braille keyboard.
If the braille display driver is a L{baseObject.ScriptableObject}, it can provide scripts specific to input gestures from this display.
"""
Expand Down Expand Up @@ -3973,18 +3974,80 @@ def _get_id(self):
"""
raise NotImplementedError

#: The index of the routing key or C{None} if this is not a routing key.
#: @type: int
routingIndex = None
cellIndexes: list[int] | None = None
"""Indexes of braille cells addressed by this gesture, e.g. routing keys or touch cells.
C{None} if this gesture is not cell-addressed.
Comment thread
LeonarddeR marked this conversation as resolved.
Resolved
"""

@classmethod
def idForCellCount(cls, count: int, baseName: str = "routing") -> str:
"""Return the conventional gesture id suffix for a cell-addressed gesture.

When more than one cell is addressed, the base name is prefixed with ``"multi"``
and its first character is uppercased. For example::

idForCellCount(1, "routing") # "routing"
idForCellCount(2, "routing") # "multiRouting"
idForCellCount(2, "secondRouting") # "multiSecondRouting"

:param count: Number of cells addressed.
:param baseName: The gesture id for a single-cell press in this range.
:return: The base name if *count* <= 1, otherwise the multi-prefixed form.
"""
if count > 1:
return f"multi{baseName[0].upper()}{baseName[1:]}"
return baseName

def _get_routingIndex(self) -> int | None:
"""Deprecated. Use :attr:`cellIndexes` instead.

Returns the highest cell index, or ``None`` if no cells are addressed.
"""
import NVDAState
Comment thread
LeonarddeR marked this conversation as resolved.
Resolved

if not NVDAState._allowDeprecatedAPI():
raise AttributeError(
"BrailleDisplayGesture.routingIndex is deprecated, use cellIndexes instead.",
)
log.warning(
"BrailleDisplayGesture.routingIndex is deprecated, use cellIndexes instead.",
stack_info=True,
)
Comment thread
LeonarddeR marked this conversation as resolved.
Resolved
return max(self.cellIndexes) if self.cellIndexes else None

def _set_routingIndex(self, value: int | None) -> None:
"""Deprecated. Set :attr:`cellIndexes` instead."""
import NVDAState

if not NVDAState._allowDeprecatedAPI():
raise AttributeError(
"Setting BrailleDisplayGesture.routingIndex is deprecated, set cellIndexes instead.",
)
log.warning(
"Setting BrailleDisplayGesture.routingIndex is deprecated, set cellIndexes instead.",
stack_info=True,
)
self.cellIndexes = [value] if value is not None else None

_cellIndexesStr: str | None

def _get__cellIndexesStr(self) -> str | None:
"""A string representation of cell indexes for identification and display purposes."""
if "+" not in self.id and self.cellIndexes:
# This is an indexed gesture without additional keys, in which case the identifier can be extended with indexes.
return "+".join(f"{i + 1}" for i in self.cellIndexes)
return None

def _get_identifiers(self):
ids = ["br({source}):{id}".format(source=self.source, id=self.id)]
ids = []
if self._cellIndexesStr:
ids.append(f"br({self.source}):{self.id}{self._cellIndexesStr}")
ids.append(f"br({self.source}):{self.id}")
if self.model:
# Model based ids should take priority.
ids.insert(
0,
"br({source}.{model}):{id}".format(source=self.source, model=self.model, id=self.id),
)
if self._cellIndexesStr:
ids.insert(0, f"br({self.source}.{self.model}):{self.id}{self._cellIndexesStr}")
ids.insert(1, f"br({self.source}.{self.model}):{self.id}")
import brailleInput

if isinstance(self, brailleInput.BrailleInputGesture):
Expand All @@ -3998,6 +4061,8 @@ def _get_displayName(self):
name = brailleInput.BrailleInputGesture._get_displayName(self)
if name:
return name
if self._cellIndexesStr:
return f"{self.id}{self._cellIndexesStr}"
return self.id

def _get_scriptableObject(self):
Expand Down
13 changes: 11 additions & 2 deletions source/brailleDisplayDrivers/albatross/gestures.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"br(albatross):rWheelRight",
),
"braille_routeTo": ("br(albatross):routing",),
"braille_selectRange": ("br(albatross):home1+multiRouting", "br(albatross):home2+multiRouting"),
"braille_reportFormatting": ("br(albatross):secondRouting",),
"braille_toggleFocusContextPresentation": ("br(albatross):attribute1+attribute3",),
"speechMode": ("br(albatross):attribute2+attribute4",),
Expand Down Expand Up @@ -95,16 +96,24 @@ def __init__(self, keys: Set[int], name: str):
self.source = name
self.keyCodes = set(keys)
names = []
cellIndexesByRange: dict[str, list[int]] = {}
for key in self.keyCodes:
routingTuple = self._getRoutingIndex(key)
if routingTuple:
names.append(routingTuple[0])
self.routingIndex = routingTuple[1]
rangeName, index = routingTuple
cellIndexesByRange.setdefault(rangeName, []).append(index)
else:
try:
names.append(Keys(key).name)
except (KeyError, ValueError):
log.debug(f"Unknown key with id {key}")
if cellIndexesByRange:
allIndexes: list[int] = []
for rangeName, indexes in sorted(cellIndexesByRange.items()):
indexes.sort()
allIndexes.extend(indexes)
names.append(self.idForCellCount(len(indexes), rangeName))
self.cellIndexes = allIndexes
self.id = "+".join(names)
# Try to fix the first valid key press was not recognized as a gesture
if self.id and not self.script:
Expand Down
32 changes: 21 additions & 11 deletions source/brailleDisplayDrivers/alva.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# brailleDisplayDrivers/alva.py
# 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) 2009-2018 NV Access Limited, Davy Kager, Leonard de Ruijter, Optelec B.V.
# Copyright (C) 2009-2026 NV Access Limited, Davy Kager, Leonard de Ruijter, Optelec B.V.
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

from typing import List, Union

Expand Down Expand Up @@ -476,6 +475,7 @@ def script_toggleHidKeyboardInput(self, gesture):
"braille_scrollForward": ("br(alva):t5", "br(alva):etouch3"),
"braille_routeTo": ("br(alva):routing",),
"braille_reportFormatting": ("br(alva):secondRouting",),
"braille_selectRange": ("br(alva):multiRouting",),
"review_top": ("br(alva):t1+t2",),
"review_bottom": ("br(alva):t4+t5",),
"braille_toggleTether": ("br(alva):t1+t3",),
Expand Down Expand Up @@ -522,17 +522,16 @@ def __init__(self, model, keys, brailleInput=False):
secondaryNames = []
dots = 0
space = False
cellIndexesByRange: dict[str, list[int]] = {}
for group, number in self.keyCodes:
if group == ALVA_CR_GROUP:
if number & ALVA_2ND_CR_MASK:
keyName = "secondRouting"
self.routingIndex = number & ~ALVA_2ND_CR_MASK
rangeName = "secondRouting"
cellIndex = number & ~ALVA_2ND_CR_MASK
else:
keyName = "routing"
self.routingIndex = number
names.append(keyName)
if isNoBC640:
secondaryNames.append(keyName)
rangeName = "routing"
cellIndex = number
cellIndexesByRange.setdefault(rangeName, []).append(cellIndex)
else:
try:
keyName = ALVA_KEYS[group][number]
Expand All @@ -557,6 +556,17 @@ def __init__(self, model, keys, brailleInput=False):
else:
brailleInput = False

if cellIndexesByRange:
allIndexes: list[int] = []
for rangeName, indexes in sorted(cellIndexesByRange.items()):
indexes.sort()
allIndexes.extend(indexes)
idName = self.idForCellCount(len(indexes), rangeName)
names.append(idName)
if isNoBC640:
secondaryNames.append(idName)
self.cellIndexes = allIndexes

self.id = "+".join(names)
self.secondaryId = "+".join(secondaryNames) if isNoBC640 else self.id
if brailleInput:
Expand Down
33 changes: 17 additions & 16 deletions source/brailleDisplayDrivers/baum.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# -*- coding: UTF-8 -*-
# brailleDisplayDrivers/baum.py
# 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) 2010-2017 NV Access Limited, Babbage B.V.
# Copyright (C) 2010-2026 NV Access Limited, Babbage B.V., Leonard de Ruijter
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

from io import BytesIO
from typing import Union, List, Optional
Expand Down Expand Up @@ -299,7 +297,12 @@ def _handleResponse(self, command: bytes, arg: bytes):
self._deviceID = arg.decode("latin-1", errors="strict")
elif command in KEY_NAMES:
arg = sum(byte << offset * 8 for offset, byte in enumerate(arg))
if arg < self._keysDown.get(command, 0):
prevArg = self._keysDown.get(command, 0)
if command == BAUM_ROUTING_KEY and arg > 0:
# BAUM_ROUTING_KEY sends a 1-indexed key number, not a cumulative bitmask.
# Normalize to accumulated bitmask so the shared press/release logic below works.
arg = prevArg | (1 << (arg - 1))
if arg < prevArg:
# Release.
if not self._ignoreKeyReleases:
# The first key released executes the key combination.
Expand All @@ -316,7 +319,7 @@ def _handleResponse(self, command: bytes, arg: bytes):
self._ignoreKeyReleases = False
if arg > 0:
self._keysDown[command] = arg
elif command in self._keysDown:
elif prevArg:
# All keys in this group have been released.
# #3541: Remove this group so it doesn't count as a group with keys down.
del self._keysDown[command]
Expand All @@ -342,6 +345,7 @@ def display(self, cells: List[int]):
"braille_previousLine": ("br(baum):d1",),
"braille_nextLine": ("br(baum):d3",),
"braille_routeTo": ("br(baum):routing",),
"braille_selectRange": ("br(baum):multiRouting",),
"kb:upArrow": ("br(baum):up",),
"kb:downArrow": ("br(baum):down",),
"kb:leftArrow": ("br(baum):left",),
Expand Down Expand Up @@ -406,15 +410,12 @@ def __init__(self, model, keysDown):
# 0xfc covers command keys. The space bars are covered by 0x3.
self.dots = groupKeysDown >> 8
self.space = groupKeysDown & 0x3
if group == BAUM_ROUTING_KEYS:
for index in range(braille.handler.display.numCells):
if groupKeysDown & (1 << index):
self.routingIndex = index
names.append("routing")
break
elif group == BAUM_ROUTING_KEY:
self.routingIndex = groupKeysDown - 1
names.append("routing")
if group in (BAUM_ROUTING_KEYS, BAUM_ROUTING_KEY):
self.cellIndexes = [
index for index in range(braille.handler.display.numCells) if groupKeysDown & (1 << index)
]
if self.cellIndexes:
names.append(self.idForCellCount(len(self.cellIndexes)))
else:
for index, name in enumerate(KEY_NAMES[group]):
if groupKeysDown & (1 << index):
Expand Down
2 changes: 1 addition & 1 deletion source/brailleDisplayDrivers/brailleNote.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ def __init__(
names.add(_keyNames[0])
names.update(_dotNames[1 << i] for i in range(8) if (1 << i) & dots)
elif routing is not None:
self.routingIndex = routing
self.cellIndexes = [routing]
names.add("routing")
elif qtMod is not None:
names.update(_qtKeyNames[1 << i] for i in range(4) if (1 << i) & qtMod)
Expand Down
15 changes: 10 additions & 5 deletions source/brailleDisplayDrivers/brailliantB.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 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) 2012-2023 NV Access Limited, Babbage B.V.
# Copyright (C) 2012-2026 NV Access Limited, Babbage B.V., Leonard de Ruijter
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

import time
from typing import List, Union
Expand Down Expand Up @@ -351,6 +351,7 @@ def display(self, cells: List[int]):
"braille_previousLine": ("br(brailliantB):up",),
"braille_nextLine": ("br(brailliantB):down",),
"braille_routeTo": ("br(brailliantB):routing",),
"braille_selectRange": ("br(brailliantB):multiRouting",),
"braille_toggleTether": ("br(brailliantB):up+down",),
"kb:upArrow": ("br(brailliantB):space+dot1", "br(brailliantB):stickUp"),
"kb:downArrow": ("br(brailliantB):space+dot4", "br(brailliantB):stickDown"),
Expand Down Expand Up @@ -389,6 +390,7 @@ def __init__(self, keys):

self.keyNames = names = []
isBrailleInput = True
routingIndexes: list[int] = []
for key in self.keyCodes:
if isBrailleInput:
if DOT1_KEY <= key <= DOT8_KEY:
Expand All @@ -401,12 +403,15 @@ def __init__(self, keys):
self.dots = 0
self.space = False
if key >= FIRST_ROUTING_KEY:
names.append("routing")
self.routingIndex = key - FIRST_ROUTING_KEY
routingIndexes.append(key - FIRST_ROUTING_KEY)
else:
try:
names.append(KEY_NAMES[key])
except KeyError:
log.debugWarning("Unknown key with id %d" % key)
if routingIndexes:
routingIndexes.sort()
self.cellIndexes = routingIndexes
names.append(self.idForCellCount(len(routingIndexes)))

self.id = "+".join(names)
2 changes: 1 addition & 1 deletion source/brailleDisplayDrivers/brltty.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,4 @@ def __init__(self, model, command, argument):
self.model = model
self.id = BRLAPI_CMD_KEYS[command]
if command == brlapi.KEY_CMD_ROUTE:
self.routingIndex = argument
self.cellIndexes = [argument]
2 changes: 1 addition & 1 deletion source/brailleDisplayDrivers/ecoBraille.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,4 +513,4 @@ class InputGestureRouting(braille.BrailleDisplayGesture):
def __init__(self, index):
super(InputGestureRouting, self).__init__()
self.id = "routing"
self.routingIndex = index - 1
self.cellIndexes = [index - 1]
2 changes: 1 addition & 1 deletion source/brailleDisplayDrivers/eurobraille/gestures.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def __init__(self, display: "BrailleDisplayDriver"):
if groupKeysDown & 0x100:
names.append("backSpace")
if group == constants.EB_KEY_INTERACTIVE: # Routing
self.routingIndex = (groupKeysDown & 0xFF) - 1
self.cellIndexes = [(groupKeysDown & 0xFF) - 1]
if groupKeysDown >> 8 == ord(constants.EB_KEY_INTERACTIVE_DOUBLE_CLICK):
names.append("doubleRouting")
else:
Expand Down
Loading
Loading