diff --git a/distro-packages/ArchLinux/navipy-git/PKGBUILD b/distro-packages/ArchLinux/navipy-git/PKGBUILD index df5ba9d..21e0a55 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=r11.gd9f4ac4 +pkgver=r12.g33bba72 pkgrel=1 pkgdesc='Accessible Subsonic desktop client tested with Navidrome' arch=('any') diff --git a/src/main_window.py b/src/main_window.py index 909d007..228960a 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -10,6 +10,9 @@ import time from concurrent.futures import ThreadPoolExecutor from PySide6.QtWidgets import ( QHBoxLayout, + QDialog, + QDialogButtonBox, + QHeaderView, QLabel, QMenu, QListWidget, @@ -20,6 +23,8 @@ from PySide6.QtWidgets import ( QSlider, QSplitter, QStatusBar, + QTableWidget, + QTableWidgetItem, QVBoxLayout, QWidget, ) @@ -222,6 +227,10 @@ class MainWindow(QMainWindow): # Help menu helpMenu = menuBar.addMenu("&Help") + self.keyboardShortcutsAction = QAction("&Keyboard Shortcuts", self) + self.keyboardShortcutsAction.triggered.connect(self.showKeyboardShortcuts) + helpMenu.addAction(self.keyboardShortcutsAction) + self.aboutAction = QAction("&About NaviPy", self) self.aboutAction.triggered.connect(self.showAbout) helpMenu.addAction(self.aboutAction) @@ -1830,6 +1839,64 @@ class MainWindow(QMainWindow): parent=self ) + def showKeyboardShortcuts(self): + """Show the application's keyboard shortcuts in an accessible table.""" + shortcuts = ( + ("Ctrl+O", "Connect to server"), + ("F5", "Refresh library"), + ("Shift+F5", "Full library sync"), + ("Ctrl+Q", "Quit"), + ("Space", "Play or pause"), + ("X", "Play or resume"), + ("C", "Toggle pause"), + ("V", "Stop playback"), + ("Ctrl+Left or Z", "Previous track"), + ("Ctrl+Right or B", "Next track"), + ("Alt+S", "Toggle shuffle"), + ("Alt+R", "Change repeat mode"), + ("+", "Favorite selected item"), + ("_", "Unfavorite selected item"), + ("Ctrl+Up or 0", "Increase volume"), + ("Ctrl+Down or 9", "Decrease volume"), + ("Ctrl+F", "Search"), + ("Ctrl+T", "Announce current track in a dialog"), + ("Alt+C", "Announce current track"), + ("Ctrl+P", "Announce playback position"), + ("Alt+I", "Open actions for the current item"), + ("Ctrl+Tab", "Next library tab"), + ("Ctrl+Shift+Tab", "Previous library tab"), + ("Enter", "Open selected collection or play selected song"), + ("Backspace or Alt+Left", "Go back in the library"), + ("Shift+F10 or Menu", "Open actions for selected library item"), + ("Ctrl+C", "Copy selected library row"), + ) + dialog = QDialog(self) + dialog.setWindowTitle("Keyboard Shortcuts") + dialog.setModal(True) + dialog.setMinimumSize(620, 500) + + layout = QVBoxLayout(dialog) + table = QTableWidget(len(shortcuts), 2, dialog) + table.setHorizontalHeaderLabels(["Shortcut", "Action"]) + table.setAccessibleName("Keyboard shortcuts") + table.setAccessibleDescription("A table of NaviPy keyboard shortcuts and their actions.") + table.setEditTriggers(QTableWidget.NoEditTriggers) + table.setSelectionBehavior(QTableWidget.SelectRows) + table.setSelectionMode(QTableWidget.SingleSelection) + table.verticalHeader().setVisible(False) + table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) + table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) + for row, (shortcut, action) in enumerate(shortcuts): + table.setItem(row, 0, QTableWidgetItem(shortcut)) + table.setItem(row, 1, QTableWidgetItem(action)) + layout.addWidget(table) + + buttonBox = QDialogButtonBox(QDialogButtonBox.Close, dialog) + buttonBox.rejected.connect(dialog.reject) + layout.addWidget(buttonBox) + table.setFocus() + dialog.exec() + def closeEvent(self, event): """Handle window close""" # Signal background tasks to stop diff --git a/tests/test_review_regressions.py b/tests/test_review_regressions.py index 3db4fb2..70fd0d9 100644 --- a/tests/test_review_regressions.py +++ b/tests/test_review_regressions.py @@ -9,7 +9,7 @@ from unittest.mock import Mock import pytest from PySide6.QtCore import Qt from PySide6.QtMultimedia import QMediaPlayer -from PySide6.QtWidgets import QApplication, QWidget, QTreeWidgetItem +from PySide6.QtWidgets import QApplication, QDialog, QTableWidget, QTreeWidgetItem, QWidget from src.api.client import SubsonicClient from src.api.models import Song, Playlist @@ -49,6 +49,24 @@ def test_background_error_survives_until_ui_delivery(app): assert str(errors[0]) == "synthetic failure" +def test_keyboard_shortcuts_dialog_has_accessible_shortcut_table(app, monkeypatch): + parent = QWidget() + + def inspect_dialog(dialog): + table = dialog.findChild(QTableWidget) + assert dialog.windowTitle() == "Keyboard Shortcuts" + assert table.accessibleName() == "Keyboard shortcuts" + assert table.horizontalHeaderItem(0).text() == "Shortcut" + assert table.horizontalHeaderItem(1).text() == "Action" + assert table.rowCount() >= 20 + assert table.item(15, 0).text() == "Ctrl+Down or 9" + assert table.item(15, 1).text() == "Decrease volume" + return QDialog.Accepted + + monkeypatch.setattr(QDialog, "exec", inspect_dialog) + MainWindow.showKeyboardShortcuts(parent) + + @pytest.mark.parametrize("refresh", ["forced", "full", "incremental"]) def test_refresh_bypasses_api_memory_cache(tmp_path, refresh): raw = SubsonicClient("https://fake.example", "user", "fake")