Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
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
5 changes: 5 additions & 0 deletions source/_remoteClient/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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
6 changes: 4 additions & 2 deletions source/_remoteClient/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,8 +662,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))
self.localMachine._dismissLocalBrailleMessage()
self.transport.send(type=RemoteMessageType.BRAILLE_INPUT, **dict)
return False
Expand Down
60 changes: 56 additions & 4 deletions source/braille.py
Original file line number Diff line number Diff line change
Expand Up @@ -3834,7 +3834,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,
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 @@ -3868,9 +3869,60 @@ 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.
"""

@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

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

def _get_identifiers(self):
ids = ["br({source}):{id}".format(source=self.source, id=self.id)]
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 @@ -45,6 +45,7 @@
),
"braille_routeTo": ("br(albatross):routing",),
"braille_reportFormatting": ("br(albatross):secondRouting",),
"braille_selectRange": ("br(albatross):multiRouting",),
"braille_toggleFocusContextPresentation": ("br(albatross):attribute1+attribute3",),
"speechMode": ("br(albatross):attribute2+attribute4",),
"reviewMode_previous": ("br(albatross):f1",),
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
25 changes: 18 additions & 7 deletions source/brailleDisplayDrivers/alva.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,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 +523,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 +557,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
13 changes: 7 additions & 6 deletions source/brailleDisplayDrivers/baum.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,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 @@ -407,13 +408,13 @@ def __init__(self, model, keysDown):
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
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)))
elif group == BAUM_ROUTING_KEY:
self.routingIndex = groupKeysDown - 1
self.cellIndexes = [groupKeysDown - 1]
names.append("routing")
else:
for index, name in enumerate(KEY_NAMES[group]):
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
9 changes: 7 additions & 2 deletions source/brailleDisplayDrivers/brailliantB.py
Original file line number Diff line number Diff line change
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
2 changes: 1 addition & 1 deletion source/brailleDisplayDrivers/freedomScientific.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ def __init__(self, model: str, routingIndex: int, topRow: bool = False):
else:
# pylint: disable=invalid-name
self.id = "routing"
self.routingIndex = routingIndex
self.cellIndexes = [routingIndex]
super(RoutingGesture, self).__init__(model)


Expand Down
9 changes: 7 additions & 2 deletions source/brailleDisplayDrivers/handyTech.py
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,7 @@ def script_toggleBrailleInput(self, _gesture):
{
"globalCommands.GlobalCommands": {
"braille_routeTo": ("br(handyTech):routing",),
"braille_selectRange": ("br(handyTech):multiRouting",),
"braille_scrollBack": (
"br(handytech):leftSpace",
"br(handytech):leftTakTop",
Expand Down Expand Up @@ -1209,6 +1210,7 @@ def __init__(self, model, keys, isBrailleInput=False):
self.keyNames = names = []
if isBrailleInput:
self.dots = self._calculateDots()
routingIndexes: list[int] = []
for key in keys:
if isBrailleInput and (
key in KEY_SPACES or (key in (KEY_LEFT, KEY_RIGHT) and isinstance(model, EasyBraille))
Expand All @@ -1218,13 +1220,16 @@ def __init__(self, model, keys, isBrailleInput=False):
elif isBrailleInput and key in KEY_DOTS:
names.append("dot%d" % KEY_DOTS[key])
elif KEY_ROUTING <= key < KEY_ROUTING + model.numCells:
self.routingIndex = key - KEY_ROUTING
names.append("routing")
routingIndexes.append(key - KEY_ROUTING)
else:
try:
names.append(model.keys[key])
except KeyError:
log.debugWarning("Unknown key %d" % key)
if routingIndexes:
routingIndexes.sort()
self.cellIndexes = routingIndexes
names.append(self.idForCellCount(len(routingIndexes)))

self.id = "+".join(names)

Expand Down
2 changes: 1 addition & 1 deletion source/brailleDisplayDrivers/hedoMobilLine.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,4 @@ def __init__(self, index):
super(InputGestureRouting, self).__init__()

self.id = "routing"
self.routingIndex = index
self.cellIndexes = [index]
2 changes: 1 addition & 1 deletion source/brailleDisplayDrivers/hedoProfiLine.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,4 @@ def __init__(self, index):
super(InputGestureRouting, self).__init__()

self.id = "routing"
self.routingIndex = index
self.cellIndexes = [index]
19 changes: 14 additions & 5 deletions source/brailleDisplayDrivers/hidBrailleStandard.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ def display(self, cells: List[int]):
"br(hidBrailleStandard):rockerDown",
),
"braille_routeTo": ("br(hidBrailleStandard):routerSet1_routerKey",),
"braille_selectRange": ("br(hidBrailleStandard):routerSet1_multiRouterKey",),
"braille_toggleTether": ("br(hidBrailleStandard):up+down",),
"kb:upArrow": (
"br(hidBrailleStandard):joystickUp",
Expand Down Expand Up @@ -318,8 +319,9 @@ def __init__(self, driver, dataIndices):
self.keyCodes = set(dataIndices)

self.keyNames = names = []
namePrefix = None
isBrailleInput = True
routingIndexes: list[int] = []
routingNamePrefix: str | None = None
for index in dataIndices:
buttonCapsInfo = driver._inputButtonCapsByDataIndex.get(index)
Comment thread
LeonarddeR marked this conversation as resolved.
buttonCaps = buttonCapsInfo.buttonCaps
Expand Down Expand Up @@ -354,14 +356,21 @@ def __init__(self, driver, dataIndices):
# We must assume that any input in the router set is a routing key,
# Because some devices expose the routing keys as 1-bit values
# which Windows then combines into a usage range.
self.routingIndex = buttonCapsInfo.relativeIndexInCollection
routingIndexes.append(buttonCapsInfo.relativeIndexInCollection)
usageID = BraillePageUsageID.ROUTER_KEY
# Prefix the gesture name with the specific routing collection name (E.g. routerSet1)
namePrefix = self._usageIDToGestureName(linkUsagePage, linkUsageID)
routingNamePrefix = self._usageIDToGestureName(linkUsagePage, linkUsageID)
continue
name = self._usageIDToGestureName(usagePage, usageID)
if namePrefix:
name = "_".join([namePrefix, name])
names.append(name)
if routingIndexes:
routingIndexes.sort()
self.cellIndexes = routingIndexes
routingIdName = self._usageIDToGestureName(HID_USAGE_PAGE_BRAILLE, BraillePageUsageID.ROUTER_KEY)
routingIdName = self.idForCellCount(len(routingIndexes), routingIdName)
if routingNamePrefix:
routingIdName = f"{routingNamePrefix}_{routingIdName}"
names.append(routingIdName)
self.id = "+".join(names)

def _usageIDToGestureName(self, usagePage: int, usageID: int):
Expand Down
2 changes: 1 addition & 1 deletion source/brailleDisplayDrivers/hims.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,5 +837,5 @@ class RoutingInputGesture(braille.BrailleDisplayGesture):

def __init__(self, routingINdex):
super(RoutingInputGesture, self).__init__()
self.routingIndex = routingINdex
self.cellIndexes = [routingINdex]
self.id = "routing"
2 changes: 1 addition & 1 deletion source/brailleDisplayDrivers/lilli.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,4 @@ def __init__(self, command: str, argument: int):
super(InputGesture, self).__init__()
self.id = command
if command == ROUTE_COMMAND:
self.routingIndex = argument
self.cellIndexes = [argument]
2 changes: 1 addition & 1 deletion source/brailleDisplayDrivers/nattiqbraille.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,5 +155,5 @@ class RoutingInputGesture(braille.BrailleDisplayGesture):

def __init__(self, routingIndex):
super(RoutingInputGesture, self).__init__()
self.routingIndex = routingIndex
self.cellIndexes = [routingIndex]
self.id = "routing"
Loading
Loading