diff --git a/source/_remoteClient/input.py b/source/_remoteClient/input.py index 4dcfdb94d79..3cf313494d8 100644 --- a/source/_remoteClient/input.py +++ b/source/_remoteClient/input.py @@ -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()}" diff --git a/source/_remoteClient/session.py b/source/_remoteClient/session.py index d068b30a8c3..44a7a90e4dd 100644 --- a/source/_remoteClient/session.py +++ b/source/_remoteClient/session.py @@ -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 diff --git a/source/braille.py b/source/braille.py index 6e1d36be208..72f7beb99f9 100644 --- a/source/braille.py +++ b/source/braille.py @@ -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. """ @@ -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)] diff --git a/source/brailleDisplayDrivers/albatross/gestures.py b/source/brailleDisplayDrivers/albatross/gestures.py index eefbee383c3..2d67a6fda31 100644 --- a/source/brailleDisplayDrivers/albatross/gestures.py +++ b/source/brailleDisplayDrivers/albatross/gestures.py @@ -99,7 +99,7 @@ def __init__(self, keys: Set[int], name: str): routingTuple = self._getRoutingIndex(key) if routingTuple: names.append(routingTuple[0]) - self.routingIndex = routingTuple[1] + self.cellIndexes = [routingTuple[1]] else: try: names.append(Keys(key).name) diff --git a/source/brailleDisplayDrivers/alva.py b/source/brailleDisplayDrivers/alva.py index 73f8aaf63be..b6e57c407e0 100644 --- a/source/brailleDisplayDrivers/alva.py +++ b/source/brailleDisplayDrivers/alva.py @@ -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",), @@ -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] @@ -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: diff --git a/source/brailleDisplayDrivers/baum.py b/source/brailleDisplayDrivers/baum.py index 0f2d6aa6e47..157cd836a17 100644 --- a/source/brailleDisplayDrivers/baum.py +++ b/source/brailleDisplayDrivers/baum.py @@ -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",), @@ -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]): diff --git a/source/brailleDisplayDrivers/brailleNote.py b/source/brailleDisplayDrivers/brailleNote.py index 3d843cf8e3d..febf4fdb451 100644 --- a/source/brailleDisplayDrivers/brailleNote.py +++ b/source/brailleDisplayDrivers/brailleNote.py @@ -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) diff --git a/source/brailleDisplayDrivers/brailliantB.py b/source/brailleDisplayDrivers/brailliantB.py index e7fdd639888..ce30f127cc9 100644 --- a/source/brailleDisplayDrivers/brailliantB.py +++ b/source/brailleDisplayDrivers/brailliantB.py @@ -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"), @@ -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: @@ -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) diff --git a/source/brailleDisplayDrivers/brltty.py b/source/brailleDisplayDrivers/brltty.py index b015779c164..2d4754a3f1b 100644 --- a/source/brailleDisplayDrivers/brltty.py +++ b/source/brailleDisplayDrivers/brltty.py @@ -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] diff --git a/source/brailleDisplayDrivers/ecoBraille.py b/source/brailleDisplayDrivers/ecoBraille.py index b7c0547df16..b9e1ba813ab 100644 --- a/source/brailleDisplayDrivers/ecoBraille.py +++ b/source/brailleDisplayDrivers/ecoBraille.py @@ -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] diff --git a/source/brailleDisplayDrivers/eurobraille/gestures.py b/source/brailleDisplayDrivers/eurobraille/gestures.py index b02e307cb8b..c716ae50c73 100644 --- a/source/brailleDisplayDrivers/eurobraille/gestures.py +++ b/source/brailleDisplayDrivers/eurobraille/gestures.py @@ -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: diff --git a/source/brailleDisplayDrivers/freedomScientific.py b/source/brailleDisplayDrivers/freedomScientific.py index 5511d700b4d..f3b2ddd219f 100755 --- a/source/brailleDisplayDrivers/freedomScientific.py +++ b/source/brailleDisplayDrivers/freedomScientific.py @@ -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) diff --git a/source/brailleDisplayDrivers/handyTech.py b/source/brailleDisplayDrivers/handyTech.py index ecba0ec6446..c9642abbd6c 100644 --- a/source/brailleDisplayDrivers/handyTech.py +++ b/source/brailleDisplayDrivers/handyTech.py @@ -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", @@ -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)) @@ -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) diff --git a/source/brailleDisplayDrivers/hedoMobilLine.py b/source/brailleDisplayDrivers/hedoMobilLine.py index bfabded9a31..921f273d5a4 100644 --- a/source/brailleDisplayDrivers/hedoMobilLine.py +++ b/source/brailleDisplayDrivers/hedoMobilLine.py @@ -215,4 +215,4 @@ def __init__(self, index): super(InputGestureRouting, self).__init__() self.id = "routing" - self.routingIndex = index + self.cellIndexes = [index] diff --git a/source/brailleDisplayDrivers/hedoProfiLine.py b/source/brailleDisplayDrivers/hedoProfiLine.py index f085f768037..e8d614b1bf5 100644 --- a/source/brailleDisplayDrivers/hedoProfiLine.py +++ b/source/brailleDisplayDrivers/hedoProfiLine.py @@ -211,4 +211,4 @@ def __init__(self, index): super(InputGestureRouting, self).__init__() self.id = "routing" - self.routingIndex = index + self.cellIndexes = [index] diff --git a/source/brailleDisplayDrivers/hidBrailleStandard.py b/source/brailleDisplayDrivers/hidBrailleStandard.py index f74e4e84dc0..2d45c87c14b 100644 --- a/source/brailleDisplayDrivers/hidBrailleStandard.py +++ b/source/brailleDisplayDrivers/hidBrailleStandard.py @@ -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", @@ -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) buttonCaps = buttonCapsInfo.buttonCaps @@ -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): diff --git a/source/brailleDisplayDrivers/hims.py b/source/brailleDisplayDrivers/hims.py index 25bd648bebd..e58271c2612 100644 --- a/source/brailleDisplayDrivers/hims.py +++ b/source/brailleDisplayDrivers/hims.py @@ -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" diff --git a/source/brailleDisplayDrivers/lilli.py b/source/brailleDisplayDrivers/lilli.py index b930e014bd9..c498d4de244 100644 --- a/source/brailleDisplayDrivers/lilli.py +++ b/source/brailleDisplayDrivers/lilli.py @@ -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] diff --git a/source/brailleDisplayDrivers/nattiqbraille.py b/source/brailleDisplayDrivers/nattiqbraille.py index 9088ed5c080..ab3bcf6763c 100644 --- a/source/brailleDisplayDrivers/nattiqbraille.py +++ b/source/brailleDisplayDrivers/nattiqbraille.py @@ -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" diff --git a/source/brailleDisplayDrivers/nlseReaderZoomax.py b/source/brailleDisplayDrivers/nlseReaderZoomax.py index f3ea28dd610..0a86d104c95 100644 --- a/source/brailleDisplayDrivers/nlseReaderZoomax.py +++ b/source/brailleDisplayDrivers/nlseReaderZoomax.py @@ -234,6 +234,7 @@ def display(self, cells: list[int]): "braille_previousLine": ("br(nlseReaderZoomax):d1",), "braille_nextLine": ("br(nlseReaderZoomax):d3",), "braille_routeTo": ("br(nlseReaderZoomax):routing",), + "braille_selectRange": ("br(nlseReaderZoomax):multiRouting",), "kb:upArrow": ("br(nlseReaderZoomax):up",), "kb:downArrow": ("br(nlseReaderZoomax):down",), "kb:leftArrow": ("br(nlseReaderZoomax):left",), @@ -264,11 +265,11 @@ def __init__(self, keysDown: dict[bytes, bytes]): self.dots = groupKeysDown >> 8 self.space = groupKeysDown & SPACEBAR_KEYS_MASK if group == DeviceCommand.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))) else: for index, name in enumerate(COMMAND_RESPONSE_INFO.get(group).keys): if groupKeysDown & (1 << index): diff --git a/source/brailleDisplayDrivers/papenmeier.py b/source/brailleDisplayDrivers/papenmeier.py index 2bd13ac49b2..5bb7ac71a6e 100644 --- a/source/brailleDisplayDrivers/papenmeier.py +++ b/source/brailleDisplayDrivers/papenmeier.py @@ -679,7 +679,7 @@ def __init__(self, keys: Optional[Union[bytes, int]], driver: BrailleDisplayDriv length = len(decodedkeys) if length == 1 and 32 <= decodedkeys[0] < 32 + driver.numCells * 2: # routing keys - self.routingIndex = (decodedkeys[0] - 32) // 2 + self.cellIndexes = [(decodedkeys[0] - 32) // 2] self.id = "route" if decodedkeys[0] % 2 == 1: self.id = "upperRouting" diff --git a/source/brailleDisplayDrivers/papenmeier_serial.py b/source/brailleDisplayDrivers/papenmeier_serial.py index bef1b1ec43c..1b9338d6994 100644 --- a/source/brailleDisplayDrivers/papenmeier_serial.py +++ b/source/brailleDisplayDrivers/papenmeier_serial.py @@ -305,11 +305,12 @@ def __init__( self.id = driver._lastkey return if pressed == 1 and keyindex >= 0: - self.routingIndex = keyindex - driver._offsetHorizontal + cellIndex = keyindex - driver._offsetHorizontal self.id = "route" if keyindex > 255: - self.routingIndex -= 256 + cellIndex -= 256 self.id = "upperRouting" + self.cellIndexes = [cellIndex] elif pressed == 0: k: str = brl_keyname(keyindex, driver) if driver._lastkey != k: diff --git a/source/brailleDisplayDrivers/seika.py b/source/brailleDisplayDrivers/seika.py index a4e5ede2a82..1bd2b549fac 100644 --- a/source/brailleDisplayDrivers/seika.py +++ b/source/brailleDisplayDrivers/seika.py @@ -221,4 +221,4 @@ def __init__(self, index): super(InputGestureRouting, self).__init__() self.id = "routing" - self.routingIndex = index + self.cellIndexes = [index] diff --git a/source/brailleDisplayDrivers/seikantk.py b/source/brailleDisplayDrivers/seikantk.py index 92a454163b6..149ffa820e1 100644 --- a/source/brailleDisplayDrivers/seikantk.py +++ b/source/brailleDisplayDrivers/seikantk.py @@ -295,12 +295,13 @@ def _handleInfo(self, arg: bytes): def _handleRouting(self, arg: bytes): routingIndexes = _getRoutingIndexes(arg) - for routingIndex in routingIndexes: - gesture = InputGestureRouting(routingIndex) - try: - inputCore.manager.executeGesture(gesture) - except inputCore.NoInputGestureAction: - log.debug("No action for Seika Notetaker routing command") + if not routingIndexes: + return + gesture = InputGestureRouting(sorted(routingIndexes)) + try: + inputCore.manager.executeGesture(gesture) + except inputCore.NoInputGestureAction: + log.debug("No action for Seika Notetaker routing command") def _handleKeys(self, arg: bytes): brailleDots = arg[0] @@ -331,6 +332,7 @@ def _handleKeysRouting(self, arg: bytes): { "globalCommands.GlobalCommands": { "braille_routeTo": ("br(seikantk):routing",), + "braille_selectRange": ("br(seikantk):multiRouting",), "braille_scrollBack": ("br(seikantk):LB",), "braille_scrollForward": ("br(seikantk):RB",), "braille_previousLine": ("br(seikantk):LJ_UP",), @@ -366,10 +368,13 @@ def _handleKeysRouting(self, arg: bytes): class InputGestureRouting(braille.BrailleDisplayGesture): source = BrailleDisplayDriver.name - def __init__(self, index): + def __init__(self, indexes: list[int] | int): super().__init__() - self.id = "routing" - self.routingIndex = index + if isinstance(indexes, int): + # Backwards compat: callers historically passed a single index. + indexes = [indexes] + self.cellIndexes = indexes + self.id = self.idForCellCount(len(self.cellIndexes)) def _getKeyNames(keys: int, names: Dict[int, str]) -> Set[str]: @@ -404,6 +409,6 @@ def __init__(self, keys=None, dots=None, space=0, routing=None): names.update(_getKeyNames(space, _keyNames)) names.update(_getKeyNames(dots, _dotNames)) elif routing is not None: - self.routingIndex = routing + self.cellIndexes = [routing] names.add("routing") self.id = "+".join(names) diff --git a/source/brailleViewer/brailleViewerInputGesture.py b/source/brailleViewer/brailleViewerInputGesture.py index 84a41517571..3c04e6414e8 100644 --- a/source/brailleViewer/brailleViewerInputGesture.py +++ b/source/brailleViewer/brailleViewerInputGesture.py @@ -13,7 +13,7 @@ class BrailleViewerGesture_RouteTo(BrailleDisplayGesture): def __init__(self, argument): super().__init__() - self.routingIndex = argument + self.cellIndexes = [argument] import globalCommands self.script = globalCommands.commands.script_braille_routeTo diff --git a/source/globalCommands.py b/source/globalCommands.py index 57490c715cb..b94de881d9e 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -4222,7 +4222,9 @@ def script_braille_scrollForward(self, gesture): category=SCRCAT_BRAILLE, ) def script_braille_routeTo(self, gesture): - braille.handler.routeTo(gesture.routingIndex) + if not gesture.cellIndexes: + return + braille.handler.routeTo(gesture.cellIndexes[0]) @script( # Translators: Input help mode message for Braille report formatting command. @@ -4230,13 +4232,38 @@ def script_braille_routeTo(self, gesture): category=SCRCAT_BRAILLE, ) def script_braille_reportFormatting(self, gesture): - info = braille.handler.getTextInfoForWindowPos(gesture.routingIndex) + if not gesture.cellIndexes: + return + info = braille.handler.getTextInfoForWindowPos(gesture.cellIndexes[0]) if info is None: # Translators: Reported when trying to obtain formatting information (such as font name, indentation and so on) but there is no formatting information for the text under cursor. ui.message(_("No formatting information")) return self._reportFormattingHelper(info, False) + @script( + # Translators: Input help mode message for a braille command. + description=_("Selects the text from the first up to the last braille cell"), + category=SCRCAT_BRAILLE, + ) + def script_braille_selectRange(self, gesture): + if not gesture.cellIndexes or len(gesture.cellIndexes) < 2: + return + startPos = min(gesture.cellIndexes) + endPos = max(gesture.cellIndexes) + startInfo = braille.handler.getTextInfoForWindowPos(startPos) + endInfo = braille.handler.getTextInfoForWindowPos(endPos) + if startInfo is None or endInfo is None: + # Translators: Reported when selection via multiple routing keys is not possible. + ui.message(_("Cannot select from braille routing keys")) + return + startInfo.setEndPoint(endInfo, "endToEnd") + try: + startInfo.updateSelection() + except NotImplementedError: + # Translators: Reported when selection via multiple routing keys is not supported by the focused control. + ui.message(_("Selection not supported here")) + @script( # Translators: Input help mode message for a braille command. description=_("Moves the braille display to the previous line"), diff --git a/tests/unit/test_braille/test_brailleDisplayDrivers.py b/tests/unit/test_braille/test_brailleDisplayDrivers.py index 2e2a9f883c3..e3a35f3f3b5 100644 --- a/tests/unit/test_braille/test_brailleDisplayDrivers.py +++ b/tests/unit/test_braille/test_brailleDisplayDrivers.py @@ -178,6 +178,68 @@ def test_identifiers(self): self.assertRegex(gesture, braille.BrailleDisplayGesture.ID_PARTS_REGEX) +class TestBrailleDisplayGestureCellIndexes(unittest.TestCase): + """Tests for :attr:`braille.BrailleDisplayGesture.cellIndexes` and the deprecated ``routingIndex`` shim.""" + + def _makeGesture(self): + class _Gesture(braille.BrailleDisplayGesture): + source = "test" + id = "routing" + + return _Gesture() + + def test_default_cellIndexes_none(self): + g = self._makeGesture() + self.assertIsNone(g.cellIndexes) + + def test_idForCellCount(self): + self.assertEqual("routing", braille.BrailleDisplayGesture.idForCellCount(0)) + self.assertEqual("routing", braille.BrailleDisplayGesture.idForCellCount(1)) + self.assertEqual("multiRouting", braille.BrailleDisplayGesture.idForCellCount(2)) + self.assertEqual("multiRouting", braille.BrailleDisplayGesture.idForCellCount(5)) + + def test_idForCellCount_custom_baseName(self): + self.assertEqual("secondRouting", braille.BrailleDisplayGesture.idForCellCount(1, "secondRouting")) + self.assertEqual( + "multiSecondRouting", + braille.BrailleDisplayGesture.idForCellCount(2, "secondRouting"), + ) + self.assertEqual("route", braille.BrailleDisplayGesture.idForCellCount(1, "route")) + self.assertEqual("multiRoute", braille.BrailleDisplayGesture.idForCellCount(2, "route")) + self.assertEqual("multiUpperRouting", braille.BrailleDisplayGesture.idForCellCount(3, "upperRouting")) + + def test_routingIndex_getter_returns_highest_cell(self): + g = self._makeGesture() + g.cellIndexes = [3, 7] + self.assertEqual(7, g.routingIndex) + + def test_routingIndex_getter_none_when_empty(self): + g = self._makeGesture() + self.assertIsNone(g.routingIndex) + + def test_routingIndex_setter_wraps_into_cellIndexes(self): + g = self._makeGesture() + g.routingIndex = 5 + self.assertEqual([5], g.cellIndexes) + + def test_routingIndex_setter_none_clears_cellIndexes(self): + g = self._makeGesture() + g.cellIndexes = [1, 2] + g.routingIndex = None + self.assertIsNone(g.cellIndexes) + + def test_multiRouting_identifier_matches_regex(self): + class _Gesture(braille.BrailleDisplayGesture): + source = "test" + id = "multiRouting" + + g = _Gesture() + g.cellIndexes = [0, 3, 7] + for identifier in g.identifiers: + if identifier.startswith("br"): + self.assertRegex(identifier, braille.BrailleDisplayGesture.ID_PARTS_REGEX) + + class TestBRLTTY(unittest.TestCase): """Tests the integrity of the bundled brlapi module.""" diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index b9b38b1655d..06d07b5a48e 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -32,6 +32,9 @@ The triple-press keyboard shortcut (`NVDA+ctrl+r`) is not affected, as it is int * DotPad braille displays now support multi-button combination gestures. (#19565, @bramd) * You can now press multiple buttons simultaneously to create custom gestures (e.g., `f1+panLeft`). * A new voice setting "Natural pause after punctuation" was added for OneCore voices, allowing users to turn punctuation pauses on or off. (#11876, @gexgd0419) +* On supported braille displays, pressing multiple routing keys simultaneously can now be bound to a new "multi routing" gesture. (#20001, @LeonarddeR) + * The "select range" command, which selects the text from the first up to the last pressed routing key, is bound to this gesture by default on supporting drivers. + * Drivers with built-in support for multi routing: ALVA, Baum (and compatible), HumanWare Brailliant BI/B series, Handy Tech, NLS eReader Zoomax, Seika Notetaker, and Standard HID Braille displays. ### Changes @@ -107,8 +110,14 @@ Use the individual test commands instead: `runcheckpot.bat`, `rununittests.bat`, * Added a `percentageToValue` function to convert a percentage to the corresponding configuration value. * Added a `clampedIncrementAndUpdateConfig` function to update a configuration value by applying a step, constrained within its valid range. +* `braille.BrailleDisplayGesture` now exposes a `cellIndexes` list attribute, replacing the single-valued `routingIndex`. (#20001, @LeonarddeR) + * Drivers should set `cellIndexes` directly instead of `routingIndex`. + * When a gesture addresses more than one cell, its `id` should be set to `"multiRouting"` (or be built via the new `BrailleDisplayGesture.idForCellCount(n)` helper). + * `cellIndexes` is not limited to routing keys; touch-sensitive cells (e.g. Handy Tech Active Tactile Control) can reuse the same attribute. + #### Deprecations +* `braille.BrailleDisplayGesture.routingIndex` is deprecated. Use `cellIndexes` instead. (#20001, @LeonarddeR) * The `speechDictHandler.ENTRY_TYPE_*` constants are deprecated. Use the `speechDictHandler.types.EntryType` enumeration instead. (#19430, @LeonarddeR) * `speechDictHandler.SpeechDictEntry` and `speechDictHandler.SpeechDict` have been moved to `speechDictHandler.types`. (#19430, @LeonarddeR)