Files
voculinux/tests/test_settings_shortcuts.py
T
Jatin K MalikandGitHub 86ca22ff5c feat(shortcuts): add push-to-talk mode and improve shortcut reliability (#278)
* feat(shortcuts): add push-to-talk mode as alternative to toggle

This commit implements push-to-talk functionality (Issue #252) as an
alternative to the existing double-tap toggle mode:

- Add keyboard shortcut mode setting (toggle vs push_to_talk)
- In push-to-talk mode: hold key to speak, release to stop
- Update both pynput and evdev backends to support press/release callbacks
- Add UI controls in settings dialog for mode selection
- Maintain backward compatibility with existing toggle mode as default

Closes #252
Related: Closes #185 (superseded by push-to-talk approach)

* fix(shortcuts): remove duplicate code blocks that broke settings dialog

The previous push-to-talk implementation had duplicate code blocks
in tray_indicator.py and settings_dialog.py that caused the settings
dialog to fail silently. This commit cleans up the duplicate code
while preserving the push-to-talk functionality.

Fixes issue where settings dialog would not open.

* fix(ui): add set_subtitle method to PreferenceRow class

The PreferenceRow class was missing a set_subtitle method, which is called by
_update_shortcut_ui_for_mode when switching between toggle and push-to-talk
modes. This caused an AttributeError that silently crashed the settings dialog
during initialization.

Also stores subtitle_label as instance variable for later updates.

* fix(keyboard): remove duplicate code and fix mode handling

Critical fixes for keyboard backends:
- evdev_backend.py: Removed duplicate __init__ and _handle_key_event methods
- pynput_backend.py: Removed duplicate __init__, _on_press, _on_release methods
- Both backends now properly handle toggle and push-to-talk modes

Fixed first_run_dialog.py:
- Added do_response() override to properly close dialog
- Added explicit destroy() call in show_first_run_dialog()

Fixed tray_indicator.py:
- Added _unregister_keyboard_callbacks() method
- Updated _setup_keyboard_shortcuts() to properly clean up old callbacks
- Updated update_shortcut() to properly handle mode switching

Fixed settings_dialog.py:
- Added set_subtitle() method to PreferenceRow class
- Updated UI text for push-to-talk mode
- Fixed _on_shortcut_changed to properly pass mode to callback

All 547 tests pass locally.

* feat(shortcuts): improve mode switching and add mode-aware display names

Key improvements:
- Add SHORTCUT_MODE_DISPLAY_NAMES for mode-specific shortcut names
- Add get_shortcut_display_name() function for mode-aware display
- Fix restart_with_shortcut() to properly save and restore all callbacks
- Fix update_shortcut() to properly handle mode changes
- Fix _setup_keyboard_shortcuts() to clear old callbacks before registering new ones
- Ensure only one type of callback is active at a time

This fixes issues where:
- Old callbacks remained registered after mode switch
- Single-tap triggered action after switching from push-to-talk to toggle
- Display names weren't appropriate for the selected mode

* fix(shortcuts): stabilize live mode switching callback lifecycle

* test(shortcuts): cover callback reset on mode transitions

* fix(shortcuts): remove super preset and migrate legacy config

* test(shortcuts): update coverage for super preset removal
2026-02-28 20:40:32 -08:00

180 lines
7.2 KiB
Python

"""
Tests for keyboard shortcuts section in settings dialog.
Uses source code inspection to avoid GTK mocking issues.
"""
import os
import unittest
def _get_source_code():
"""Read the settings dialog source file."""
source_path = os.path.join(
os.path.dirname(__file__),
"..",
"src",
"vocalinux",
"ui",
"settings_dialog.py",
)
with open(source_path, "r") as f:
return f.read()
class TestSettingsDialogShortcutsSection(unittest.TestCase):
"""Test cases for keyboard shortcuts section in settings dialog."""
def setUp(self):
"""Set up test fixtures."""
self.source_code = _get_source_code()
def test_shortcuts_section_exists(self):
"""Test that shortcuts section build method exists."""
self.assertIn("def _build_shortcuts_section(self)", self.source_code)
def test_shortcuts_section_called_in_init(self):
"""Test that shortcuts section is built in __init__."""
self.assertIn("self._build_shortcuts_section()", self.source_code)
def test_shortcut_combo_widget_created(self):
"""Test that shortcut combo box widget is created."""
self.assertIn("self.shortcut_combo = Gtk.ComboBoxText()", self.source_code)
def test_shortcut_combo_size_request(self):
"""Test that shortcut combo has size request set."""
self.assertIn("self.shortcut_combo.set_size_request(200, -1)", self.source_code)
def test_shortcut_combo_tooltip(self):
"""Test that shortcut combo has a tooltip."""
self.assertIn("self.shortcut_combo.set_tooltip_text(", self.source_code)
def test_shortcut_options_populated(self):
"""Test that shortcut options are populated from SHORTCUT_DISPLAY_NAMES."""
self.assertIn(
"for shortcut_id, display_name in SHORTCUT_DISPLAY_NAMES.items()", self.source_code
)
self.assertIn("self.shortcut_combo.append(shortcut_id, display_name)", self.source_code)
def test_shortcut_config_read(self):
"""Test that shortcut is read from config."""
self.assertIn('self.config_manager.get("shortcuts", "toggle_recognition"', self.source_code)
def test_shortcut_changed_handler_exists(self):
"""Test that shortcut changed handler exists."""
self.assertIn("def _on_shortcut_changed(self, widget)", self.source_code)
def test_shortcut_change_saved_to_config(self):
"""Test that shortcut change is saved to config."""
self.assertIn(
'self.config_manager.set("shortcuts", "toggle_recognition", shortcut_id)',
self.source_code,
)
def test_shortcut_change_triggers_save(self):
"""Test that shortcut change triggers config save."""
# After setting the shortcut, save_settings should be called
self.assertIn("self.config_manager.save_settings()", self.source_code)
def test_shortcut_preference_row_title(self):
"""Test that preference row has correct title."""
self.assertIn('title="Shortcut Key"', self.source_code)
def test_shortcut_preference_row_subtitle(self):
"""Test that preference row has descriptive subtitle."""
# The subtitle is now dynamic based on mode (toggle or push-to-talk)
self.assertIn("set_subtitle", self.source_code)
"""Test that preference row has correct title."""
self.assertIn('title="Shortcut Key"', self.source_code)
"""Test that preference row has correct title."""
self.assertIn('title="Toggle Recognition"', self.source_code)
def test_shortcut_preference_row_subtitle(self):
"""Test that preference row has descriptive subtitle."""
# The subtitle is now dynamic based on mode (toggle or push-to-talk)
self.assertIn("set_subtitle", self.source_code)
def test_shortcut_info_label_exists(self):
"""Test that info label exists for user guidance."""
self.assertIn("self.shortcut_info_label", self.source_code)
def test_shortcut_info_box_styling(self):
"""Test that info box has proper styling."""
self.assertIn('get_style_context().add_class("info-box")', self.source_code)
def test_shortcut_group_title(self):
"""Test that shortcuts group has proper title."""
self.assertIn('title="Keyboard Shortcuts"', self.source_code)
def test_imports_shortcut_constants(self):
"""Test that shortcut constants are imported."""
# Check for imports (may be multi-line)
self.assertIn("from .keyboard_backends import", self.source_code)
self.assertIn("SHORTCUT_DISPLAY_NAMES", self.source_code)
self.assertIn("SUPPORTED_SHORTCUTS", self.source_code)
class TestKeyboardBackendsBase(unittest.TestCase):
"""Test cases for keyboard backends base module."""
def test_supported_shortcuts_defined(self):
"""Test that SUPPORTED_SHORTCUTS is defined."""
from vocalinux.ui.keyboard_backends.base import SUPPORTED_SHORTCUTS
self.assertIsInstance(SUPPORTED_SHORTCUTS, dict)
self.assertIn("ctrl+ctrl", SUPPORTED_SHORTCUTS)
self.assertIn("alt+alt", SUPPORTED_SHORTCUTS)
self.assertIn("shift+shift", SUPPORTED_SHORTCUTS)
def test_shortcut_display_names_defined(self):
"""Test that SHORTCUT_DISPLAY_NAMES is defined."""
from vocalinux.ui.keyboard_backends.base import SHORTCUT_DISPLAY_NAMES
self.assertIsInstance(SHORTCUT_DISPLAY_NAMES, dict)
self.assertEqual(
set(SHORTCUT_DISPLAY_NAMES.keys()),
{"ctrl+ctrl", "alt+alt", "shift+shift"},
)
def test_default_shortcut_defined(self):
"""Test that DEFAULT_SHORTCUT is defined."""
from vocalinux.ui.keyboard_backends.base import DEFAULT_SHORTCUT
self.assertEqual(DEFAULT_SHORTCUT, "ctrl+ctrl")
def test_keyboard_backend_shortcut_property(self):
"""Test that KeyboardBackend has shortcut property."""
from vocalinux.ui.keyboard_backends.base import KeyboardBackend
# Can't instantiate abstract class, but can check the property exists
self.assertTrue(hasattr(KeyboardBackend, "shortcut"))
self.assertTrue(hasattr(KeyboardBackend, "modifier_key"))
self.assertTrue(hasattr(KeyboardBackend, "set_shortcut"))
class TestConfigManagerShortcuts(unittest.TestCase):
"""Test cases for shortcut configuration in config manager."""
def test_shortcuts_section_in_default_config(self):
"""Test that shortcuts section exists in DEFAULT_CONFIG."""
from vocalinux.ui.config_manager import DEFAULT_CONFIG
self.assertIn("shortcuts", DEFAULT_CONFIG)
self.assertIn("toggle_recognition", DEFAULT_CONFIG["shortcuts"])
self.assertEqual(DEFAULT_CONFIG["shortcuts"]["toggle_recognition"], "ctrl+ctrl")
def test_config_manager_get_shortcut(self):
"""Test getting shortcut from config manager."""
from unittest.mock import patch
with patch("vocalinux.ui.config_manager.os.path.exists", return_value=False):
from vocalinux.ui.config_manager import ConfigManager
config = ConfigManager()
shortcut = config.get("shortcuts", "toggle_recognition", "ctrl+ctrl")
self.assertEqual(shortcut, "ctrl+ctrl")
if __name__ == "__main__":
unittest.main()