lazylabel-gui 1.1.5__py3-none-any.whl → 1.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,416 +1,417 @@
1
- """Hotkey configuration dialog."""
2
-
3
- from PyQt6.QtCore import Qt, QTimer, pyqtSignal
4
- from PyQt6.QtGui import QColor, QFont, QKeySequence
5
- from PyQt6.QtWidgets import (
6
- QDialog,
7
- QHBoxLayout,
8
- QHeaderView,
9
- QLabel,
10
- QLineEdit,
11
- QMessageBox,
12
- QPushButton,
13
- QTableWidget,
14
- QTableWidgetItem,
15
- QTabWidget,
16
- QVBoxLayout,
17
- QWidget,
18
- )
19
-
20
- from ..config import HotkeyAction, HotkeyManager
21
-
22
-
23
- class HotkeyLineEdit(QLineEdit):
24
- """Custom line edit that captures key sequences."""
25
-
26
- key_captured = pyqtSignal(str)
27
-
28
- def __init__(self, parent=None):
29
- super().__init__(parent)
30
- self.setReadOnly(True)
31
- self.setPlaceholderText("Click and press a key (Esc to cancel)")
32
- self.capturing = False
33
- self.original_style = self.styleSheet()
34
-
35
- # Timeout timer to auto-cancel capture
36
- self.timeout_timer = QTimer()
37
- self.timeout_timer.setSingleShot(True)
38
- self.timeout_timer.timeout.connect(self._timeout_capture)
39
- self.timeout_duration = 15000 # 15 seconds
40
-
41
- def mousePressEvent(self, event):
42
- """Start capturing keys when clicked."""
43
- if event.button() == Qt.MouseButton.LeftButton:
44
- self.start_capture()
45
- super().mousePressEvent(event)
46
-
47
- def start_capture(self):
48
- """Start capturing key input."""
49
- self.capturing = True
50
- self.setText("Press a key (Esc to cancel)")
51
- self.setStyleSheet("background-color: #ffeb3b; color: black;")
52
- self.setFocus()
53
- self.timeout_timer.start(self.timeout_duration)
54
-
55
- def stop_capture(self):
56
- """Stop capturing key input."""
57
- self.capturing = False
58
- self.setStyleSheet(self.original_style)
59
- self.clearFocus()
60
- self.timeout_timer.stop()
61
-
62
- def keyPressEvent(self, event):
63
- """Capture key press events."""
64
- if not self.capturing:
65
- super().keyPressEvent(event)
66
- return
67
-
68
- # Handle Escape key to cancel capture
69
- if event.key() == Qt.Key.Key_Escape:
70
- self.setText("") # Clear the field
71
- self.stop_capture()
72
- return
73
-
74
- # Ignore modifier-only keys and other problematic keys
75
- ignored_keys = {
76
- Qt.Key.Key_Control,
77
- Qt.Key.Key_Shift,
78
- Qt.Key.Key_Alt,
79
- Qt.Key.Key_Meta,
80
- Qt.Key.Key_CapsLock,
81
- Qt.Key.Key_NumLock,
82
- Qt.Key.Key_ScrollLock,
83
- Qt.Key.Key_unknown,
84
- Qt.Key.Key_Tab,
85
- Qt.Key.Key_Backtab,
86
- }
87
-
88
- if event.key() in ignored_keys:
89
- return
90
-
91
- try:
92
- # Create key sequence - properly handle modifiers
93
- modifiers = event.modifiers()
94
- key = event.key()
95
-
96
- # Skip invalid keys
97
- if key == 0 or key == Qt.Key.Key_unknown:
98
- return
99
-
100
- # Convert modifiers to int and combine with key
101
- modifier_int = (
102
- int(modifiers.value) if hasattr(modifiers, "value") else int(modifiers)
103
- )
104
- key_combination = key | modifier_int
105
-
106
- key_sequence = QKeySequence(key_combination)
107
- key_string = key_sequence.toString()
108
-
109
- # Only accept valid, non-empty key strings
110
- if key_string and key_string.strip():
111
- self.setText(key_string)
112
- self.key_captured.emit(key_string)
113
- self.stop_capture()
114
- else:
115
- # Invalid key combination, just ignore
116
- return
117
-
118
- except Exception as e:
119
- print(f"Error capturing key sequence: {e}")
120
- # Cancel capture on any error
121
- self.setText("")
122
- self.stop_capture()
123
-
124
- def focusOutEvent(self, event):
125
- """Stop capturing when focus is lost."""
126
- if self.capturing:
127
- self.stop_capture()
128
- if not self.text() or self.text().startswith("Press a key"):
129
- self.setText("")
130
- super().focusOutEvent(event)
131
-
132
- def _timeout_capture(self):
133
- """Handle capture timeout."""
134
- if self.capturing:
135
- self.setText("")
136
- self.stop_capture()
137
-
138
-
139
- class HotkeyDialog(QDialog):
140
- """Dialog for configuring hotkeys."""
141
-
142
- def __init__(self, hotkey_manager: HotkeyManager, parent=None):
143
- super().__init__(parent)
144
- self.hotkey_manager = hotkey_manager
145
- self.modified = False
146
- self.key_widgets = {} # Maps (action_name, key_type) to widget
147
-
148
- self.setWindowTitle("Hotkey Configuration")
149
- self.setModal(True)
150
- self.resize(800, 600)
151
-
152
- self._setup_ui()
153
- self._populate_hotkeys()
154
-
155
- def _setup_ui(self):
156
- """Setup the dialog UI."""
157
- layout = QVBoxLayout(self)
158
-
159
- # Title
160
- title = QLabel("Hotkey Configuration")
161
- title_font = QFont()
162
- title_font.setPointSize(16)
163
- title_font.setBold(True)
164
- title.setFont(title_font)
165
- title.setAlignment(Qt.AlignmentFlag.AlignCenter)
166
- layout.addWidget(title)
167
-
168
- # Instructions
169
- instructions = QLabel(
170
- "Click on a hotkey field and press the desired key combination. "
171
- "Mouse-related actions cannot be modified."
172
- )
173
- instructions.setWordWrap(True)
174
- instructions.setStyleSheet("color: #666; margin: 10px;")
175
- layout.addWidget(instructions)
176
-
177
- # Tab widget for categories
178
- self.tab_widget = QTabWidget()
179
- layout.addWidget(self.tab_widget)
180
-
181
- # Button layout
182
- button_layout = QHBoxLayout()
183
-
184
- # Save button
185
- self.save_button = QPushButton("Save Hotkeys")
186
- self.save_button.setToolTip(
187
- "Save hotkeys to file for persistence between sessions"
188
- )
189
- self.save_button.clicked.connect(self._save_hotkeys)
190
- button_layout.addWidget(self.save_button)
191
-
192
- # Defaults button
193
- self.defaults_button = QPushButton("Reset to Defaults")
194
- self.defaults_button.setToolTip("Reset all hotkeys to default values")
195
- self.defaults_button.clicked.connect(self._reset_to_defaults)
196
- button_layout.addWidget(self.defaults_button)
197
-
198
- button_layout.addStretch()
199
-
200
- # Close button
201
- self.close_button = QPushButton("Close")
202
- self.close_button.clicked.connect(self.accept)
203
- button_layout.addWidget(self.close_button)
204
-
205
- layout.addLayout(button_layout)
206
-
207
- def _create_category_tab(
208
- self, category_name: str, actions: list[HotkeyAction]
209
- ) -> QWidget:
210
- """Create a tab for a category of hotkeys."""
211
- widget = QWidget()
212
- layout = QVBoxLayout(widget)
213
-
214
- # Create table
215
- table = QTableWidget()
216
- table.setColumnCount(4)
217
- table.setHorizontalHeaderLabels(
218
- ["Action", "Description", "Primary Key", "Secondary Key"]
219
- )
220
- table.setRowCount(len(actions))
221
-
222
- # Configure table
223
- header = table.horizontalHeader()
224
- header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
225
- header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
226
- header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
227
- header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
228
-
229
- table.setAlternatingRowColors(True)
230
- table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
231
-
232
- # Populate table
233
- for row, action in enumerate(actions):
234
- # Action name
235
- name_item = QTableWidgetItem(action.name.replace("_", " ").title())
236
- name_item.setFlags(name_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
237
- table.setItem(row, 0, name_item)
238
-
239
- # Description
240
- desc_item = QTableWidgetItem(action.description)
241
- desc_item.setFlags(desc_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
242
- table.setItem(row, 1, desc_item)
243
-
244
- # Primary key
245
- primary_edit = HotkeyLineEdit()
246
- primary_edit.setText(action.primary_key or "")
247
- primary_edit.setEnabled(not action.mouse_related)
248
- if action.mouse_related:
249
- primary_edit.setStyleSheet("background-color: #f0f0f0; color: #666;")
250
- primary_edit.setToolTip("Mouse-related actions cannot be modified")
251
- else:
252
- primary_edit.key_captured.connect(
253
- lambda key, name=action.name: self._update_primary_key(name, key)
254
- )
255
- table.setCellWidget(row, 2, primary_edit)
256
- self.key_widgets[(action.name, "primary")] = primary_edit
257
-
258
- # Secondary key
259
- secondary_edit = HotkeyLineEdit()
260
- secondary_edit.setText(action.secondary_key or "")
261
- secondary_edit.setEnabled(not action.mouse_related)
262
- if action.mouse_related:
263
- secondary_edit.setStyleSheet("background-color: #f0f0f0; color: #666;")
264
- secondary_edit.setToolTip("Mouse-related actions cannot be modified")
265
- else:
266
- secondary_edit.key_captured.connect(
267
- lambda key, name=action.name: self._update_secondary_key(name, key)
268
- )
269
- table.setCellWidget(row, 3, secondary_edit)
270
- self.key_widgets[(action.name, "secondary")] = secondary_edit
271
-
272
- # Style mouse-related rows
273
- if action.mouse_related:
274
- for col in range(4):
275
- item = table.item(row, col)
276
- if item:
277
- item.setBackground(QColor("#f8f8f8"))
278
-
279
- layout.addWidget(table)
280
- return widget
281
-
282
- def _populate_hotkeys(self):
283
- """Populate the hotkey tabs."""
284
- categories = self.hotkey_manager.get_actions_by_category()
285
-
286
- # Define tab order
287
- tab_order = [
288
- "Modes",
289
- "Actions",
290
- "Navigation",
291
- "Segments",
292
- "View",
293
- "Movement",
294
- "Mouse",
295
- "General",
296
- ]
297
-
298
- for category in tab_order:
299
- if category in categories:
300
- tab_widget = self._create_category_tab(category, categories[category])
301
- self.tab_widget.addTab(tab_widget, category)
302
-
303
- # Add any remaining categories
304
- for category, actions in categories.items():
305
- if category not in tab_order:
306
- tab_widget = self._create_category_tab(category, actions)
307
- self.tab_widget.addTab(tab_widget, category)
308
-
309
- def _update_primary_key(self, action_name: str, key: str):
310
- """Update primary key for an action."""
311
- # Check for conflicts
312
- conflict = self.hotkey_manager.is_key_in_use(key, exclude_action=action_name)
313
- if conflict:
314
- QMessageBox.warning(
315
- self,
316
- "Key Conflict",
317
- f"The key '{key}' is already used by '{conflict.replace('_', ' ').title()}'. "
318
- "Please choose a different key.",
319
- )
320
- # Reset the field
321
- widget = self.key_widgets.get((action_name, "primary"))
322
- if widget:
323
- action = self.hotkey_manager.get_action(action_name)
324
- widget.setText(action.primary_key if action else "")
325
- return
326
-
327
- # Update the hotkey
328
- if self.hotkey_manager.set_primary_key(action_name, key):
329
- self.modified = True
330
-
331
- def _update_secondary_key(self, action_name: str, key: str):
332
- """Update secondary key for an action."""
333
- # Allow empty key for secondary
334
- if not key:
335
- self.hotkey_manager.set_secondary_key(action_name, None)
336
- self.modified = True
337
- return
338
-
339
- # Check for conflicts
340
- conflict = self.hotkey_manager.is_key_in_use(key, exclude_action=action_name)
341
- if conflict:
342
- QMessageBox.warning(
343
- self,
344
- "Key Conflict",
345
- f"The key '{key}' is already used by '{conflict.replace('_', ' ').title()}'. "
346
- "Please choose a different key.",
347
- )
348
- # Reset the field
349
- widget = self.key_widgets.get((action_name, "secondary"))
350
- if widget:
351
- action = self.hotkey_manager.get_action(action_name)
352
- widget.setText(action.secondary_key or "")
353
- return
354
-
355
- # Update the hotkey
356
- if self.hotkey_manager.set_secondary_key(action_name, key):
357
- self.modified = True
358
-
359
- def _save_hotkeys(self):
360
- """Save hotkeys to file."""
361
- try:
362
- self.hotkey_manager.save_hotkeys()
363
- QMessageBox.information(
364
- self,
365
- "Hotkeys Saved",
366
- "Hotkeys have been saved and will persist between sessions.",
367
- )
368
- except Exception as e:
369
- QMessageBox.critical(
370
- self, "Save Error", f"Failed to save hotkeys: {str(e)}"
371
- )
372
-
373
- def _reset_to_defaults(self):
374
- """Reset all hotkeys to defaults."""
375
- reply = QMessageBox.question(
376
- self,
377
- "Reset Hotkeys",
378
- "Are you sure you want to reset all hotkeys to their default values?",
379
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
380
- QMessageBox.StandardButton.No,
381
- )
382
-
383
- if reply == QMessageBox.StandardButton.Yes:
384
- self.hotkey_manager.reset_to_defaults()
385
- self.modified = True
386
-
387
- # Update all widgets
388
- for (action_name, key_type), widget in self.key_widgets.items():
389
- action = self.hotkey_manager.get_action(action_name)
390
- if action:
391
- if key_type == "primary":
392
- widget.setText(action.primary_key or "")
393
- else:
394
- widget.setText(action.secondary_key or "")
395
-
396
- def closeEvent(self, event):
397
- """Handle dialog close."""
398
- if self.modified:
399
- reply = QMessageBox.question(
400
- self,
401
- "Unsaved Changes",
402
- "You have unsaved hotkey changes. Do you want to apply them for this session?",
403
- QMessageBox.StandardButton.Yes
404
- | QMessageBox.StandardButton.No
405
- | QMessageBox.StandardButton.Cancel,
406
- QMessageBox.StandardButton.Yes,
407
- )
408
-
409
- if reply == QMessageBox.StandardButton.Cancel:
410
- event.ignore()
411
- return
412
- elif reply == QMessageBox.StandardButton.No:
413
- # Reload from file to discard changes
414
- self.hotkey_manager.load_hotkeys()
415
-
416
- super().closeEvent(event)
1
+ """Hotkey configuration dialog."""
2
+
3
+ from PyQt6.QtCore import Qt, QTimer, pyqtSignal
4
+ from PyQt6.QtGui import QColor, QFont, QKeySequence
5
+ from PyQt6.QtWidgets import (
6
+ QDialog,
7
+ QHBoxLayout,
8
+ QHeaderView,
9
+ QLabel,
10
+ QLineEdit,
11
+ QMessageBox,
12
+ QPushButton,
13
+ QTableWidget,
14
+ QTableWidgetItem,
15
+ QTabWidget,
16
+ QVBoxLayout,
17
+ QWidget,
18
+ )
19
+
20
+ from ..config import HotkeyAction, HotkeyManager
21
+ from ..utils.logger import logger
22
+
23
+
24
+ class HotkeyLineEdit(QLineEdit):
25
+ """Custom line edit that captures key sequences."""
26
+
27
+ key_captured = pyqtSignal(str)
28
+
29
+ def __init__(self, parent=None):
30
+ super().__init__(parent)
31
+ self.setReadOnly(True)
32
+ self.setPlaceholderText("Click and press a key (Esc to cancel)")
33
+ self.capturing = False
34
+ self.original_style = self.styleSheet()
35
+
36
+ # Timeout timer to auto-cancel capture
37
+ self.timeout_timer = QTimer()
38
+ self.timeout_timer.setSingleShot(True)
39
+ self.timeout_timer.timeout.connect(self._timeout_capture)
40
+ self.timeout_duration = 15000 # 15 seconds
41
+
42
+ def mousePressEvent(self, event):
43
+ """Start capturing keys when clicked."""
44
+ if event.button() == Qt.MouseButton.LeftButton:
45
+ self.start_capture()
46
+ super().mousePressEvent(event)
47
+
48
+ def start_capture(self):
49
+ """Start capturing key input."""
50
+ self.capturing = True
51
+ self.setText("Press a key (Esc to cancel)")
52
+ self.setStyleSheet("background-color: #ffeb3b; color: black;")
53
+ self.setFocus()
54
+ self.timeout_timer.start(self.timeout_duration)
55
+
56
+ def stop_capture(self):
57
+ """Stop capturing key input."""
58
+ self.capturing = False
59
+ self.setStyleSheet(self.original_style)
60
+ self.clearFocus()
61
+ self.timeout_timer.stop()
62
+
63
+ def keyPressEvent(self, event):
64
+ """Capture key press events."""
65
+ if not self.capturing:
66
+ super().keyPressEvent(event)
67
+ return
68
+
69
+ # Handle Escape key to cancel capture
70
+ if event.key() == Qt.Key.Key_Escape:
71
+ self.setText("") # Clear the field
72
+ self.stop_capture()
73
+ return
74
+
75
+ # Ignore modifier-only keys and other problematic keys
76
+ ignored_keys = {
77
+ Qt.Key.Key_Control,
78
+ Qt.Key.Key_Shift,
79
+ Qt.Key.Key_Alt,
80
+ Qt.Key.Key_Meta,
81
+ Qt.Key.Key_CapsLock,
82
+ Qt.Key.Key_NumLock,
83
+ Qt.Key.Key_ScrollLock,
84
+ Qt.Key.Key_unknown,
85
+ Qt.Key.Key_Tab,
86
+ Qt.Key.Key_Backtab,
87
+ }
88
+
89
+ if event.key() in ignored_keys:
90
+ return
91
+
92
+ try:
93
+ # Create key sequence - properly handle modifiers
94
+ modifiers = event.modifiers()
95
+ key = event.key()
96
+
97
+ # Skip invalid keys
98
+ if key == 0 or key == Qt.Key.Key_unknown:
99
+ return
100
+
101
+ # Convert modifiers to int and combine with key
102
+ modifier_int = (
103
+ int(modifiers.value) if hasattr(modifiers, "value") else int(modifiers)
104
+ )
105
+ key_combination = key | modifier_int
106
+
107
+ key_sequence = QKeySequence(key_combination)
108
+ key_string = key_sequence.toString()
109
+
110
+ # Only accept valid, non-empty key strings
111
+ if key_string and key_string.strip():
112
+ self.setText(key_string)
113
+ self.key_captured.emit(key_string)
114
+ self.stop_capture()
115
+ else:
116
+ # Invalid key combination, just ignore
117
+ return
118
+
119
+ except Exception as e:
120
+ logger.error(f"Error capturing key sequence: {e}")
121
+ # Cancel capture on any error
122
+ self.setText("")
123
+ self.stop_capture()
124
+
125
+ def focusOutEvent(self, event):
126
+ """Stop capturing when focus is lost."""
127
+ if self.capturing:
128
+ self.stop_capture()
129
+ if not self.text() or self.text().startswith("Press a key"):
130
+ self.setText("")
131
+ super().focusOutEvent(event)
132
+
133
+ def _timeout_capture(self):
134
+ """Handle capture timeout."""
135
+ if self.capturing:
136
+ self.setText("")
137
+ self.stop_capture()
138
+
139
+
140
+ class HotkeyDialog(QDialog):
141
+ """Dialog for configuring hotkeys."""
142
+
143
+ def __init__(self, hotkey_manager: HotkeyManager, parent=None):
144
+ super().__init__(parent)
145
+ self.hotkey_manager = hotkey_manager
146
+ self.modified = False
147
+ self.key_widgets = {} # Maps (action_name, key_type) to widget
148
+
149
+ self.setWindowTitle("Hotkey Configuration")
150
+ self.setModal(True)
151
+ self.resize(800, 600)
152
+
153
+ self._setup_ui()
154
+ self._populate_hotkeys()
155
+
156
+ def _setup_ui(self):
157
+ """Setup the dialog UI."""
158
+ layout = QVBoxLayout(self)
159
+
160
+ # Title
161
+ title = QLabel("Hotkey Configuration")
162
+ title_font = QFont()
163
+ title_font.setPointSize(16)
164
+ title_font.setBold(True)
165
+ title.setFont(title_font)
166
+ title.setAlignment(Qt.AlignmentFlag.AlignCenter)
167
+ layout.addWidget(title)
168
+
169
+ # Instructions
170
+ instructions = QLabel(
171
+ "Click on a hotkey field and press the desired key combination. "
172
+ "Mouse-related actions cannot be modified."
173
+ )
174
+ instructions.setWordWrap(True)
175
+ instructions.setStyleSheet("color: #666; margin: 10px;")
176
+ layout.addWidget(instructions)
177
+
178
+ # Tab widget for categories
179
+ self.tab_widget = QTabWidget()
180
+ layout.addWidget(self.tab_widget)
181
+
182
+ # Button layout
183
+ button_layout = QHBoxLayout()
184
+
185
+ # Save button
186
+ self.save_button = QPushButton("Save Hotkeys")
187
+ self.save_button.setToolTip(
188
+ "Save hotkeys to file for persistence between sessions"
189
+ )
190
+ self.save_button.clicked.connect(self._save_hotkeys)
191
+ button_layout.addWidget(self.save_button)
192
+
193
+ # Defaults button
194
+ self.defaults_button = QPushButton("Reset to Defaults")
195
+ self.defaults_button.setToolTip("Reset all hotkeys to default values")
196
+ self.defaults_button.clicked.connect(self._reset_to_defaults)
197
+ button_layout.addWidget(self.defaults_button)
198
+
199
+ button_layout.addStretch()
200
+
201
+ # Close button
202
+ self.close_button = QPushButton("Close")
203
+ self.close_button.clicked.connect(self.accept)
204
+ button_layout.addWidget(self.close_button)
205
+
206
+ layout.addLayout(button_layout)
207
+
208
+ def _create_category_tab(
209
+ self, category_name: str, actions: list[HotkeyAction]
210
+ ) -> QWidget:
211
+ """Create a tab for a category of hotkeys."""
212
+ widget = QWidget()
213
+ layout = QVBoxLayout(widget)
214
+
215
+ # Create table
216
+ table = QTableWidget()
217
+ table.setColumnCount(4)
218
+ table.setHorizontalHeaderLabels(
219
+ ["Action", "Description", "Primary Key", "Secondary Key"]
220
+ )
221
+ table.setRowCount(len(actions))
222
+
223
+ # Configure table
224
+ header = table.horizontalHeader()
225
+ header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
226
+ header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
227
+ header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
228
+ header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
229
+
230
+ table.setAlternatingRowColors(True)
231
+ table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
232
+
233
+ # Populate table
234
+ for row, action in enumerate(actions):
235
+ # Action name
236
+ name_item = QTableWidgetItem(action.name.replace("_", " ").title())
237
+ name_item.setFlags(name_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
238
+ table.setItem(row, 0, name_item)
239
+
240
+ # Description
241
+ desc_item = QTableWidgetItem(action.description)
242
+ desc_item.setFlags(desc_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
243
+ table.setItem(row, 1, desc_item)
244
+
245
+ # Primary key
246
+ primary_edit = HotkeyLineEdit()
247
+ primary_edit.setText(action.primary_key or "")
248
+ primary_edit.setEnabled(not action.mouse_related)
249
+ if action.mouse_related:
250
+ primary_edit.setStyleSheet("background-color: #f0f0f0; color: #666;")
251
+ primary_edit.setToolTip("Mouse-related actions cannot be modified")
252
+ else:
253
+ primary_edit.key_captured.connect(
254
+ lambda key, name=action.name: self._update_primary_key(name, key)
255
+ )
256
+ table.setCellWidget(row, 2, primary_edit)
257
+ self.key_widgets[(action.name, "primary")] = primary_edit
258
+
259
+ # Secondary key
260
+ secondary_edit = HotkeyLineEdit()
261
+ secondary_edit.setText(action.secondary_key or "")
262
+ secondary_edit.setEnabled(not action.mouse_related)
263
+ if action.mouse_related:
264
+ secondary_edit.setStyleSheet("background-color: #f0f0f0; color: #666;")
265
+ secondary_edit.setToolTip("Mouse-related actions cannot be modified")
266
+ else:
267
+ secondary_edit.key_captured.connect(
268
+ lambda key, name=action.name: self._update_secondary_key(name, key)
269
+ )
270
+ table.setCellWidget(row, 3, secondary_edit)
271
+ self.key_widgets[(action.name, "secondary")] = secondary_edit
272
+
273
+ # Style mouse-related rows
274
+ if action.mouse_related:
275
+ for col in range(4):
276
+ item = table.item(row, col)
277
+ if item:
278
+ item.setBackground(QColor("#f8f8f8"))
279
+
280
+ layout.addWidget(table)
281
+ return widget
282
+
283
+ def _populate_hotkeys(self):
284
+ """Populate the hotkey tabs."""
285
+ categories = self.hotkey_manager.get_actions_by_category()
286
+
287
+ # Define tab order
288
+ tab_order = [
289
+ "Modes",
290
+ "Actions",
291
+ "Navigation",
292
+ "Segments",
293
+ "View",
294
+ "Movement",
295
+ "Mouse",
296
+ "General",
297
+ ]
298
+
299
+ for category in tab_order:
300
+ if category in categories:
301
+ tab_widget = self._create_category_tab(category, categories[category])
302
+ self.tab_widget.addTab(tab_widget, category)
303
+
304
+ # Add any remaining categories
305
+ for category, actions in categories.items():
306
+ if category not in tab_order:
307
+ tab_widget = self._create_category_tab(category, actions)
308
+ self.tab_widget.addTab(tab_widget, category)
309
+
310
+ def _update_primary_key(self, action_name: str, key: str):
311
+ """Update primary key for an action."""
312
+ # Check for conflicts
313
+ conflict = self.hotkey_manager.is_key_in_use(key, exclude_action=action_name)
314
+ if conflict:
315
+ QMessageBox.warning(
316
+ self,
317
+ "Key Conflict",
318
+ f"The key '{key}' is already used by '{conflict.replace('_', ' ').title()}'. "
319
+ "Please choose a different key.",
320
+ )
321
+ # Reset the field
322
+ widget = self.key_widgets.get((action_name, "primary"))
323
+ if widget:
324
+ action = self.hotkey_manager.get_action(action_name)
325
+ widget.setText(action.primary_key if action else "")
326
+ return
327
+
328
+ # Update the hotkey
329
+ if self.hotkey_manager.set_primary_key(action_name, key):
330
+ self.modified = True
331
+
332
+ def _update_secondary_key(self, action_name: str, key: str):
333
+ """Update secondary key for an action."""
334
+ # Allow empty key for secondary
335
+ if not key:
336
+ self.hotkey_manager.set_secondary_key(action_name, None)
337
+ self.modified = True
338
+ return
339
+
340
+ # Check for conflicts
341
+ conflict = self.hotkey_manager.is_key_in_use(key, exclude_action=action_name)
342
+ if conflict:
343
+ QMessageBox.warning(
344
+ self,
345
+ "Key Conflict",
346
+ f"The key '{key}' is already used by '{conflict.replace('_', ' ').title()}'. "
347
+ "Please choose a different key.",
348
+ )
349
+ # Reset the field
350
+ widget = self.key_widgets.get((action_name, "secondary"))
351
+ if widget:
352
+ action = self.hotkey_manager.get_action(action_name)
353
+ widget.setText(action.secondary_key or "")
354
+ return
355
+
356
+ # Update the hotkey
357
+ if self.hotkey_manager.set_secondary_key(action_name, key):
358
+ self.modified = True
359
+
360
+ def _save_hotkeys(self):
361
+ """Save hotkeys to file."""
362
+ try:
363
+ self.hotkey_manager.save_hotkeys()
364
+ QMessageBox.information(
365
+ self,
366
+ "Hotkeys Saved",
367
+ "Hotkeys have been saved and will persist between sessions.",
368
+ )
369
+ except Exception as e:
370
+ QMessageBox.critical(
371
+ self, "Save Error", f"Failed to save hotkeys: {str(e)}"
372
+ )
373
+
374
+ def _reset_to_defaults(self):
375
+ """Reset all hotkeys to defaults."""
376
+ reply = QMessageBox.question(
377
+ self,
378
+ "Reset Hotkeys",
379
+ "Are you sure you want to reset all hotkeys to their default values?",
380
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
381
+ QMessageBox.StandardButton.No,
382
+ )
383
+
384
+ if reply == QMessageBox.StandardButton.Yes:
385
+ self.hotkey_manager.reset_to_defaults()
386
+ self.modified = True
387
+
388
+ # Update all widgets
389
+ for (action_name, key_type), widget in self.key_widgets.items():
390
+ action = self.hotkey_manager.get_action(action_name)
391
+ if action:
392
+ if key_type == "primary":
393
+ widget.setText(action.primary_key or "")
394
+ else:
395
+ widget.setText(action.secondary_key or "")
396
+
397
+ def closeEvent(self, event):
398
+ """Handle dialog close."""
399
+ if self.modified:
400
+ reply = QMessageBox.question(
401
+ self,
402
+ "Unsaved Changes",
403
+ "You have unsaved hotkey changes. Do you want to apply them for this session?",
404
+ QMessageBox.StandardButton.Yes
405
+ | QMessageBox.StandardButton.No
406
+ | QMessageBox.StandardButton.Cancel,
407
+ QMessageBox.StandardButton.Yes,
408
+ )
409
+
410
+ if reply == QMessageBox.StandardButton.Cancel:
411
+ event.ignore()
412
+ return
413
+ elif reply == QMessageBox.StandardButton.No:
414
+ # Reload from file to discard changes
415
+ self.hotkey_manager.load_hotkeys()
416
+
417
+ super().closeEvent(event)