* feat(shortcuts): configurable modifier+key hotkeys (e.g. Alt+R) Generalize the shortcut system beyond single-modifier double-tap/hold to support modifier+key combos (alt+r, ctrl+alt+r, super+space) in both toggle (press once) and push-to-talk (hold) modes. - base.py: ShortcutSpec + parse_shortcut_spec/is_valid_shortcut/format label; parse_shortcut kept for back-compat (bare modifiers still rejected). - evdev + pynput backends: combo detection with global (cross-device) modifier tracking so split keyboards work; legacy gestures unchanged. - keyboard_shortcuts: validate via is_valid_shortcut instead of a fixed allowlist. - settings_dialog: additive Custom Shortcut row (typed entry + live Record capture). - tests: 34 new tests (parser, back-compat, evdev + pynput combo, split keyboard). * fix(shortcuts): apply saved shortcut (not stale preset) on mode change When a custom shortcut (e.g. alt+r) is active, the preset combo still points at a default/previous preset. _on_shortcut_mode_changed read the combo, so toggling Toggle<->Push-to-Talk restarted the live listener on the wrong shortcut while config still said alt+r. Read the saved shortcut from config instead. Addresses Codex P2 review on PR #493. * style: apply black formatting to configurable-hotkeys files * test: cover _gdk_keyname_to_token shortcut-capture mapping * fix(shortcuts): reset combo state after dropped/lost evdev events SYN_DROPPED and device-disconnect recovery cleared key_pressed_devices but not the combo state. A dropped modifier *release* left _combo_pressed thinking the modifier was held, so the main key alone could falsely toggle/start dictation, and a push-to-talk hold could stay stuck. Add _reset_combo_state() (clears the pressed set and ends any live hold) and call it from both recovery paths. Addresses maintainer review on PR #493. * fix(settings): combo-aware toggle label (single press, not double-tap) _update_shortcut_ui_for_mode always said 'Double-tap the key' for toggle mode, but a modifier+key combo (e.g. alt+r) toggles on a single press. Read the saved shortcut and use get_shortcut_display_name so a combo shows 'Press Alt+R'. Addresses maintainer review on PR #493. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
58 lines
2.3 KiB
Python
58 lines
2.3 KiB
Python
"""Regression test: switching shortcut mode must use the saved shortcut.
|
|
|
|
A custom shortcut (e.g. "alt+r") is stored in config but leaves the preset
|
|
combo pointing at a default/previous preset. When the user toggles between
|
|
Toggle and Push-to-Talk, the live listener must be restarted on the *saved*
|
|
shortcut, not the stale preset shown in the combo. (Codex P2 on PR #493.)
|
|
|
|
The settings dialog can't be instantiated under the mocked-GTK test harness, so
|
|
(like test_settings_shortcuts.py) this asserts against the source of the
|
|
_on_shortcut_mode_changed method.
|
|
"""
|
|
|
|
import inspect
|
|
import re
|
|
|
|
from vocalinux.ui import settings_dialog
|
|
|
|
|
|
def _method_source(name: str) -> str:
|
|
src = inspect.getsource(settings_dialog)
|
|
# Grab from `def <name>(` up to the next method definition.
|
|
match = re.search(rf"\n def {name}\(.*?(?=\n def )", src, re.DOTALL)
|
|
assert match, f"could not locate method {name}"
|
|
return match.group(0)
|
|
|
|
|
|
def test_mode_change_reads_saved_shortcut_from_config():
|
|
body = _method_source("_on_shortcut_mode_changed")
|
|
assert "self.config_manager.get_str(" in body
|
|
assert '"toggle_recognition"' in body
|
|
|
|
|
|
def test_mode_change_does_not_use_stale_preset_combo():
|
|
body = _method_source("_on_shortcut_mode_changed")
|
|
# The stale preset combo must not drive the live mode-change apply.
|
|
assert "self.shortcut_combo.get_active_id()" not in body
|
|
|
|
|
|
def test_mode_ui_toggle_is_combo_aware():
|
|
# A combo (alt+r) toggles on a single press, so the toggle label must branch
|
|
# on whether the shortcut is a combo rather than always saying "Double-tap".
|
|
body = _method_source("_update_shortcut_ui_for_mode")
|
|
assert "is_combo" in body
|
|
assert "get_shortcut_display_name(" in body
|
|
assert "self.config_manager.get_str(" in body
|
|
|
|
|
|
def test_mode_ui_double_tap_text_is_guarded_for_non_combo():
|
|
body = _method_source("_update_shortcut_ui_for_mode")
|
|
# The "Double-tap" wording must sit inside the non-combo branch, not fire
|
|
# unconditionally for every toggle shortcut.
|
|
idx_if = body.find("if is_combo:")
|
|
# The user-facing "Double-tap this key" subtitle (not the example in a
|
|
# comment) must sit inside the non-combo branch.
|
|
idx_double_tap = body.find("Double-tap this key")
|
|
assert idx_if != -1
|
|
assert idx_double_tap > idx_if
|