From 79eb15afa66bcb63758205c26c465b9a72de2e3a Mon Sep 17 00:00:00 2001 From: Amir Date: Sat, 4 Jul 2026 19:25:04 +0330 Subject: [PATCH 1/2] Input gestures dialog. Added context menu, shortcut, and new change action --- source/gui/inputGestures.py | 104 ++++++++++++++++++++++++++++++++++++ user_docs/en/changes.md | 2 + 2 files changed, 106 insertions(+) diff --git a/source/gui/inputGestures.py b/source/gui/inputGestures.py index 8227e9712a6..8567c2555e6 100644 --- a/source/gui/inputGestures.py +++ b/source/gui/inputGestures.py @@ -64,6 +64,7 @@ class _GestureVM: displayName: str #: How the gesture should be displayed normalizedGestureIdentifier: str #: As per items in inputCore.AllGesturesScriptInfo.gestures canAdd = False #: adding children is not supported. + canChange = True #: gestures can be changed canRemove = True #: gestures can be removed def __init__(self, normalizedGestureIdentifier: str): @@ -78,6 +79,7 @@ class _PendingGesture: # Translators: The prompt to enter a gesture in the Input Gestures dialog. displayName = _("Enter input gesture:") canAdd = False + canChange = False canRemove = False def __repr__(self): @@ -89,6 +91,7 @@ class _ScriptVM: scriptInfo: inputCore.AllGesturesScriptInfo gestures: List[Union[_GestureVM, _PendingGesture]] canAdd = True #: able to add gestures that trigger this script + canChange = False #: Scripts can not be Changed canRemove = False #: Scripts can not be removed addedGestures: List[_GestureVM] #: These will also be in self.gestures #: These will not be in self.gestures anymore. Key is the normalized Gesture Identifier. @@ -143,6 +146,7 @@ class _CategoryVM: displayName: str #: Translated display name for the category scripts: List[_ScriptVM] canAdd = False #: not able to add Scripts + canChange = False #: categories can not be changed canRemove = False #: categories can not be removed def __init__(self, displayName: str, scripts: _ScriptsModel): @@ -189,6 +193,7 @@ class _PendingEmulatedGestureVM: # Translators: The prompt to enter an emulated gesture in the Input Gestures dialog. displayName = _("Enter gesture to emulate:") canAdd = False + canChange = False canRemove = False def __repr__(self): @@ -199,6 +204,7 @@ class _EmuCategoryVM: displayName = inputCore.SCRCAT_KBEMU #: Translated display name for the gesture emulation category scripts: List[Union[_ScriptVM, _EmulatedGestureVM, _PendingEmulatedGestureVM]] canAdd = True #: Can add new emulated gestures + canChange = False #: categories can not be changed canRemove = False #: categories can not be removed addedKbEmulation: List[_EmulatedGestureVM] #: These will also be in self.scripts #: These will not be in self.scripts anymore. Key is the scriptInfo display name. @@ -605,6 +611,9 @@ def makeSettings(self, settingsSizer): self.gesturesVM = _InputGesturesViewModel() tree = self.tree = _GesturesTree(self, self.gesturesVM) tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.onTreeSelect) + tree.Bind(wx.EVT_CHAR_HOOK, self.onCharHook) + tree.Bind(wx.EVT_CONTEXT_MENU, self.onContextMenu) + tree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.onContextMenu) settingsSizer.Add(tree, proportion=1, flag=wx.EXPAND) settingsSizer.AddSpacer(guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_VERTICAL) @@ -616,6 +625,11 @@ def makeSettings(self, settingsSizer): self.addButton.Bind(wx.EVT_BUTTON, self.onAdd) self.addButton.Disable() + # Translators: The label of a button to change a gesture in the Input Gestures dialog. + self.changeButton = bHelper.addButton(self, label=_("C&hange")) + self.changeButton.Bind(wx.EVT_BUTTON, self.onChange) + self.changeButton.Disable() + # Translators: The label of a button to remove a gesture in the Input Gestures dialog. self.removeButton = bHelper.addButton(self, label=_("&Remove")) self.removeButton.Bind(wx.EVT_BUTTON, self.onRemove) @@ -630,6 +644,48 @@ def makeSettings(self, settingsSizer): settingsSizer.Add(bHelper.sizer, flag=wx.EXPAND) self.tree.Bind(wx.EVT_WINDOW_DESTROY, self._onDestroyTree) + def onCharHook(self, evt): + selectedItems = self.tree.getSelectedItemData() + key = evt.GetKeyCode() + if selectedItems is None: + item = None + else: + # get the leaf of the selection + item = next((item for item in reversed(selectedItems) if item is not None), None) + pendingAdd = self.gesturesVM.isExpectingNewEmuGesture or self.gesturesVM.isExpectingNewGesture + if item and not pendingAdd: + if key == wx.WXK_DELETE and item.canRemove: + self.onRemove(None) + evt.Skip() + + def onContextMenu(self, evt): + selectedItems = self.tree.getSelectedItemData() + if selectedItems is None: + item = None + else: + # get the leaf of the selection + item = next((item for item in reversed(selectedItems) if item is not None), None) + pendingAdd = self.gesturesVM.isExpectingNewEmuGesture or self.gesturesVM.isExpectingNewGesture + menu = wx.Menu() + if item and not pendingAdd: + if item.canAdd: + # Translators: Context menu item label to add a new gesture + addItem = menu.Append(wx.ID_ANY, _("&Add")) + self.Bind(wx.EVT_MENU, self.onAdd, addItem) + if item.canChange: + # Translators: Context menu item label to change a gesture + changeItem = menu.Append(wx.ID_ANY, _("C&hange")) + self.Bind(wx.EVT_MENU, self.onChange, changeItem) + if item.canRemove: + # Translators: Context menu item label to remove a gesture + removeItem = menu.Append(wx.ID_ANY, _("&Remove")) + self.Bind(wx.EVT_MENU, self.onRemove, removeItem) + # Translators: Context menu item label to reset factory to defaults + resetItem = menu.Append(wx.ID_ANY, _("Reset to factory &defaults")) + self.Bind(wx.EVT_MENU, self.onReset, resetItem) + self.PopupMenu(menu) + menu.Destroy() + def postInit(self): self.tree.RefreshItems() self.tree.SetFocus() @@ -664,6 +720,7 @@ def _refreshButtonState(self): item = next((item for item in reversed(selectedItems) if item is not None), None) pendingAdd = self.gesturesVM.isExpectingNewEmuGesture or self.gesturesVM.isExpectingNewGesture self.addButton.Enabled = bool(item and item.canAdd and not pendingAdd) + self.changeButton.Enabled = bool(item and item.canChange and not pendingAdd) self.removeButton.Enabled = bool(item and item.canRemove and not pendingAdd) def onAdd(self, evt): @@ -712,6 +769,53 @@ def addGestureCaptor(gesture: inputCore.InputGesture): else: log.error("unable to do 'add' action for selected item") + def onChange(self, evt): + if inputCore.manager._captureFunc: + # don't change while already in process of add/change. + return + + selectedItems = self.tree.getSelectedItemData() + assert selectedItems is not None + catVM, scriptVM, gestureVM = selectedItems + log.debug(f"selection: {catVM}, {scriptVM}, {gestureVM}") + + if isinstance(scriptVM, _EmulatedGestureVM) and isinstance(catVM, _EmuCategoryVM): + catVM.removeEmulation(scriptVM) + self.gesturesVM.isExpectingNewEmuGesture = catVM + pending = catVM.createPendingEmuGesture() + self.tree.doRefresh(focus=(catVM, pending, None)) + self._refreshButtonState() + + def changeKbEmuGestureCaptor(gesture: inputCore.InputGesture): + if not isinstance(gesture, keyboardHandler.KeyboardInputGesture) or gesture.isModifier: + return False + inputCore.manager._captureFunc = None + wx.CallAfter(self._addCapturedKbEmu, gesture, catVM) + return False + + inputCore.manager._captureFunc = changeKbEmuGestureCaptor + elif gestureVM is not None and isinstance(scriptVM, _ScriptVM): + scriptVM.removeGesture(gestureVM) + self.gesturesVM.isExpectingNewGesture = scriptVM + pendingGesture = scriptVM.createPendingGesture() + self.tree.doRefresh(focus=(catVM, scriptVM, pendingGesture)) + self._refreshButtonState() + + def changeGestureCaptor(gesture: inputCore.InputGesture): + if gesture.isModifier: + return False + if isinstance(catVM, _EmuCategoryVM): + gesName = keyLabels.getKeyCombinationLabel(gesture.normalizedIdentifiers[-1][3:]) + if gesName == scriptVM.scriptInfo.displayName: + return False + inputCore.manager._captureFunc = None + wx.CallAfter(self._addCaptured, catVM, scriptVM, gesture) + return False + + inputCore.manager._captureFunc = changeGestureCaptor + else: + log.error(f"unable to do 'change' action for selected item: {catVM}, {scriptVM}, {gestureVM}") + def _addCaptured(self, catVM: _CategoryVMTypes, scriptVM: _ScriptVMTypes, gesture): gids = gesture.normalizedIdentifiers if len(gids) > 1: diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 48271702006..bed34d16b28 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -16,6 +16,8 @@ * Drivers with built-in support for multi routing: ALVA, Albatross (only when combined with `home1` or `home2`), Baum (and compatible), Freedom Scientific Focus/PAC Mate, HumanWare Brailliant BI/B series, Handy Tech, NLS eReader Zoomax, Seika Notetaker, and Standard HID Braille displays. * The braille "word wrap" option has been replaced with a four-valued "Text wrap" option: Off, Show mark when words are cut, At word boundaries, and At word or syllable boundaries. (#17010, @LeonarddeR) * In modes that show a continuation mark, when a word is cut across rows, the last cell of the row now shows a continuation mark (braille dots 7-8) so it is clear that the word continues on the next row. +* Added context menu and shortcuts support to the Input gestures dialog. (#16816, @amirmahdifard) +* It is now possible to change an existing gesture in the input gestures dialog. (#10983, @amirmahdifard) * The "At word or syllable boundaries" option uses hyphenation dictionaries to split long words at syllable boundaries when they do not fit on the display. * Magnifier: A new unassigned command has been added to move the mouse cursor to the center of the magnified view. (#20127, @CyrilleB79) From 424ee6edfdbaa1e9b4c650d8c74ced9a6a82ce3b Mon Sep 17 00:00:00 2001 From: Amir Date: Thu, 9 Jul 2026 13:45:30 +0330 Subject: [PATCH 2/2] Applyed all requested changes --- source/gui/inputGestures.py | 70 ++----------------------------------- user_docs/en/changes.md | 1 - user_docs/en/userGuide.md | 4 ++- 3 files changed, 5 insertions(+), 70 deletions(-) diff --git a/source/gui/inputGestures.py b/source/gui/inputGestures.py index 8567c2555e6..d211d669444 100644 --- a/source/gui/inputGestures.py +++ b/source/gui/inputGestures.py @@ -64,7 +64,6 @@ class _GestureVM: displayName: str #: How the gesture should be displayed normalizedGestureIdentifier: str #: As per items in inputCore.AllGesturesScriptInfo.gestures canAdd = False #: adding children is not supported. - canChange = True #: gestures can be changed canRemove = True #: gestures can be removed def __init__(self, normalizedGestureIdentifier: str): @@ -79,7 +78,6 @@ class _PendingGesture: # Translators: The prompt to enter a gesture in the Input Gestures dialog. displayName = _("Enter input gesture:") canAdd = False - canChange = False canRemove = False def __repr__(self): @@ -91,7 +89,6 @@ class _ScriptVM: scriptInfo: inputCore.AllGesturesScriptInfo gestures: List[Union[_GestureVM, _PendingGesture]] canAdd = True #: able to add gestures that trigger this script - canChange = False #: Scripts can not be Changed canRemove = False #: Scripts can not be removed addedGestures: List[_GestureVM] #: These will also be in self.gestures #: These will not be in self.gestures anymore. Key is the normalized Gesture Identifier. @@ -146,7 +143,6 @@ class _CategoryVM: displayName: str #: Translated display name for the category scripts: List[_ScriptVM] canAdd = False #: not able to add Scripts - canChange = False #: categories can not be changed canRemove = False #: categories can not be removed def __init__(self, displayName: str, scripts: _ScriptsModel): @@ -193,7 +189,6 @@ class _PendingEmulatedGestureVM: # Translators: The prompt to enter an emulated gesture in the Input Gestures dialog. displayName = _("Enter gesture to emulate:") canAdd = False - canChange = False canRemove = False def __repr__(self): @@ -204,7 +199,6 @@ class _EmuCategoryVM: displayName = inputCore.SCRCAT_KBEMU #: Translated display name for the gesture emulation category scripts: List[Union[_ScriptVM, _EmulatedGestureVM, _PendingEmulatedGestureVM]] canAdd = True #: Can add new emulated gestures - canChange = False #: categories can not be changed canRemove = False #: categories can not be removed addedKbEmulation: List[_EmulatedGestureVM] #: These will also be in self.scripts #: These will not be in self.scripts anymore. Key is the scriptInfo display name. @@ -625,11 +619,6 @@ def makeSettings(self, settingsSizer): self.addButton.Bind(wx.EVT_BUTTON, self.onAdd) self.addButton.Disable() - # Translators: The label of a button to change a gesture in the Input Gestures dialog. - self.changeButton = bHelper.addButton(self, label=_("C&hange")) - self.changeButton.Bind(wx.EVT_BUTTON, self.onChange) - self.changeButton.Disable() - # Translators: The label of a button to remove a gesture in the Input Gestures dialog. self.removeButton = bHelper.addButton(self, label=_("&Remove")) self.removeButton.Bind(wx.EVT_BUTTON, self.onRemove) @@ -644,7 +633,7 @@ def makeSettings(self, settingsSizer): settingsSizer.Add(bHelper.sizer, flag=wx.EXPAND) self.tree.Bind(wx.EVT_WINDOW_DESTROY, self._onDestroyTree) - def onCharHook(self, evt): + def onCharHook(self, evt: wx.KeyEvent): selectedItems = self.tree.getSelectedItemData() key = evt.GetKeyCode() if selectedItems is None: @@ -658,7 +647,7 @@ def onCharHook(self, evt): self.onRemove(None) evt.Skip() - def onContextMenu(self, evt): + def onContextMenu(self, evt: wx.ContextMenuEvent): selectedItems = self.tree.getSelectedItemData() if selectedItems is None: item = None @@ -672,17 +661,10 @@ def onContextMenu(self, evt): # Translators: Context menu item label to add a new gesture addItem = menu.Append(wx.ID_ANY, _("&Add")) self.Bind(wx.EVT_MENU, self.onAdd, addItem) - if item.canChange: - # Translators: Context menu item label to change a gesture - changeItem = menu.Append(wx.ID_ANY, _("C&hange")) - self.Bind(wx.EVT_MENU, self.onChange, changeItem) if item.canRemove: # Translators: Context menu item label to remove a gesture removeItem = menu.Append(wx.ID_ANY, _("&Remove")) self.Bind(wx.EVT_MENU, self.onRemove, removeItem) - # Translators: Context menu item label to reset factory to defaults - resetItem = menu.Append(wx.ID_ANY, _("Reset to factory &defaults")) - self.Bind(wx.EVT_MENU, self.onReset, resetItem) self.PopupMenu(menu) menu.Destroy() @@ -720,7 +702,6 @@ def _refreshButtonState(self): item = next((item for item in reversed(selectedItems) if item is not None), None) pendingAdd = self.gesturesVM.isExpectingNewEmuGesture or self.gesturesVM.isExpectingNewGesture self.addButton.Enabled = bool(item and item.canAdd and not pendingAdd) - self.changeButton.Enabled = bool(item and item.canChange and not pendingAdd) self.removeButton.Enabled = bool(item and item.canRemove and not pendingAdd) def onAdd(self, evt): @@ -769,53 +750,6 @@ def addGestureCaptor(gesture: inputCore.InputGesture): else: log.error("unable to do 'add' action for selected item") - def onChange(self, evt): - if inputCore.manager._captureFunc: - # don't change while already in process of add/change. - return - - selectedItems = self.tree.getSelectedItemData() - assert selectedItems is not None - catVM, scriptVM, gestureVM = selectedItems - log.debug(f"selection: {catVM}, {scriptVM}, {gestureVM}") - - if isinstance(scriptVM, _EmulatedGestureVM) and isinstance(catVM, _EmuCategoryVM): - catVM.removeEmulation(scriptVM) - self.gesturesVM.isExpectingNewEmuGesture = catVM - pending = catVM.createPendingEmuGesture() - self.tree.doRefresh(focus=(catVM, pending, None)) - self._refreshButtonState() - - def changeKbEmuGestureCaptor(gesture: inputCore.InputGesture): - if not isinstance(gesture, keyboardHandler.KeyboardInputGesture) or gesture.isModifier: - return False - inputCore.manager._captureFunc = None - wx.CallAfter(self._addCapturedKbEmu, gesture, catVM) - return False - - inputCore.manager._captureFunc = changeKbEmuGestureCaptor - elif gestureVM is not None and isinstance(scriptVM, _ScriptVM): - scriptVM.removeGesture(gestureVM) - self.gesturesVM.isExpectingNewGesture = scriptVM - pendingGesture = scriptVM.createPendingGesture() - self.tree.doRefresh(focus=(catVM, scriptVM, pendingGesture)) - self._refreshButtonState() - - def changeGestureCaptor(gesture: inputCore.InputGesture): - if gesture.isModifier: - return False - if isinstance(catVM, _EmuCategoryVM): - gesName = keyLabels.getKeyCombinationLabel(gesture.normalizedIdentifiers[-1][3:]) - if gesName == scriptVM.scriptInfo.displayName: - return False - inputCore.manager._captureFunc = None - wx.CallAfter(self._addCaptured, catVM, scriptVM, gesture) - return False - - inputCore.manager._captureFunc = changeGestureCaptor - else: - log.error(f"unable to do 'change' action for selected item: {catVM}, {scriptVM}, {gestureVM}") - def _addCaptured(self, catVM: _CategoryVMTypes, scriptVM: _ScriptVMTypes, gesture): gids = gesture.normalizedIdentifiers if len(gids) > 1: diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index bed34d16b28..91f1406a865 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -17,7 +17,6 @@ * The braille "word wrap" option has been replaced with a four-valued "Text wrap" option: Off, Show mark when words are cut, At word boundaries, and At word or syllable boundaries. (#17010, @LeonarddeR) * In modes that show a continuation mark, when a word is cut across rows, the last cell of the row now shows a continuation mark (braille dots 7-8) so it is clear that the word continues on the next row. * Added context menu and shortcuts support to the Input gestures dialog. (#16816, @amirmahdifard) -* It is now possible to change an existing gesture in the input gestures dialog. (#10983, @amirmahdifard) * The "At word or syllable boundaries" option uses hyphenation dictionaries to split long words at syllable boundaries when they do not fit on the display. * Magnifier: A new unassigned command has been added to move the mouse cursor to the center of the magnified view. (#20127, @CyrilleB79) diff --git a/user_docs/en/userGuide.md b/user_docs/en/userGuide.md index c2355d178b2..a606b6a7cee 100644 --- a/user_docs/en/userGuide.md +++ b/user_docs/en/userGuide.md @@ -4383,7 +4383,9 @@ Often, a gesture can be interpreted in more than one way. For example, if you pressed a key on the keyboard, you may wish it to be specific to the current keyboard layout (e.g. desktop or laptop) or you may wish it to apply for all layouts. In this case, a menu will appear allowing you to select the desired option. -To remove a gesture from a command, select the gesture and press the Remove button. +To remove a gesture from a command, select the gesture and press the Remove button or the delete key. + +You can also use the context menu to add gestures to commands and remove gestures. The Emulated system keyboard keys category contains NVDA commands that emulate keys on the system keyboard. These emulated system keyboard keys can be used to control a system keyboard right from your braille display.