feat: add configurable keyboard shortcut system
- src/hotkeys.py: HotkeyManager with 17 default shortcuts, config load/save via config.ini, KeyCaptureDialog for reassignment UI - src/config_loader.py: [Hotkeys] section defaults, _set_defaults called on every load to backfill missing sections - src/main.py: integrate HotkeyManager, seek_forward/backward (+/-5s), context-aware select_all/delete_selected, Settings tab UI with shortcut table and Reset to Defaults button
This commit is contained in:
parent
8cc4ef5279
commit
1ace4ad98c
@ -60,11 +60,10 @@ class ConfigLoader:
|
||||
if os.path.exists(self.config_file):
|
||||
logger.info(f"Loading configuration from {self.config_file}")
|
||||
self.config.read(self.config_file)
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"Configuration file {self.config_file} not found, using defaults")
|
||||
self._set_defaults()
|
||||
return False
|
||||
self._set_defaults()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load configuration: {e}")
|
||||
self._set_defaults()
|
||||
@ -128,6 +127,32 @@ class ConfigLoader:
|
||||
self.config.set('Advanced', 'temp_dir', 'temp')
|
||||
self.config.set('Advanced', 'concurrent_downloads', '2')
|
||||
self.config.set('Advanced', 'debug', 'false')
|
||||
|
||||
# Hotkeys section
|
||||
if not self.config.has_section('Hotkeys'):
|
||||
self.config.add_section('Hotkeys')
|
||||
|
||||
hotkey_defaults = {
|
||||
'play_pause': 'Space',
|
||||
'prev_track': 'Ctrl+Left',
|
||||
'next_track': 'Ctrl+Right',
|
||||
'volume_up': 'Ctrl+Up',
|
||||
'volume_down': 'Ctrl+Down',
|
||||
'seek_forward': 'Right',
|
||||
'seek_backward': 'Left',
|
||||
'tab_download': 'Ctrl+1',
|
||||
'tab_library': 'Ctrl+2',
|
||||
'tab_ipod': 'Ctrl+3',
|
||||
'tab_settings': 'Ctrl+4',
|
||||
'search_focus': 'Ctrl+F',
|
||||
'select_all': 'Ctrl+A',
|
||||
'library_refresh': 'F5',
|
||||
'ipod_refresh': 'Ctrl+R',
|
||||
'download': 'Ctrl+Enter',
|
||||
'delete_selected': 'Delete',
|
||||
}
|
||||
for key, value in hotkey_defaults.items():
|
||||
self.config.set('Hotkeys', key, value)
|
||||
|
||||
def get(self, section: str, option: str, fallback: Any = None) -> Any:
|
||||
"""
|
||||
|
||||
308
src/hotkeys.py
Normal file
308
src/hotkeys.py
Normal file
@ -0,0 +1,308 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Hotkey Manager for neo-pod-desktop
|
||||
Provides configurable keyboard shortcuts with scope awareness.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Callable, Dict, Optional
|
||||
|
||||
from PyQt6.QtCore import Qt, QObject
|
||||
from PyQt6.QtGui import QKeySequence, QShortcut, QAction
|
||||
from PyQt6.QtWidgets import QApplication, QWidget, QDialog, QVBoxLayout, QLabel, QPushButton
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
QSeq = QKeySequence
|
||||
|
||||
# Key names that QKeySequence doesn't expose as enum values
|
||||
_KEY_MAP = {
|
||||
"Space": QSeq(Qt.Key.Key_Space),
|
||||
"Delete": QSeq(Qt.Key.Key_Delete),
|
||||
"Up": QSeq(Qt.Key.Key_Up),
|
||||
"Down": QSeq(Qt.Key.Key_Down),
|
||||
"Left": QSeq(Qt.Key.Key_Left),
|
||||
"Right": QSeq(Qt.Key.Key_Right),
|
||||
"Enter": QSeq(Qt.Key.Key_Enter),
|
||||
"Return": QSeq(Qt.Key.Key_Return),
|
||||
"Esc": QSeq(Qt.Key.Key_Escape),
|
||||
"Escape": QSeq(Qt.Key.Key_Escape),
|
||||
"Tab": QSeq(Qt.Key.Key_Tab),
|
||||
"Backspace": QSeq(Qt.Key.Key_Backspace),
|
||||
"Plus": QSeq(Qt.Key.Key_Plus),
|
||||
"Minus": QSeq(Qt.Key.Key_Minus),
|
||||
"F1": QSeq(Qt.Key.Key_F1),
|
||||
"F2": QSeq(Qt.Key.Key_F2),
|
||||
"F3": QSeq(Qt.Key.Key_F3),
|
||||
"F4": QSeq(Qt.Key.Key_F4),
|
||||
"F5": QSeq(Qt.Key.Key_F5),
|
||||
"F6": QSeq(Qt.Key.Key_F6),
|
||||
"F7": QSeq(Qt.Key.Key_F7),
|
||||
"F8": QSeq(Qt.Key.Key_F8),
|
||||
"F9": QSeq(Qt.Key.Key_F9),
|
||||
"F10": QSeq(Qt.Key.Key_F10),
|
||||
"F11": QSeq(Qt.Key.Key_F11),
|
||||
"F12": QSeq(Qt.Key.Key_F12),
|
||||
}
|
||||
|
||||
|
||||
def _build_seq(text: str) -> QKeySequence:
|
||||
"""Build a QKeySequence from a human-readable shortcut string."""
|
||||
text = text.strip()
|
||||
if not text:
|
||||
return QSeq()
|
||||
known = _KEY_MAP.get(text)
|
||||
if known is not None:
|
||||
return known
|
||||
return QSeq(text)
|
||||
|
||||
|
||||
def _seq_to_string(seq: QKeySequence) -> str:
|
||||
"""Convert a QKeySequence to a human-readable string."""
|
||||
return seq.toString()
|
||||
|
||||
|
||||
def _is_editable_focused() -> bool:
|
||||
"""Return True if an editable widget currently has keyboard focus."""
|
||||
from PyQt6.QtWidgets import QLineEdit, QTextEdit, QPlainTextEdit, QSpinBox, QDoubleSpinBox
|
||||
w = QApplication.focusWidget()
|
||||
if w is None:
|
||||
return False
|
||||
return isinstance(w, (QLineEdit, QTextEdit, QPlainTextEdit, QSpinBox, QDoubleSpinBox))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Default shortcut definitions
|
||||
# (action_name, default_key_string, display_label, scope)
|
||||
# scope: "global" — always active; "tab" — handler decides based on current tab
|
||||
# ---------------------------------------------------------------------------
|
||||
DEFAULTS = [
|
||||
("play_pause", "Space", "Play / Pause", "global"),
|
||||
("prev_track", "Ctrl+Left", "Previous Track", "global"),
|
||||
("next_track", "Ctrl+Right", "Next Track", "global"),
|
||||
("volume_up", "Ctrl+Up", "Volume Up", "global"),
|
||||
("volume_down", "Ctrl+Down", "Volume Down", "global"),
|
||||
("seek_forward", "Right", "Seek Forward 5s", "global"),
|
||||
("seek_backward", "Left", "Seek Backward 5s", "global"),
|
||||
("tab_download", "Ctrl+1", "Switch to Download", "global"),
|
||||
("tab_library", "Ctrl+2", "Switch to Library", "global"),
|
||||
("tab_ipod", "Ctrl+3", "Switch to iPod", "global"),
|
||||
("tab_settings", "Ctrl+4", "Switch to Settings", "global"),
|
||||
("search_focus", "Ctrl+F", "Focus Search", "global"),
|
||||
("select_all", "Ctrl+A", "Select All", "global"),
|
||||
("library_refresh", "F5", "Refresh Library", "global"),
|
||||
("ipod_refresh", "Ctrl+R", "Refresh Devices", "global"),
|
||||
("download", "Ctrl+Enter", "Start Download", "global"),
|
||||
("delete_selected", "Delete", "Delete Selected", "global"),
|
||||
]
|
||||
|
||||
|
||||
class KeyCaptureDialog(QDialog):
|
||||
"""Modal dialog that captures a single key combination."""
|
||||
|
||||
def __init__(self, parent=None, current: str = ""):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Press New Shortcut")
|
||||
self.setMinimumWidth(320)
|
||||
self.setModal(True)
|
||||
|
||||
self._captured_seq: QKeySequence = QSeq()
|
||||
self._modifiers = Qt.KeyboardModifier.NoModifier
|
||||
self._key = Qt.Key.Key_unknown
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self._label = QLabel(f"Press new shortcut for this action...\n(current: {current or 'none'})")
|
||||
self._label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self._label.setWordWrap(True)
|
||||
layout.addWidget(self._label)
|
||||
|
||||
clear_btn = QPushButton("Clear (Disable)")
|
||||
clear_btn.clicked.connect(self._on_clear)
|
||||
layout.addWidget(clear_btn)
|
||||
|
||||
layout.addWidget(QPushButton("Cancel", clicked=self.reject))
|
||||
|
||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.setFocus()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
key = event.key()
|
||||
modifiers = event.modifiers()
|
||||
mod_mask = modifiers & (
|
||||
Qt.KeyboardModifier.ControlModifier
|
||||
| Qt.KeyboardModifier.ShiftModifier
|
||||
| Qt.KeyboardModifier.AltModifier
|
||||
| Qt.KeyboardModifier.MetaModifier
|
||||
)
|
||||
|
||||
# Ignore pure modifier presses
|
||||
ignore = {
|
||||
Qt.Key.Key_Control, Qt.Key.Key_Shift, Qt.Key.Key_Alt, Qt.Key.Key_Meta,
|
||||
Qt.Key.Key_Super_L, Qt.Key.Key_Super_R,
|
||||
}
|
||||
if key in ignore:
|
||||
return
|
||||
|
||||
# Build the sequence
|
||||
keys = int(mod_mask) | int(key)
|
||||
self._captured_seq = QSeq(keys)
|
||||
self._label.setText(f"Captured: {self._captured_seq.toString()}\nPress Close to accept")
|
||||
self.accept()
|
||||
|
||||
def _on_clear(self):
|
||||
self._captured_seq = QSeq()
|
||||
self._label.setText("Shortcut cleared (disabled)")
|
||||
self.accept()
|
||||
|
||||
def captured_sequence(self) -> QKeySequence:
|
||||
return self._captured_seq
|
||||
|
||||
|
||||
class HotkeyManager:
|
||||
"""Manages all keyboard shortcuts for the application.
|
||||
|
||||
Loads key bindings from config, creates QShortcut instances,
|
||||
and supports live reassignment at runtime.
|
||||
"""
|
||||
|
||||
def __init__(self, parent: QWidget, config_loader=None):
|
||||
self._parent = parent
|
||||
self._config_loader = config_loader
|
||||
|
||||
# action_name → (QShortcut, callback, default_seq, scope)
|
||||
self._shortcuts: Dict[str, QShortcut] = {}
|
||||
self._callbacks: Dict[str, Callable[[], None]] = {}
|
||||
self._defaults: Dict[str, QKeySequence] = {}
|
||||
self._labels: Dict[str, str] = {}
|
||||
self._scopes: Dict[str, str] = {}
|
||||
|
||||
for name, key_str, label, scope in DEFAULTS:
|
||||
self._defaults[name] = _build_seq(key_str)
|
||||
self._labels[name] = label
|
||||
self._scopes[name] = scope
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Registration
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def register(self, action_name: str, callback: Callable[[], None]) -> None:
|
||||
"""Register a shortcut with its callback.
|
||||
|
||||
If a saved key exists in config it is used; otherwise the default.
|
||||
"""
|
||||
saved_key = None
|
||||
if self._config_loader:
|
||||
saved_key = self._config_loader.get("Hotkeys", action_name, fallback=None)
|
||||
|
||||
seq = _build_seq(saved_key) if saved_key else self._defaults.get(action_name, QSeq())
|
||||
self._callbacks[action_name] = callback
|
||||
|
||||
scope = self._scopes.get(action_name, "global")
|
||||
context = Qt.ShortcutContext.WindowShortcut
|
||||
|
||||
sc = QShortcut(seq, self._parent, context=context)
|
||||
|
||||
def _make_handler(name):
|
||||
def handler():
|
||||
self._dispatch(name)
|
||||
return handler
|
||||
|
||||
sc.activated.connect(_make_handler(action_name))
|
||||
self._shortcuts[action_name] = sc
|
||||
|
||||
def register_all(self, action_map: Dict[str, Callable[[], None]]) -> None:
|
||||
"""Register all known shortcuts from an action_name→callback mapping."""
|
||||
for name, callback in action_map.items():
|
||||
self.register(name, callback)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Dispatch
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _dispatch(self, action_name: str):
|
||||
"""Check if the shortcut should fire (not blocked by editable focus),
|
||||
then invoke the callback.
|
||||
"""
|
||||
# For seek arrows and space, don't fire when text editing
|
||||
editable_blocked = {"seek_forward", "seek_backward", "play_pause"}
|
||||
if action_name in editable_blocked and _is_editable_focused():
|
||||
return
|
||||
|
||||
cb = self._callbacks.get(action_name)
|
||||
if cb:
|
||||
cb()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Configuration persistence
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def save_to_config(self) -> None:
|
||||
"""Write all current key bindings to the config loader."""
|
||||
if self._config_loader is None:
|
||||
return
|
||||
for name, sc in self._shortcuts.items():
|
||||
key_str = sc.key().toString()
|
||||
self._config_loader.set("Hotkeys", name, key_str if key_str else "")
|
||||
self._config_loader.save()
|
||||
|
||||
def load_from_config(self) -> None:
|
||||
"""Re-read key bindings from config and update all shortcuts."""
|
||||
if self._config_loader is None:
|
||||
return
|
||||
for name, sc in self._shortcuts.items():
|
||||
saved = self._config_loader.get("Hotkeys", name, fallback=None)
|
||||
if saved is not None:
|
||||
seq = _build_seq(saved) if saved != "" else QSeq()
|
||||
else:
|
||||
seq = self._defaults.get(name, QSeq())
|
||||
sc.setKey(seq)
|
||||
|
||||
def reset_to_defaults(self) -> None:
|
||||
"""Reset all shortcuts to their default keys and save."""
|
||||
for name, sc in self._shortcuts.items():
|
||||
default_seq = self._defaults.get(name, QSeq())
|
||||
sc.setKey(default_seq)
|
||||
self.save_to_config()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Query API (for Settings UI)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def get_label(self, action_name: str) -> str:
|
||||
return self._labels.get(action_name, action_name)
|
||||
|
||||
def get_current_key_string(self, action_name: str) -> str:
|
||||
sc = self._shortcuts.get(action_name)
|
||||
if sc is None:
|
||||
return ""
|
||||
return sc.key().toString()
|
||||
|
||||
def get_default_key_string(self, action_name: str) -> str:
|
||||
return self._defaults.get(action_name, QSeq()).toString()
|
||||
|
||||
def all_action_names(self):
|
||||
return list(self._shortcuts.keys())
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Live reassignment (called from Settings UI)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def set_shortcut(self, action_name: str, key_string: str) -> None:
|
||||
"""Change the key binding for an action at runtime and persist."""
|
||||
sc = self._shortcuts.get(action_name)
|
||||
if sc is None:
|
||||
return
|
||||
seq = _build_seq(key_string)
|
||||
sc.setKey(seq)
|
||||
if self._config_loader:
|
||||
self._config_loader.set("Hotkeys", action_name, key_string)
|
||||
self._config_loader.save()
|
||||
|
||||
def set_shortcut_seq(self, action_name: str, seq: QKeySequence) -> None:
|
||||
"""Set a shortcut directly from a QKeySequence (from key capture dialog)."""
|
||||
sc = self._shortcuts.get(action_name)
|
||||
if sc is not None:
|
||||
sc.setKey(seq)
|
||||
if self._config_loader:
|
||||
self._config_loader.set("Hotkeys", action_name, seq.toString())
|
||||
self._config_loader.save()
|
||||
148
src/main.py
148
src/main.py
@ -26,6 +26,8 @@ from audio_converter import AudioConverter
|
||||
from metadata_handler import MetadataHandler
|
||||
from ipod_device import IPodDevice
|
||||
from artwork.cache import cover_cache
|
||||
from config_loader import ConfigLoader
|
||||
from hotkeys import HotkeyManager, KeyCaptureDialog
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -282,6 +284,9 @@ class MainWindow(QMainWindow):
|
||||
self.current_mount_point: Optional[str] = None
|
||||
self.show_download_tab = False
|
||||
|
||||
self.config_loader = ConfigLoader()
|
||||
self.hotkey_manager: Optional[HotkeyManager] = None
|
||||
|
||||
self.player: Optional[QMediaPlayer] = None
|
||||
self.audio_output: Optional[QAudioOutput] = None
|
||||
self.current_playback_index: int = -1
|
||||
@ -289,6 +294,8 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self._setup_ui()
|
||||
self._setup_player()
|
||||
self._load_settings()
|
||||
self._setup_hotkeys()
|
||||
self._scan_library()
|
||||
self._on_refresh_devices_clicked()
|
||||
|
||||
@ -304,6 +311,54 @@ class MainWindow(QMainWindow):
|
||||
self.worker_thread.deleteLater()
|
||||
self.worker_thread = None
|
||||
|
||||
def _load_settings(self):
|
||||
config = self.config_loader
|
||||
self.output_dir_input.setText(
|
||||
config.get("General", "output_dir",
|
||||
fallback=os.path.join(os.path.expanduser("~"), "Music", "iPod"))
|
||||
)
|
||||
self.format_combo.setCurrentText(config.get("General", "format", fallback="m4a"))
|
||||
self.quality_spin.setValue(config.get_int("General", "quality", fallback=256))
|
||||
self.enable_video_check.setChecked(
|
||||
config.get_boolean("Video", "enable_video", fallback=False)
|
||||
)
|
||||
self.embed_artwork_check.setChecked(
|
||||
config.get_boolean("Metadata", "embed_artwork", fallback=True)
|
||||
)
|
||||
self.clean_temp_check.setChecked(
|
||||
config.get_boolean("Advanced", "clean_temp", fallback=True)
|
||||
)
|
||||
self.auto_detect_check.setChecked(
|
||||
config.get_boolean("Device", "auto_detect", fallback=True)
|
||||
)
|
||||
|
||||
def _setup_hotkeys(self):
|
||||
self.hotkey_manager = HotkeyManager(self, self.config_loader)
|
||||
self.hotkey_manager.register_all({
|
||||
"play_pause": self._on_play_pause_clicked,
|
||||
"prev_track": self._on_prev_clicked,
|
||||
"next_track": self._on_next_clicked,
|
||||
"volume_up": lambda: self._adjust_volume(5),
|
||||
"volume_down": lambda: self._adjust_volume(-5),
|
||||
"seek_forward": self._seek_forward,
|
||||
"seek_backward": self._seek_backward,
|
||||
"tab_download": lambda: self.tab_widget.setCurrentIndex(self.download_tab_index),
|
||||
"tab_library": lambda: self.tab_widget.setCurrentIndex(1),
|
||||
"tab_ipod": lambda: self.tab_widget.setCurrentIndex(2),
|
||||
"tab_settings": lambda: self.tab_widget.setCurrentIndex(3),
|
||||
"search_focus": self._focus_search,
|
||||
"select_all": self._select_all_current,
|
||||
"library_refresh": self._on_refresh_library_clicked,
|
||||
"ipod_refresh": self._on_refresh_devices_clicked,
|
||||
"download": self._on_download_clicked,
|
||||
"delete_selected": self._delete_selected_current,
|
||||
})
|
||||
self._refresh_shortcuts_table()
|
||||
|
||||
def _adjust_volume(self, delta: int):
|
||||
new_val = max(0, min(100, self.volume_slider.value() + delta))
|
||||
self.volume_slider.setValue(new_val)
|
||||
|
||||
def _setup_ui(self):
|
||||
main_widget = QWidget()
|
||||
main_layout = QVBoxLayout(main_widget)
|
||||
@ -668,6 +723,28 @@ class MainWindow(QMainWindow):
|
||||
|
||||
layout.addWidget(advanced_group)
|
||||
|
||||
shortcuts_group = QGroupBox("Keyboard Shortcuts")
|
||||
shortcuts_layout = QVBoxLayout(shortcuts_group)
|
||||
|
||||
self.shortcuts_table = QTableWidget()
|
||||
self.shortcuts_table.setColumnCount(2)
|
||||
self.shortcuts_table.setHorizontalHeaderLabels(["Action", "Shortcut"])
|
||||
self.shortcuts_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||
self.shortcuts_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
|
||||
self.shortcuts_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
self.shortcuts_table.setAlternatingRowColors(True)
|
||||
self.shortcuts_table.cellDoubleClicked.connect(self._on_shortcut_double_clicked)
|
||||
shortcuts_header = self.shortcuts_table.horizontalHeader()
|
||||
shortcuts_header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
shortcuts_header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
||||
shortcuts_layout.addWidget(self.shortcuts_table)
|
||||
|
||||
reset_shortcuts_btn = QPushButton("Reset to Defaults")
|
||||
reset_shortcuts_btn.clicked.connect(self._on_reset_shortcuts_clicked)
|
||||
shortcuts_layout.addWidget(reset_shortcuts_btn)
|
||||
|
||||
layout.addWidget(shortcuts_group)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
about_label = QLabel(
|
||||
@ -770,6 +847,37 @@ class MainWindow(QMainWindow):
|
||||
idx = min(self.library_table.rowCount() - 1, self.current_playback_index + 1)
|
||||
self._play_track_from_index(idx)
|
||||
|
||||
def _seek_forward(self):
|
||||
if self.player is None or self.player.duration() <= 0:
|
||||
return
|
||||
new_pos = min(self.player.duration(), self.player.position() + 5000)
|
||||
self.player.setPosition(new_pos)
|
||||
|
||||
def _seek_backward(self):
|
||||
if self.player is None or self.player.duration() <= 0:
|
||||
return
|
||||
new_pos = max(0, self.player.position() - 5000)
|
||||
self.player.setPosition(new_pos)
|
||||
|
||||
def _focus_search(self):
|
||||
self.tab_widget.setCurrentIndex(1)
|
||||
self.library_search_input.setFocus()
|
||||
self.library_search_input.selectAll()
|
||||
|
||||
def _select_all_current(self):
|
||||
idx = self.tab_widget.currentIndex()
|
||||
if idx == 1:
|
||||
self.library_table.selectAll()
|
||||
elif idx == 2:
|
||||
self.transferred_list.selectAll()
|
||||
|
||||
def _delete_selected_current(self):
|
||||
idx = self.tab_widget.currentIndex()
|
||||
if idx == 1:
|
||||
self._on_library_remove_selected()
|
||||
elif idx == 2:
|
||||
self._on_remove_tracks_clicked()
|
||||
|
||||
def _on_slider_pressed(self):
|
||||
if self.player:
|
||||
self.player.setPosition(
|
||||
@ -1745,6 +1853,46 @@ class MainWindow(QMainWindow):
|
||||
QMessageBox.information(self, "Duplicates" if success else "Error", message)
|
||||
self._cleanup_worker()
|
||||
|
||||
def _refresh_shortcuts_table(self):
|
||||
if self.hotkey_manager is None:
|
||||
return
|
||||
names = self.hotkey_manager.all_action_names()
|
||||
self.shortcuts_table.setRowCount(len(names))
|
||||
for row, name in enumerate(names):
|
||||
label = self.hotkey_manager.get_label(name)
|
||||
key = self.hotkey_manager.get_current_key_string(name)
|
||||
self.shortcuts_table.setItem(row, 0, QTableWidgetItem(label))
|
||||
item = QTableWidgetItem(key if key else "\u2014")
|
||||
self.shortcuts_table.setItem(row, 1, item)
|
||||
|
||||
def _on_shortcut_double_clicked(self, row: int, _col: int):
|
||||
if self.hotkey_manager is None:
|
||||
return
|
||||
names = self.hotkey_manager.all_action_names()
|
||||
if row < 0 or row >= len(names):
|
||||
return
|
||||
action_name = names[row]
|
||||
current = self.hotkey_manager.get_current_key_string(action_name)
|
||||
|
||||
dialog = KeyCaptureDialog(self, current=current)
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
seq = dialog.captured_sequence()
|
||||
self.hotkey_manager.set_shortcut_seq(action_name, seq)
|
||||
self._refresh_shortcuts_table()
|
||||
|
||||
def _on_reset_shortcuts_clicked(self):
|
||||
if self.hotkey_manager is None:
|
||||
return
|
||||
reply = QMessageBox.question(
|
||||
self, "Reset Shortcuts",
|
||||
"Reset all keyboard shortcuts to their default values?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
self.hotkey_manager.reset_to_defaults()
|
||||
self._refresh_shortcuts_table()
|
||||
QMessageBox.information(self, "Done", "Shortcuts reset to defaults.")
|
||||
|
||||
def closeEvent(self, event):
|
||||
if self.worker_thread and self.worker_thread.isRunning():
|
||||
self.worker_thread.stop()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user