From 3ab834d00c2f0b659601b0d916a00fce7b6af961 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Wed, 16 Sep 2026 10:49:54 -0400 Subject: [PATCH] Attempt to make buttons activatable with enter. --- distro-packages/ArchLinux/navipy-git/PKGBUILD | 2 +- navipy.py | 2 + src/accessibility/button_keyboard.py | 33 +++++++++ tests/test_button_keyboard.py | 71 +++++++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/accessibility/button_keyboard.py create mode 100644 tests/test_button_keyboard.py diff --git a/distro-packages/ArchLinux/navipy-git/PKGBUILD b/distro-packages/ArchLinux/navipy-git/PKGBUILD index d4ad5e1..77b2139 100644 --- a/distro-packages/ArchLinux/navipy-git/PKGBUILD +++ b/distro-packages/ArchLinux/navipy-git/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Storm Dragon pkgname=navipy-git -pkgver=r15.gf39ec4d +pkgver=r16.geb63cfe pkgrel=1 pkgdesc='Accessible Subsonic desktop client tested with Navidrome' arch=('any') diff --git a/navipy.py b/navipy.py index 3007b23..bbca7bf 100755 --- a/navipy.py +++ b/navipy.py @@ -19,6 +19,7 @@ srcPath = Path(__file__).parent / 'src' sys.path.insert(0, str(srcPath.parent)) from src.main_window import MainWindow +from src.accessibility.button_keyboard import install_button_keyboard_support from src.config.settings import getDataDir @@ -60,6 +61,7 @@ def main(): app.setApplicationName("NaviPy") app.setApplicationVersion("0.1.0") app.setOrganizationName("NaviPy") + app.buttonKeyboardFilter = install_button_keyboard_support(app) # Enable accessibility app.setAttribute(Qt.AA_UseHighDpiPixmaps) diff --git a/src/accessibility/button_keyboard.py b/src/accessibility/button_keyboard.py new file mode 100644 index 0000000..96e9d5d --- /dev/null +++ b/src/accessibility/button_keyboard.py @@ -0,0 +1,33 @@ +"""Keyboard activation support for push buttons.""" + +from PySide6.QtCore import QEvent, QObject, Qt +from PySide6.QtWidgets import QApplication, QPushButton + + +class ButtonKeyboardFilter(QObject): + """Make Enter behave like a native activation key on push buttons.""" + + def eventFilter(self, watched, event): + if not isinstance(watched, QPushButton): + return super().eventFilter(watched, event) + + if event.type() not in (QEvent.KeyPress, QEvent.KeyRelease): + return super().eventFilter(watched, event) + + if event.key() not in (Qt.Key_Return, Qt.Key_Enter): + return super().eventFilter(watched, event) + + if event.modifiers() not in (Qt.NoModifier, Qt.KeypadModifier): + return super().eventFilter(watched, event) + + event.accept() + if event.type() == QEvent.KeyRelease and not event.isAutoRepeat(): + watched.click() + return True + + +def install_button_keyboard_support(app: QApplication) -> ButtonKeyboardFilter: + """Install application-wide Enter-key support for push buttons.""" + keyboardFilter = ButtonKeyboardFilter(app) + app.installEventFilter(keyboardFilter) + return keyboardFilter diff --git a/tests/test_button_keyboard.py b/tests/test_button_keyboard.py new file mode 100644 index 0000000..f4ce5a2 --- /dev/null +++ b/tests/test_button_keyboard.py @@ -0,0 +1,71 @@ +"""Tests for consistent keyboard activation of push buttons.""" + +import pytest + +from PySide6.QtCore import Qt +from PySide6.QtTest import QTest +from PySide6.QtWidgets import QApplication, QCheckBox, QDialogButtonBox, QPushButton + +from src.accessibility.button_keyboard import install_button_keyboard_support + + +@pytest.fixture(scope="module") +def app(): + application = QApplication.instance() or QApplication([]) + keyboardFilter = install_button_keyboard_support(application) + yield application + application.removeEventFilter(keyboardFilter) + + +@pytest.mark.parametrize("key", (Qt.Key_Return, Qt.Key_Enter)) +def test_enter_activates_focused_push_button(app, key): + button = QPushButton("Clear selection") + clicks = [] + button.clicked.connect(lambda: clicks.append(True)) + button.show() + button.setFocus() + + QTest.keyClick(button, key) + + assert clicks == [True] + + +def test_enter_does_not_activate_disabled_push_button(app): + button = QPushButton("Clear play cue") + clicks = [] + button.clicked.connect(lambda: clicks.append(True)) + button.setEnabled(False) + + QTest.keyClick(button, Qt.Key_Return) + + assert clicks == [] + + +def test_enter_activates_dialog_button_box_buttons(app): + buttonBox = QDialogButtonBox(QDialogButtonBox.Close) + closeButton = buttonBox.button(QDialogButtonBox.Close) + clicks = [] + closeButton.clicked.connect(lambda: clicks.append(True)) + + QTest.keyClick(closeButton, Qt.Key_Return) + + assert clicks == [True] + + +def test_modified_enter_is_not_treated_as_button_activation(app): + button = QPushButton("Clear selection") + clicks = [] + button.clicked.connect(lambda: clicks.append(True)) + + QTest.keyClick(button, Qt.Key_Return, Qt.ControlModifier) + + assert clicks == [] + + +def test_enter_handling_is_limited_to_push_buttons(app): + checkbox = QCheckBox("Select") + checkbox.setChecked(False) + + QTest.keyClick(checkbox, Qt.Key_Return) + + assert not checkbox.isChecked()