|
|
@@ -0,0 +1,728 @@
|
|
|
+"""
|
|
|
+Keyboard Shortcuts Manager for UI.
|
|
|
+
|
|
|
+Implements Story 7.13: Keyboard shortcut support with
|
|
|
+common operation shortcuts and configuration panel.
|
|
|
+"""
|
|
|
+
|
|
|
+from typing import Dict, Optional, List, Tuple
|
|
|
+from dataclasses import dataclass
|
|
|
+from enum import Enum
|
|
|
+
|
|
|
+from PyQt6.QtWidgets import (
|
|
|
+ QWidget,
|
|
|
+ QVBoxLayout,
|
|
|
+ QHBoxLayout,
|
|
|
+ QTableView,
|
|
|
+ QPushButton,
|
|
|
+ QLabel,
|
|
|
+ QLineEdit,
|
|
|
+ QDialog,
|
|
|
+ QDialogButtonBox,
|
|
|
+ QFormLayout,
|
|
|
+ QMessageBox,
|
|
|
+ QGroupBox,
|
|
|
+ QHeaderView,
|
|
|
+ QAbstractItemView,
|
|
|
+ QApplication,
|
|
|
+ QComboBox,
|
|
|
+)
|
|
|
+from PyQt6.QtCore import Qt, pyqtSignal, QAbstractTableModel, QModelIndex, QSettings
|
|
|
+from PyQt6.QtGui import QKeySequence, QAction, QShortcut, QFont
|
|
|
+
|
|
|
+
|
|
|
+class StandardAction(Enum):
|
|
|
+ """Standard application actions with default shortcuts."""
|
|
|
+
|
|
|
+ # File operations
|
|
|
+ OPEN_FILE = "open_file"
|
|
|
+ SAVE_FILE = "save_file"
|
|
|
+ EXPORT = "export"
|
|
|
+ EXIT = "exit"
|
|
|
+
|
|
|
+ # Edit operations
|
|
|
+ UNDO = "undo"
|
|
|
+ REDO = "redo"
|
|
|
+ CUT = "cut"
|
|
|
+ COPY = "copy"
|
|
|
+ PASTE = "paste"
|
|
|
+ SELECT_ALL = "select_all"
|
|
|
+ FIND = "find"
|
|
|
+ SETTINGS = "settings"
|
|
|
+
|
|
|
+ # View operations
|
|
|
+ FULL_SCREEN = "full_screen"
|
|
|
+ ZOOM_IN = "zoom_in"
|
|
|
+ ZOOM_OUT = "zoom_out"
|
|
|
+ RESET_ZOOM = "reset_zoom"
|
|
|
+
|
|
|
+ # Translation operations
|
|
|
+ START_TRANSLATION = "start_translation"
|
|
|
+ PAUSE_TRANSLATION = "pause_translation"
|
|
|
+ CANCEL_TRANSLATION = "cancel_translation"
|
|
|
+
|
|
|
+ # Navigation
|
|
|
+ NEXT_FILE = "next_file"
|
|
|
+ PREVIOUS_FILE = "previous_file"
|
|
|
+ NEXT_CHAPTER = "next_chapter"
|
|
|
+ PREVIOUS_CHAPTER = "previous_chapter"
|
|
|
+
|
|
|
+ # Help
|
|
|
+ HELP = "help"
|
|
|
+ ABOUT = "about"
|
|
|
+
|
|
|
+
|
|
|
+# Default shortcuts mapping
|
|
|
+DEFAULT_SHORTCUTS: Dict[StandardAction, str] = {
|
|
|
+ # File operations
|
|
|
+ StandardAction.OPEN_FILE: "Ctrl+O",
|
|
|
+ StandardAction.SAVE_FILE: "Ctrl+S",
|
|
|
+ StandardAction.EXPORT: "Ctrl+E",
|
|
|
+ StandardAction.EXIT: "Ctrl+Q",
|
|
|
+
|
|
|
+ # Edit operations
|
|
|
+ StandardAction.UNDO: "Ctrl+Z",
|
|
|
+ StandardAction.REDO: "Ctrl+Shift+Z",
|
|
|
+ StandardAction.CUT: "Ctrl+X",
|
|
|
+ StandardAction.COPY: "Ctrl+C",
|
|
|
+ StandardAction.PASTE: "Ctrl+V",
|
|
|
+ StandardAction.SELECT_ALL: "Ctrl+A",
|
|
|
+ StandardAction.FIND: "Ctrl+F",
|
|
|
+ StandardAction.SETTINGS: "Ctrl+,",
|
|
|
+
|
|
|
+ # View operations
|
|
|
+ StandardAction.FULL_SCREEN: "F11",
|
|
|
+ StandardAction.ZOOM_IN: "Ctrl++",
|
|
|
+ StandardAction.ZOOM_OUT: "Ctrl+-",
|
|
|
+ StandardAction.RESET_ZOOM: "Ctrl+0",
|
|
|
+
|
|
|
+ # Translation operations
|
|
|
+ StandardAction.START_TRANSLATION: "F5",
|
|
|
+ StandardAction.PAUSE_TRANSLATION: "F6",
|
|
|
+ StandardAction.CANCEL_TRANSLATION: "Esc",
|
|
|
+
|
|
|
+ # Navigation
|
|
|
+ StandardAction.NEXT_FILE: "Ctrl+Tab",
|
|
|
+ StandardAction.PREVIOUS_FILE: "Ctrl+Shift+Tab",
|
|
|
+ StandardAction.NEXT_CHAPTER: "Alt+Down",
|
|
|
+ StandardAction.PREVIOUS_CHAPTER: "Alt+Up",
|
|
|
+
|
|
|
+ # Help
|
|
|
+ StandardAction.HELP: "F1",
|
|
|
+ StandardAction.ABOUT: "Ctrl+F1",
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+# Display names for actions
|
|
|
+ACTION_NAMES: Dict[StandardAction, str] = {
|
|
|
+ # File operations
|
|
|
+ StandardAction.OPEN_FILE: "打开文件",
|
|
|
+ StandardAction.SAVE_FILE: "保存",
|
|
|
+ StandardAction.EXPORT: "导出",
|
|
|
+ StandardAction.EXIT: "退出",
|
|
|
+
|
|
|
+ # Edit operations
|
|
|
+ StandardAction.UNDO: "撤销",
|
|
|
+ StandardAction.REDO: "重做",
|
|
|
+ StandardAction.CUT: "剪切",
|
|
|
+ StandardAction.COPY: "复制",
|
|
|
+ StandardAction.PASTE: "粘贴",
|
|
|
+ StandardAction.SELECT_ALL: "全选",
|
|
|
+ StandardAction.FIND: "查找",
|
|
|
+ StandardAction.SETTINGS: "设置",
|
|
|
+
|
|
|
+ # View operations
|
|
|
+ StandardAction.FULL_SCREEN: "全屏",
|
|
|
+ StandardAction.ZOOM_IN: "放大",
|
|
|
+ StandardAction.ZOOM_OUT: "缩小",
|
|
|
+ StandardAction.RESET_ZOOM: "重置缩放",
|
|
|
+
|
|
|
+ # Translation operations
|
|
|
+ StandardAction.START_TRANSLATION: "开始翻译",
|
|
|
+ StandardAction.PAUSE_TRANSLATION: "暂停翻译",
|
|
|
+ StandardAction.CANCEL_TRANSLATION: "取消翻译",
|
|
|
+
|
|
|
+ # Navigation
|
|
|
+ StandardAction.NEXT_FILE: "下一个文件",
|
|
|
+ StandardAction.PREVIOUS_FILE: "上一个文件",
|
|
|
+ StandardAction.NEXT_CHAPTER: "下一章",
|
|
|
+ StandardAction.PREVIOUS_CHAPTER: "上一章",
|
|
|
+
|
|
|
+ # Help
|
|
|
+ StandardAction.HELP: "帮助",
|
|
|
+ StandardAction.ABOUT: "关于",
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+@dataclass
|
|
|
+class ShortcutEntry:
|
|
|
+ """An entry in the shortcuts configuration."""
|
|
|
+
|
|
|
+ action: StandardAction
|
|
|
+ key_sequence: str
|
|
|
+ default_key_sequence: str
|
|
|
+ name: str
|
|
|
+ category: str
|
|
|
+
|
|
|
+ @property
|
|
|
+ def is_modified(self) -> bool:
|
|
|
+ """Check if shortcut has been modified from default."""
|
|
|
+ return self.key_sequence != self.default_key_sequence
|
|
|
+
|
|
|
+
|
|
|
+class ShortcutsManager:
|
|
|
+ """
|
|
|
+ Manager for keyboard shortcuts.
|
|
|
+
|
|
|
+ Features:
|
|
|
+ - Default shortcut mappings
|
|
|
+ - Persistent storage via QSettings
|
|
|
+ - Shortcut conflict detection
|
|
|
+ - Reset to defaults
|
|
|
+ """
|
|
|
+
|
|
|
+ # Settings keys
|
|
|
+ SETTINGS_PREFIX = "shortcuts/"
|
|
|
+
|
|
|
+ def __init__(self) -> None:
|
|
|
+ """Initialize the shortcuts manager."""
|
|
|
+ self._settings = QSettings("BMAD", "NovelTranslator")
|
|
|
+ self._shortcuts: Dict[StandardAction, QKeySequence] = {}
|
|
|
+ self._actions: Dict[StandardAction, QAction] = {}
|
|
|
+
|
|
|
+ # Load shortcuts from settings
|
|
|
+ self._load_settings()
|
|
|
+
|
|
|
+ def _load_settings(self) -> None:
|
|
|
+ """Load shortcuts from QSettings."""
|
|
|
+ for action, default_str in DEFAULT_SHORTCUTS.items():
|
|
|
+ saved_str = self._settings.value(
|
|
|
+ self.SETTINGS_PREFIX + action.value,
|
|
|
+ default_str
|
|
|
+ )
|
|
|
+ self._shortcuts[action] = QKeySequence(saved_str)
|
|
|
+
|
|
|
+ def _save_settings(self) -> None:
|
|
|
+ """Save shortcuts to QSettings."""
|
|
|
+ for action, key_seq in self._shortcuts.items():
|
|
|
+ self._settings.setValue(
|
|
|
+ self.SETTINGS_PREFIX + action.value,
|
|
|
+ key_seq.toString()
|
|
|
+ )
|
|
|
+
|
|
|
+ def get_shortcut(self, action: StandardAction) -> QKeySequence:
|
|
|
+ """
|
|
|
+ Get the key sequence for an action.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ action: The action to get the shortcut for
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ The key sequence for the action
|
|
|
+ """
|
|
|
+ return self._shortcuts.get(action, QKeySequence())
|
|
|
+
|
|
|
+ def set_shortcut(self, action: StandardAction, key_sequence: str) -> bool:
|
|
|
+ """
|
|
|
+ Set a shortcut for an action.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ action: The action to set the shortcut for
|
|
|
+ key_sequence: The new key sequence string
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ True if successful, False if there's a conflict
|
|
|
+ """
|
|
|
+ # Check for conflicts
|
|
|
+ new_seq = QKeySequence(key_sequence)
|
|
|
+ if not new_seq.isEmpty():
|
|
|
+ for existing_action, existing_seq in self._shortcuts.items():
|
|
|
+ if existing_action != action and existing_seq == new_seq:
|
|
|
+ return False # Conflict detected
|
|
|
+
|
|
|
+ self._shortcuts[action] = new_seq
|
|
|
+ self._save_settings()
|
|
|
+
|
|
|
+ # Update associated QAction if exists
|
|
|
+ if action in self._actions:
|
|
|
+ self._actions[action].setShortcut(new_seq)
|
|
|
+
|
|
|
+ return True
|
|
|
+
|
|
|
+ def register_action(self, action: StandardAction, qaction: QAction) -> None:
|
|
|
+ """
|
|
|
+ Register a QAction with a standard action.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ action: The standard action to register
|
|
|
+ qaction: The QAction to associate
|
|
|
+ """
|
|
|
+ self._actions[action] = qaction
|
|
|
+ qaction.setShortcut(self._shortcuts[action])
|
|
|
+
|
|
|
+ def create_action(
|
|
|
+ self,
|
|
|
+ action: StandardAction,
|
|
|
+ parent: Optional[QWidget] = None,
|
|
|
+ callback: Optional[callable] = None,
|
|
|
+ ) -> QAction:
|
|
|
+ """
|
|
|
+ Create a QAction with the appropriate shortcut.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ action: The standard action
|
|
|
+ parent: Parent widget
|
|
|
+ callback: Optional callback function
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ Configured QAction
|
|
|
+ """
|
|
|
+ qaction = QAction(ACTION_NAMES[action], parent)
|
|
|
+ qaction.setShortcut(self._shortcuts[action])
|
|
|
+
|
|
|
+ if callback:
|
|
|
+ qaction.triggered.connect(callback)
|
|
|
+
|
|
|
+ self._actions[action] = qaction
|
|
|
+ return qaction
|
|
|
+
|
|
|
+ def reset_to_defaults(self) -> None:
|
|
|
+ """Reset all shortcuts to default values."""
|
|
|
+ for action, default_str in DEFAULT_SHORTCUTS.items():
|
|
|
+ self._shortcuts[action] = QKeySequence(default_str)
|
|
|
+ if action in self._actions:
|
|
|
+ self._actions[action].setShortcut(QKeySequence(default_str))
|
|
|
+
|
|
|
+ self._save_settings()
|
|
|
+
|
|
|
+ def reset_action(self, action: StandardAction) -> None:
|
|
|
+ """
|
|
|
+ Reset a single action to its default shortcut.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ action: The action to reset
|
|
|
+ """
|
|
|
+ default_seq = QKeySequence(DEFAULT_SHORTCUTS[action])
|
|
|
+ self._shortcuts[action] = default_seq
|
|
|
+
|
|
|
+ if action in self._actions:
|
|
|
+ self._actions[action].setShortcut(default_seq)
|
|
|
+
|
|
|
+ self._save_settings()
|
|
|
+
|
|
|
+ def get_all_entries(self) -> List[ShortcutEntry]:
|
|
|
+ """
|
|
|
+ Get all shortcut entries.
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ List of all shortcut entries
|
|
|
+ """
|
|
|
+ entries = []
|
|
|
+
|
|
|
+ # Group by category
|
|
|
+ categories = {
|
|
|
+ StandardAction.OPEN_FILE: "文件",
|
|
|
+ StandardAction.SAVE_FILE: "文件",
|
|
|
+ StandardAction.EXPORT: "文件",
|
|
|
+ StandardAction.EXIT: "文件",
|
|
|
+ StandardAction.UNDO: "编辑",
|
|
|
+ StandardAction.REDO: "编辑",
|
|
|
+ StandardAction.CUT: "编辑",
|
|
|
+ StandardAction.COPY: "编辑",
|
|
|
+ StandardAction.PASTE: "编辑",
|
|
|
+ StandardAction.SELECT_ALL: "编辑",
|
|
|
+ StandardAction.FIND: "编辑",
|
|
|
+ StandardAction.SETTINGS: "编辑",
|
|
|
+ StandardAction.FULL_SCREEN: "视图",
|
|
|
+ StandardAction.ZOOM_IN: "视图",
|
|
|
+ StandardAction.ZOOM_OUT: "视图",
|
|
|
+ StandardAction.RESET_ZOOM: "视图",
|
|
|
+ StandardAction.START_TRANSLATION: "翻译",
|
|
|
+ StandardAction.PAUSE_TRANSLATION: "翻译",
|
|
|
+ StandardAction.CANCEL_TRANSLATION: "翻译",
|
|
|
+ StandardAction.NEXT_FILE: "导航",
|
|
|
+ StandardAction.PREVIOUS_FILE: "导航",
|
|
|
+ StandardAction.NEXT_CHAPTER: "导航",
|
|
|
+ StandardAction.PREVIOUS_CHAPTER: "导航",
|
|
|
+ StandardAction.HELP: "帮助",
|
|
|
+ StandardAction.ABOUT: "帮助",
|
|
|
+ }
|
|
|
+
|
|
|
+ for action in StandardAction:
|
|
|
+ key_seq = self._shortcuts[action].toString()
|
|
|
+ default_seq = DEFAULT_SHORTCUTS[action]
|
|
|
+ category = categories.get(action, "其他")
|
|
|
+
|
|
|
+ entries.append(ShortcutEntry(
|
|
|
+ action=action,
|
|
|
+ key_sequence=key_seq,
|
|
|
+ default_key_sequence=default_seq,
|
|
|
+ name=ACTION_NAMES[action],
|
|
|
+ category=category
|
|
|
+ ))
|
|
|
+
|
|
|
+ return entries
|
|
|
+
|
|
|
+
|
|
|
+class ShortcutsModel(QAbstractTableModel):
|
|
|
+ """Table model for shortcuts configuration."""
|
|
|
+
|
|
|
+ COL_ACTION = 0
|
|
|
+ COL_SHORTCUT = 1
|
|
|
+ COL_DEFAULT = 2
|
|
|
+ COLUMN_COUNT = 3
|
|
|
+
|
|
|
+ HEADERS = ["操作", "快捷键", "默认"]
|
|
|
+
|
|
|
+ def __init__(self, entries: List[ShortcutEntry], parent=None):
|
|
|
+ """Initialize the model."""
|
|
|
+ super().__init__(parent)
|
|
|
+ self._entries = entries
|
|
|
+
|
|
|
+ def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
|
|
+ """Return the number of rows."""
|
|
|
+ return len(self._entries)
|
|
|
+
|
|
|
+ def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
|
|
+ """Return the number of columns."""
|
|
|
+ return self.COLUMN_COUNT
|
|
|
+
|
|
|
+ def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
|
|
|
+ """Return data for the given index."""
|
|
|
+ if not index.isValid():
|
|
|
+ return None
|
|
|
+
|
|
|
+ row = index.row()
|
|
|
+ col = index.column()
|
|
|
+
|
|
|
+ if row < 0 or row >= len(self._entries):
|
|
|
+ return None
|
|
|
+
|
|
|
+ entry = self._entries[row]
|
|
|
+
|
|
|
+ if role == Qt.ItemDataRole.DisplayRole:
|
|
|
+ if col == self.COL_ACTION:
|
|
|
+ return entry.name
|
|
|
+ elif col == self.COL_SHORTCUT:
|
|
|
+ return entry.key_sequence
|
|
|
+ elif col == self.COL_DEFAULT:
|
|
|
+ return entry.default_key_sequence
|
|
|
+
|
|
|
+ elif role == Qt.ItemDataRole.FontRole:
|
|
|
+ if col == self.COL_SHORTCUT and entry.is_modified:
|
|
|
+ font = QFont()
|
|
|
+ font.setBold(True)
|
|
|
+ return font
|
|
|
+
|
|
|
+ elif role == Qt.ItemDataRole.TextAlignmentRole:
|
|
|
+ if col == self.COL_SHORTCUT or col == self.COL_DEFAULT:
|
|
|
+ return Qt.AlignmentFlag.AlignCenter
|
|
|
+ return Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
|
|
+
|
|
|
+ return None
|
|
|
+
|
|
|
+ def headerData(self, section: int, orientation: Qt.Orientation,
|
|
|
+ role: int = Qt.ItemDataRole.DisplayRole):
|
|
|
+ """Return header data."""
|
|
|
+ if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
|
|
|
+ if 0 <= section < self.COLUMN_COUNT:
|
|
|
+ return self.HEADERS[section]
|
|
|
+ return None
|
|
|
+
|
|
|
+ def get_entry_at_row(self, row: int) -> Optional[ShortcutEntry]:
|
|
|
+ """Get the entry at the given row."""
|
|
|
+ if 0 <= row < len(self._entries):
|
|
|
+ return self._entries[row]
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+class ShortcutCaptureButton(QPushButton):
|
|
|
+ """
|
|
|
+ Button that captures key sequences when clicked.
|
|
|
+
|
|
|
+ When clicked, it waits for the user to press a key combination
|
|
|
+ and displays the resulting shortcut.
|
|
|
+ """
|
|
|
+
|
|
|
+ shortcut_captured = pyqtSignal(str) # Emitted when a shortcut is captured
|
|
|
+
|
|
|
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
|
|
|
+ """Initialize the button."""
|
|
|
+ super().__init__("点击设置快捷键", parent)
|
|
|
+
|
|
|
+ self._capturing = False
|
|
|
+ self._current_sequence: str = ""
|
|
|
+ self._pressed_keys: List[int] = []
|
|
|
+
|
|
|
+ self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
|
|
+ self.clicked.connect(self._start_capture)
|
|
|
+
|
|
|
+ def set_shortcut(self, sequence: str) -> None:
|
|
|
+ """Set the current shortcut display."""
|
|
|
+ self._current_sequence = sequence
|
|
|
+ if sequence:
|
|
|
+ self.setText(QKeySequence(sequence).toString())
|
|
|
+ else:
|
|
|
+ self.setText("点击设置快捷键")
|
|
|
+
|
|
|
+ def _start_capture(self) -> None:
|
|
|
+ """Start capturing keyboard input."""
|
|
|
+ self._capturing = True
|
|
|
+ self._pressed_keys.clear()
|
|
|
+ self.setText("按下快捷键...")
|
|
|
+ self.grabKeyboard()
|
|
|
+ self.setFocus()
|
|
|
+
|
|
|
+ def _stop_capture(self) -> None:
|
|
|
+ """Stop capturing and emit the captured shortcut."""
|
|
|
+ self._capturing = False
|
|
|
+ self.releaseKeyboard()
|
|
|
+
|
|
|
+ if self._pressed_keys:
|
|
|
+ # Create key sequence from pressed keys
|
|
|
+ modifiers = 0
|
|
|
+ key = 0
|
|
|
+
|
|
|
+ for k in self._pressed_keys:
|
|
|
+ if int(k) & (Qt.Key.Key_ShiftModifier | Qt.Key.Key_ControlModifier |
|
|
|
+ Qt.Key.Key_AltModifier | Qt.Key.Key_MetaModifier):
|
|
|
+ modifiers |= int(k)
|
|
|
+ else:
|
|
|
+ key = int(k) & ~int(Qt.Key.Key.KeypadModifier)
|
|
|
+
|
|
|
+ if key:
|
|
|
+ sequence = QKeySequence(modifiers | key).toString()
|
|
|
+ self._current_sequence = sequence
|
|
|
+ self.setText(sequence)
|
|
|
+ self.shortcut_captured.emit(sequence)
|
|
|
+ else:
|
|
|
+ self.setText("点击设置快捷键")
|
|
|
+ else:
|
|
|
+ self.setText("点击设置快捷键")
|
|
|
+
|
|
|
+ self._pressed_keys.clear()
|
|
|
+
|
|
|
+ def keyPressEvent(self, event) -> None:
|
|
|
+ """Handle key press event during capture."""
|
|
|
+ if self._capturing:
|
|
|
+ key = event.key()
|
|
|
+ modifiers = event.modifiers()
|
|
|
+
|
|
|
+ # Ignore modifier-only presses
|
|
|
+ if key in (Qt.Key.Key_Shift, Qt.Key.Key_Control, Qt.Key.Key_Alt, Qt.Key.Key_Meta):
|
|
|
+ return
|
|
|
+
|
|
|
+ # Build the sequence
|
|
|
+ self._pressed_keys.append(modifiers | key)
|
|
|
+ self._stop_capture()
|
|
|
+ else:
|
|
|
+ super().keyPressEvent(event)
|
|
|
+
|
|
|
+ def keyReleaseEvent(self, event) -> None:
|
|
|
+ """Handle key release event."""
|
|
|
+ if self._capturing:
|
|
|
+ event.accept()
|
|
|
+ else:
|
|
|
+ super().keyReleaseEvent(event)
|
|
|
+
|
|
|
+
|
|
|
+class ShortcutsDialog(QDialog):
|
|
|
+ """
|
|
|
+ Dialog for configuring keyboard shortcuts.
|
|
|
+ """
|
|
|
+
|
|
|
+ shortcuts_changed = pyqtSignal()
|
|
|
+
|
|
|
+ def __init__(self, manager: ShortcutsManager, parent: Optional[QWidget] = None) -> None:
|
|
|
+ """Initialize the dialog."""
|
|
|
+ super().__init__(parent)
|
|
|
+
|
|
|
+ self._manager = manager
|
|
|
+ self._model: Optional[ShortcutsModel] = None
|
|
|
+ self._selected_entry: Optional[ShortcutEntry] = None
|
|
|
+
|
|
|
+ self._setup_ui()
|
|
|
+
|
|
|
+ def _setup_ui(self) -> None:
|
|
|
+ """Set up the dialog UI."""
|
|
|
+ self.setWindowTitle("快捷键配置")
|
|
|
+ self.setMinimumSize(600, 500)
|
|
|
+
|
|
|
+ layout = QVBoxLayout(self)
|
|
|
+
|
|
|
+ # Instructions
|
|
|
+ info_label = QLabel(
|
|
|
+ "双击表格中的快捷键列可以自定义快捷键。"
|
|
|
+ "点击"恢复默认"可以重置选中的快捷键。"
|
|
|
+ )
|
|
|
+ info_label.setWordWrap(True)
|
|
|
+ info_label.setStyleSheet("color: #666; padding: 4px;")
|
|
|
+ layout.addWidget(info_label)
|
|
|
+
|
|
|
+ # Filter by category
|
|
|
+ filter_layout = QHBoxLayout()
|
|
|
+ filter_layout.addWidget(QLabel("筛选:"))
|
|
|
+ self._category_combo = QComboBox()
|
|
|
+ self._category_combo.addItem("全部")
|
|
|
+ self._category_combo.currentIndexChanged.connect(self._on_category_changed)
|
|
|
+ filter_layout.addWidget(self._category_combo)
|
|
|
+ filter_layout.addStretch()
|
|
|
+ layout.addLayout(filter_layout)
|
|
|
+
|
|
|
+ # Shortcuts table
|
|
|
+ self._table = QTableView()
|
|
|
+ self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
|
+ self._table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
|
+ self._table.setAlternatingRowColors(True)
|
|
|
+ self._table.horizontalHeader().setStretchLastSection(True)
|
|
|
+ layout.addWidget(self._table)
|
|
|
+
|
|
|
+ # Create and populate model
|
|
|
+ entries = self._manager.get_all_entries()
|
|
|
+ self._all_entries = entries
|
|
|
+ self._model = ShortcutsModel(entries, self)
|
|
|
+ self._table.setModel(self._model)
|
|
|
+
|
|
|
+ # Populate categories
|
|
|
+ categories = sorted(set(e.category for e in entries))
|
|
|
+ for cat in categories:
|
|
|
+ self._category_combo.addItem(cat)
|
|
|
+
|
|
|
+ # Editor panel
|
|
|
+ editor_group = QGroupBox("编辑快捷键")
|
|
|
+ editor_layout = QFormLayout(editor_group)
|
|
|
+
|
|
|
+ self._action_label = QLabel("-")
|
|
|
+ self._action_label.setFont(QFont("", 10, QFont.Weight.Bold))
|
|
|
+ editor_layout.addRow("操作:", self._action_label)
|
|
|
+
|
|
|
+ self._capture_btn = ShortcutCaptureButton()
|
|
|
+ self._capture_btn.shortcut_captured.connect(self._on_shortcut_captured)
|
|
|
+ self._capture_btn.setEnabled(False)
|
|
|
+ editor_layout.addRow("快捷键:", self._capture_btn)
|
|
|
+
|
|
|
+ self._default_label = QLabel("-")
|
|
|
+ editor_layout.addRow("默认:", self._default_label)
|
|
|
+
|
|
|
+ layout.addWidget(editor_group)
|
|
|
+
|
|
|
+ # Buttons
|
|
|
+ button_layout = QHBoxLayout()
|
|
|
+ button_layout.addStretch()
|
|
|
+
|
|
|
+ self._reset_btn = QPushButton("恢复默认")
|
|
|
+ self._reset_btn.setEnabled(False)
|
|
|
+ self._reset_btn.clicked.connect(self._on_reset_to_default)
|
|
|
+ button_layout.addWidget(self._reset_btn)
|
|
|
+
|
|
|
+ reset_all_btn = QPushButton("全部恢复默认")
|
|
|
+ reset_all_btn.clicked.connect(self._on_reset_all)
|
|
|
+ button_layout.addWidget(reset_all_btn)
|
|
|
+
|
|
|
+ self._ok_btn = QPushButton("确定")
|
|
|
+ self._ok_btn.clicked.connect(self.accept)
|
|
|
+ button_layout.addWidget(self._ok_btn)
|
|
|
+
|
|
|
+ layout.addLayout(button_layout)
|
|
|
+
|
|
|
+ # Connect selection
|
|
|
+ self._table.selectionModel().selectionChanged.connect(self._on_selection_changed)
|
|
|
+
|
|
|
+ def _on_selection_changed(self) -> None:
|
|
|
+ """Handle table selection change."""
|
|
|
+ selected = self._table.selectionModel().selectedRows()
|
|
|
+ if selected:
|
|
|
+ row = selected[0].row()
|
|
|
+ entry = self._model.get_entry_at_row(row)
|
|
|
+ if entry:
|
|
|
+ self._selected_entry = entry
|
|
|
+ self._action_label.setText(entry.name)
|
|
|
+ self._default_label.setText(entry.default_key_sequence)
|
|
|
+ self._capture_btn.set_shortcut(entry.key_sequence)
|
|
|
+ self._capture_btn.setEnabled(True)
|
|
|
+ self._reset_btn.setEnabled(entry.is_modified)
|
|
|
+ else:
|
|
|
+ self._selected_entry = None
|
|
|
+ self._action_label.setText("-")
|
|
|
+ self._default_label.setText("-")
|
|
|
+ self._capture_btn.setText("点击设置快捷键")
|
|
|
+ self._capture_btn.setEnabled(False)
|
|
|
+ self._reset_btn.setEnabled(False)
|
|
|
+
|
|
|
+ def _on_shortcut_captured(self, sequence: str) -> None:
|
|
|
+ """Handle shortcut capture."""
|
|
|
+ if self._selected_entry:
|
|
|
+ success = self._manager.set_shortcut(self._selected_entry.action, sequence)
|
|
|
+ if success:
|
|
|
+ # Update model
|
|
|
+ self._model = ShortcutsModel(self._manager.get_all_entries(), self)
|
|
|
+ self._table.setModel(self._model)
|
|
|
+ self._on_category_changed() # Reapply filter
|
|
|
+ self._reset_btn.setEnabled(self._selected_entry.is_modified)
|
|
|
+ self.shortcuts_changed.emit()
|
|
|
+ else:
|
|
|
+ QMessageBox.warning(
|
|
|
+ self,
|
|
|
+ "快捷键冲突",
|
|
|
+ f"快捷键「{sequence}」已被其他操作使用,\n"
|
|
|
+ "请选择其他快捷键。"
|
|
|
+ )
|
|
|
+ self._capture_btn.set_shortcut(self._selected_entry.key_sequence)
|
|
|
+
|
|
|
+ def _on_reset_to_default(self) -> None:
|
|
|
+ """Handle reset to default button."""
|
|
|
+ if self._selected_entry:
|
|
|
+ self._manager.reset_action(self._selected_entry.action)
|
|
|
+ self._model = ShortcutsModel(self._manager.get_all_entries(), self)
|
|
|
+ self._table.setModel(self._model)
|
|
|
+ self._on_category_changed()
|
|
|
+ self._capture_btn.set_shortcut(self._selected_entry.key_sequence)
|
|
|
+ self._reset_btn.setEnabled(False)
|
|
|
+ self.shortcuts_changed.emit()
|
|
|
+
|
|
|
+ def _on_reset_all(self) -> None:
|
|
|
+ """Handle reset all button."""
|
|
|
+ reply = QMessageBox.question(
|
|
|
+ self,
|
|
|
+ "确认重置",
|
|
|
+ "确定要将所有快捷键恢复为默认值吗?",
|
|
|
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
|
+ QMessageBox.StandardButton.No
|
|
|
+ )
|
|
|
+
|
|
|
+ if reply == QMessageBox.StandardButton.Yes:
|
|
|
+ self._manager.reset_to_defaults()
|
|
|
+ self._model = ShortcutsModel(self._manager.get_all_entries(), self)
|
|
|
+ self._table.setModel(self._model)
|
|
|
+ self._on_category_changed()
|
|
|
+ if self._selected_entry:
|
|
|
+ entry = self._manager.get_all_entries()[
|
|
|
+ list(StandardAction).index(self._selected_entry.action)
|
|
|
+ ]
|
|
|
+ self._capture_btn.set_shortcut(entry.key_sequence)
|
|
|
+ self._reset_btn.setEnabled(False)
|
|
|
+ self.shortcuts_changed.emit()
|
|
|
+
|
|
|
+ def _on_category_changed(self) -> None:
|
|
|
+ """Handle category filter change."""
|
|
|
+ category = self._category_combo.currentText()
|
|
|
+
|
|
|
+ if category == "全部":
|
|
|
+ filtered = self._all_entries
|
|
|
+ else:
|
|
|
+ filtered = [e for e in self._all_entries if e.category == category]
|
|
|
+
|
|
|
+ self._model = ShortcutsModel(filtered, self)
|
|
|
+ self._table.setModel(self._model)
|
|
|
+
|
|
|
+
|
|
|
+# Singleton instance
|
|
|
+_shortcuts_manager_instance: Optional[ShortcutsManager] = None
|
|
|
+
|
|
|
+
|
|
|
+def get_shortcuts_manager() -> ShortcutsManager:
|
|
|
+ """Get the singleton shortcuts manager instance."""
|
|
|
+ global _shortcuts_manager_instance
|
|
|
+ if _shortcuts_manager_instance is None:
|
|
|
+ _shortcuts_manager_instance = ShortcutsManager()
|
|
|
+ return _shortcuts_manager_instance
|