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