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