lazylabel-gui 1.1.2__py3-none-any.whl → 1.1.4__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 (38) hide show
  1. lazylabel/__init__.py +9 -9
  2. lazylabel/config/__init__.py +7 -7
  3. lazylabel/config/hotkeys.py +96 -57
  4. lazylabel/config/paths.py +40 -41
  5. lazylabel/config/settings.py +65 -66
  6. lazylabel/core/__init__.py +7 -7
  7. lazylabel/core/file_manager.py +122 -106
  8. lazylabel/core/model_manager.py +95 -97
  9. lazylabel/core/segment_manager.py +183 -171
  10. lazylabel/main.py +37 -36
  11. lazylabel/models/__init__.py +5 -5
  12. lazylabel/models/sam_model.py +200 -195
  13. lazylabel/ui/__init__.py +8 -8
  14. lazylabel/ui/control_panel.py +245 -241
  15. lazylabel/ui/editable_vertex.py +64 -64
  16. lazylabel/ui/hotkey_dialog.py +416 -384
  17. lazylabel/ui/hoverable_pixelmap_item.py +22 -22
  18. lazylabel/ui/hoverable_polygon_item.py +38 -39
  19. lazylabel/ui/main_window.py +1860 -1659
  20. lazylabel/ui/numeric_table_widget_item.py +9 -9
  21. lazylabel/ui/photo_viewer.py +51 -54
  22. lazylabel/ui/reorderable_class_table.py +60 -61
  23. lazylabel/ui/right_panel.py +332 -315
  24. lazylabel/ui/widgets/__init__.py +8 -8
  25. lazylabel/ui/widgets/adjustments_widget.py +108 -108
  26. lazylabel/ui/widgets/model_selection_widget.py +101 -94
  27. lazylabel/ui/widgets/settings_widget.py +113 -106
  28. lazylabel/ui/widgets/status_bar.py +109 -109
  29. lazylabel/utils/__init__.py +6 -6
  30. lazylabel/utils/custom_file_system_model.py +133 -132
  31. lazylabel/utils/utils.py +12 -12
  32. {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.4.dist-info}/METADATA +243 -197
  33. lazylabel_gui-1.1.4.dist-info/RECORD +37 -0
  34. {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.4.dist-info}/licenses/LICENSE +21 -21
  35. lazylabel_gui-1.1.2.dist-info/RECORD +0 -37
  36. {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.4.dist-info}/WHEEL +0 -0
  37. {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.4.dist-info}/entry_points.txt +0 -0
  38. {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.4.dist-info}/top_level.txt +0 -0
@@ -1,241 +1,245 @@
1
- """Left control panel with mode controls and settings."""
2
-
3
- from PyQt6.QtWidgets import (
4
- QWidget,
5
- QVBoxLayout,
6
- QPushButton,
7
- QLabel,
8
- QFrame,
9
- QHBoxLayout,
10
- QCheckBox,
11
- QSlider,
12
- QGroupBox,
13
- QComboBox,
14
- )
15
- from PyQt6.QtCore import Qt, pyqtSignal
16
-
17
- from .widgets import ModelSelectionWidget, SettingsWidget, AdjustmentsWidget
18
-
19
-
20
- class ControlPanel(QWidget):
21
- """Left control panel with mode controls and settings."""
22
-
23
- # Signals
24
- sam_mode_requested = pyqtSignal()
25
- polygon_mode_requested = pyqtSignal()
26
- selection_mode_requested = pyqtSignal()
27
- clear_points_requested = pyqtSignal()
28
- fit_view_requested = pyqtSignal()
29
- browse_models_requested = pyqtSignal()
30
- refresh_models_requested = pyqtSignal()
31
- model_selected = pyqtSignal(str)
32
- annotation_size_changed = pyqtSignal(int)
33
- pan_speed_changed = pyqtSignal(int)
34
- join_threshold_changed = pyqtSignal(int)
35
- hotkeys_requested = pyqtSignal()
36
- pop_out_requested = pyqtSignal()
37
-
38
- def __init__(self, parent=None):
39
- super().__init__(parent)
40
- self.setMinimumWidth(50) # Allow collapsing but maintain minimum
41
- self.preferred_width = 250 # Store preferred width for expansion
42
- self._setup_ui()
43
- self._connect_signals()
44
-
45
- def _setup_ui(self):
46
- """Setup the UI layout."""
47
- layout = QVBoxLayout(self)
48
- layout.setAlignment(Qt.AlignmentFlag.AlignTop)
49
-
50
- # Top button row
51
- toggle_layout = QHBoxLayout()
52
- toggle_layout.addStretch()
53
-
54
- self.btn_popout = QPushButton("⋯")
55
- self.btn_popout.setToolTip("Pop out panel to separate window")
56
- self.btn_popout.setMaximumWidth(30)
57
- toggle_layout.addWidget(self.btn_popout)
58
-
59
- layout.addLayout(toggle_layout)
60
-
61
- # Main controls widget
62
- self.main_controls_widget = QWidget()
63
- main_layout = QVBoxLayout(self.main_controls_widget)
64
- main_layout.setContentsMargins(0, 0, 0, 0)
65
-
66
- # Mode label
67
- self.mode_label = QLabel("Mode: Points")
68
- font = self.mode_label.font()
69
- font.setPointSize(14)
70
- font.setBold(True)
71
- self.mode_label.setFont(font)
72
- main_layout.addWidget(self.mode_label)
73
-
74
- # Mode buttons
75
- self._add_mode_buttons(main_layout)
76
-
77
- # Separator
78
- main_layout.addSpacing(20)
79
- main_layout.addWidget(self._create_separator())
80
- main_layout.addSpacing(10)
81
-
82
- # Model selection
83
- self.model_widget = ModelSelectionWidget()
84
- main_layout.addWidget(self.model_widget)
85
-
86
- # Separator
87
- main_layout.addSpacing(10)
88
- main_layout.addWidget(self._create_separator())
89
- main_layout.addSpacing(10)
90
-
91
- # Action buttons
92
- self._add_action_buttons(main_layout)
93
- main_layout.addSpacing(10)
94
-
95
- # Settings
96
- self.settings_widget = SettingsWidget()
97
- main_layout.addWidget(self.settings_widget)
98
-
99
- # Adjustments
100
- self.adjustments_widget = AdjustmentsWidget()
101
- main_layout.addWidget(self.adjustments_widget)
102
-
103
- main_layout.addStretch()
104
-
105
- # Status labels
106
- self.notification_label = QLabel("")
107
- font = self.notification_label.font()
108
- font.setItalic(True)
109
- self.notification_label.setFont(font)
110
- self.notification_label.setStyleSheet("color: #ffa500;")
111
- self.notification_label.setWordWrap(True)
112
- main_layout.addWidget(self.notification_label)
113
-
114
- layout.addWidget(self.main_controls_widget)
115
-
116
- def _add_mode_buttons(self, layout):
117
- """Add mode control buttons."""
118
- self.btn_sam_mode = QPushButton("Point Mode (1)")
119
- self.btn_sam_mode.setToolTip("Switch to Point Mode for AI segmentation (1)")
120
-
121
- self.btn_polygon_mode = QPushButton("Polygon Mode (2)")
122
- self.btn_polygon_mode.setToolTip("Switch to Polygon Drawing Mode (2)")
123
-
124
- self.btn_selection_mode = QPushButton("Selection Mode (E)")
125
- self.btn_selection_mode.setToolTip("Toggle segment selection (E)")
126
-
127
- layout.addWidget(self.btn_sam_mode)
128
- layout.addWidget(self.btn_polygon_mode)
129
- layout.addWidget(self.btn_selection_mode)
130
-
131
- def _add_action_buttons(self, layout):
132
- """Add action buttons."""
133
- self.btn_fit_view = QPushButton("Fit View (.)")
134
- self.btn_fit_view.setToolTip("Reset image zoom and pan to fit the view (.)")
135
-
136
- self.btn_clear_points = QPushButton("Clear Clicks (C)")
137
- self.btn_clear_points.setToolTip("Clear current temporary points/vertices (C)")
138
-
139
- self.btn_hotkeys = QPushButton("Hotkeys")
140
- self.btn_hotkeys.setToolTip("Configure keyboard shortcuts")
141
-
142
- layout.addWidget(self.btn_fit_view)
143
- layout.addWidget(self.btn_clear_points)
144
- layout.addWidget(self.btn_hotkeys)
145
-
146
- def _create_separator(self):
147
- """Create a horizontal separator line."""
148
- line = QFrame()
149
- line.setFrameShape(QFrame.Shape.HLine)
150
- return line
151
-
152
- def _connect_signals(self):
153
- """Connect internal signals."""
154
- self.btn_sam_mode.clicked.connect(self.sam_mode_requested)
155
- self.btn_polygon_mode.clicked.connect(self.polygon_mode_requested)
156
- self.btn_selection_mode.clicked.connect(self.selection_mode_requested)
157
- self.btn_clear_points.clicked.connect(self.clear_points_requested)
158
- self.btn_fit_view.clicked.connect(self.fit_view_requested)
159
- self.btn_hotkeys.clicked.connect(self.hotkeys_requested)
160
- self.btn_popout.clicked.connect(self.pop_out_requested)
161
-
162
- # Model widget signals
163
- self.model_widget.browse_requested.connect(self.browse_models_requested)
164
- self.model_widget.refresh_requested.connect(self.refresh_models_requested)
165
- self.model_widget.model_selected.connect(self.model_selected)
166
-
167
- # Adjustments widget signals
168
- self.adjustments_widget.annotation_size_changed.connect(
169
- self.annotation_size_changed
170
- )
171
- self.adjustments_widget.pan_speed_changed.connect(self.pan_speed_changed)
172
- self.adjustments_widget.join_threshold_changed.connect(
173
- self.join_threshold_changed
174
- )
175
-
176
- def mouseDoubleClickEvent(self, event):
177
- """Handle double-click to expand collapsed panel."""
178
- if self.width() < 50: # If panel is collapsed
179
- # Request expansion by calling parent method
180
- if self.parent() and hasattr(self.parent(), "_expand_left_panel"):
181
- self.parent()._expand_left_panel()
182
- super().mouseDoubleClickEvent(event)
183
-
184
- def show_notification(self, message: str, duration: int = 3000):
185
- """Show a notification message."""
186
- self.notification_label.setText(message)
187
- # Note: Timer should be handled by the caller
188
-
189
- def clear_notification(self):
190
- """Clear the notification message."""
191
- self.notification_label.clear()
192
-
193
- def set_mode_text(self, mode: str):
194
- """Set the mode label text."""
195
- self.mode_label.setText(f"Mode: {mode.replace('_', ' ').title()}")
196
-
197
- # Delegate methods for sub-widgets
198
- def populate_models(self, models):
199
- """Populate the models combo box."""
200
- self.model_widget.populate_models(models)
201
-
202
- def set_current_model(self, model_name):
203
- """Set the current model display."""
204
- self.model_widget.set_current_model(model_name)
205
-
206
- def get_settings(self):
207
- """Get current settings from the settings widget."""
208
- return self.settings_widget.get_settings()
209
-
210
- def set_settings(self, settings):
211
- """Set settings in the settings widget."""
212
- self.settings_widget.set_settings(settings)
213
-
214
- def get_annotation_size(self):
215
- """Get current annotation size."""
216
- return self.adjustments_widget.get_annotation_size()
217
-
218
- def set_annotation_size(self, value):
219
- """Set annotation size."""
220
- self.adjustments_widget.set_annotation_size(value)
221
-
222
- def set_join_threshold(self, value):
223
- """Set join threshold."""
224
- self.adjustments_widget.set_join_threshold(value)
225
-
226
- def set_sam_mode_enabled(self, enabled: bool):
227
- """Enable or disable the SAM mode button."""
228
- self.btn_sam_mode.setEnabled(enabled)
229
- if not enabled:
230
- self.btn_sam_mode.setToolTip("Point Mode (SAM model not available)")
231
- else:
232
- self.btn_sam_mode.setToolTip("Switch to Point Mode for AI segmentation (1)")
233
-
234
- def set_popout_mode(self, is_popped_out: bool):
235
- """Update the pop-out button based on panel state."""
236
- if is_popped_out:
237
- self.btn_popout.setText("⇤")
238
- self.btn_popout.setToolTip("Return panel to main window")
239
- else:
240
- self.btn_popout.setText("⋯")
241
- self.btn_popout.setToolTip("Pop out panel to separate window")
1
+ """Left control panel with mode controls and settings."""
2
+
3
+ from PyQt6.QtCore import Qt, pyqtSignal
4
+ from PyQt6.QtWidgets import (
5
+ QFrame,
6
+ QHBoxLayout,
7
+ QLabel,
8
+ QPushButton,
9
+ QVBoxLayout,
10
+ QWidget,
11
+ )
12
+
13
+ from .widgets import AdjustmentsWidget, ModelSelectionWidget, SettingsWidget
14
+
15
+
16
+ class ControlPanel(QWidget):
17
+ """Left control panel with mode controls and settings."""
18
+
19
+ # Signals
20
+ sam_mode_requested = pyqtSignal()
21
+ polygon_mode_requested = pyqtSignal()
22
+ bbox_mode_requested = pyqtSignal() # New signal for bounding box mode
23
+ selection_mode_requested = pyqtSignal()
24
+ clear_points_requested = pyqtSignal()
25
+ fit_view_requested = pyqtSignal()
26
+ browse_models_requested = pyqtSignal()
27
+ refresh_models_requested = pyqtSignal()
28
+ model_selected = pyqtSignal(str)
29
+ annotation_size_changed = pyqtSignal(int)
30
+ pan_speed_changed = pyqtSignal(int)
31
+ join_threshold_changed = pyqtSignal(int)
32
+ hotkeys_requested = pyqtSignal()
33
+ pop_out_requested = pyqtSignal()
34
+
35
+ def __init__(self, parent=None):
36
+ super().__init__(parent)
37
+ self.setMinimumWidth(50) # Allow collapsing but maintain minimum
38
+ self.preferred_width = 250 # Store preferred width for expansion
39
+ self._setup_ui()
40
+ self._connect_signals()
41
+
42
+ def _setup_ui(self):
43
+ """Setup the UI layout."""
44
+ layout = QVBoxLayout(self)
45
+ layout.setAlignment(Qt.AlignmentFlag.AlignTop)
46
+
47
+ # Top button row
48
+ toggle_layout = QHBoxLayout()
49
+ toggle_layout.addStretch()
50
+
51
+ self.btn_popout = QPushButton("⋯")
52
+ self.btn_popout.setToolTip("Pop out panel to separate window")
53
+ self.btn_popout.setMaximumWidth(30)
54
+ toggle_layout.addWidget(self.btn_popout)
55
+
56
+ layout.addLayout(toggle_layout)
57
+
58
+ # Main controls widget
59
+ self.main_controls_widget = QWidget()
60
+ main_layout = QVBoxLayout(self.main_controls_widget)
61
+ main_layout.setContentsMargins(0, 0, 0, 0)
62
+
63
+ # Mode label
64
+ self.mode_label = QLabel("Mode: Points")
65
+ font = self.mode_label.font()
66
+ font.setPointSize(14)
67
+ font.setBold(True)
68
+ self.mode_label.setFont(font)
69
+ main_layout.addWidget(self.mode_label)
70
+
71
+ # Mode buttons
72
+ self._add_mode_buttons(main_layout)
73
+
74
+ # Separator
75
+ main_layout.addSpacing(20)
76
+ main_layout.addWidget(self._create_separator())
77
+ main_layout.addSpacing(10)
78
+
79
+ # Model selection
80
+ self.model_widget = ModelSelectionWidget()
81
+ main_layout.addWidget(self.model_widget)
82
+
83
+ # Separator
84
+ main_layout.addSpacing(10)
85
+ main_layout.addWidget(self._create_separator())
86
+ main_layout.addSpacing(10)
87
+
88
+ # Action buttons
89
+ self._add_action_buttons(main_layout)
90
+ main_layout.addSpacing(10)
91
+
92
+ # Settings
93
+ self.settings_widget = SettingsWidget()
94
+ main_layout.addWidget(self.settings_widget)
95
+
96
+ # Adjustments
97
+ self.adjustments_widget = AdjustmentsWidget()
98
+ main_layout.addWidget(self.adjustments_widget)
99
+
100
+ main_layout.addStretch()
101
+
102
+ # Status labels
103
+ self.notification_label = QLabel("")
104
+ font = self.notification_label.font()
105
+ font.setItalic(True)
106
+ self.notification_label.setFont(font)
107
+ self.notification_label.setStyleSheet("color: #ffa500;")
108
+ self.notification_label.setWordWrap(True)
109
+ main_layout.addWidget(self.notification_label)
110
+
111
+ layout.addWidget(self.main_controls_widget)
112
+
113
+ def _add_mode_buttons(self, layout):
114
+ """Add mode control buttons."""
115
+ self.btn_sam_mode = QPushButton("Point Mode (1)")
116
+ self.btn_sam_mode.setToolTip("Switch to Point Mode for AI segmentation (1)")
117
+
118
+ self.btn_polygon_mode = QPushButton("Polygon Mode (2)")
119
+ self.btn_polygon_mode.setToolTip("Switch to Polygon Drawing Mode (2)")
120
+
121
+ self.btn_bbox_mode = QPushButton("BBox Mode (3)")
122
+ self.btn_bbox_mode.setToolTip("Switch to Bounding Box Drawing Mode (3)")
123
+
124
+ self.btn_selection_mode = QPushButton("Selection Mode (E)")
125
+ self.btn_selection_mode.setToolTip("Toggle segment selection (E)")
126
+
127
+ layout.addWidget(self.btn_sam_mode)
128
+ layout.addWidget(self.btn_polygon_mode)
129
+ layout.addWidget(self.btn_bbox_mode)
130
+ layout.addWidget(self.btn_selection_mode)
131
+
132
+ def _add_action_buttons(self, layout):
133
+ """Add action buttons."""
134
+ self.btn_fit_view = QPushButton("Fit View (.)")
135
+ self.btn_fit_view.setToolTip("Reset image zoom and pan to fit the view (.)")
136
+
137
+ self.btn_clear_points = QPushButton("Clear Clicks (C)")
138
+ self.btn_clear_points.setToolTip("Clear current temporary points/vertices (C)")
139
+
140
+ self.btn_hotkeys = QPushButton("Hotkeys")
141
+ self.btn_hotkeys.setToolTip("Configure keyboard shortcuts")
142
+
143
+ layout.addWidget(self.btn_fit_view)
144
+ layout.addWidget(self.btn_clear_points)
145
+ layout.addWidget(self.btn_hotkeys)
146
+
147
+ def _create_separator(self):
148
+ """Create a horizontal separator line."""
149
+ line = QFrame()
150
+ line.setFrameShape(QFrame.Shape.HLine)
151
+ return line
152
+
153
+ def _connect_signals(self):
154
+ """Connect internal signals."""
155
+ self.btn_sam_mode.clicked.connect(self.sam_mode_requested)
156
+ self.btn_polygon_mode.clicked.connect(self.polygon_mode_requested)
157
+ self.btn_bbox_mode.clicked.connect(self.bbox_mode_requested)
158
+ self.btn_selection_mode.clicked.connect(self.selection_mode_requested)
159
+ self.btn_clear_points.clicked.connect(self.clear_points_requested)
160
+ self.btn_fit_view.clicked.connect(self.fit_view_requested)
161
+ self.btn_hotkeys.clicked.connect(self.hotkeys_requested)
162
+ self.btn_popout.clicked.connect(self.pop_out_requested)
163
+
164
+ # Model widget signals
165
+ self.model_widget.browse_requested.connect(self.browse_models_requested)
166
+ self.model_widget.refresh_requested.connect(self.refresh_models_requested)
167
+ self.model_widget.model_selected.connect(self.model_selected)
168
+
169
+ # Adjustments widget signals
170
+ self.adjustments_widget.annotation_size_changed.connect(
171
+ self.annotation_size_changed
172
+ )
173
+ self.adjustments_widget.pan_speed_changed.connect(self.pan_speed_changed)
174
+ self.adjustments_widget.join_threshold_changed.connect(
175
+ self.join_threshold_changed
176
+ )
177
+
178
+ def mouseDoubleClickEvent(self, event):
179
+ """Handle double-click to expand collapsed panel."""
180
+ if (
181
+ self.width() < 50
182
+ and self.parent()
183
+ and hasattr(self.parent(), "_expand_left_panel")
184
+ ):
185
+ self.parent()._expand_left_panel()
186
+ super().mouseDoubleClickEvent(event)
187
+
188
+ def show_notification(self, message: str, duration: int = 3000):
189
+ """Show a notification message."""
190
+ self.notification_label.setText(message)
191
+ # Note: Timer should be handled by the caller
192
+
193
+ def clear_notification(self):
194
+ """Clear the notification message."""
195
+ self.notification_label.clear()
196
+
197
+ def set_mode_text(self, mode: str):
198
+ """Set the mode label text."""
199
+ self.mode_label.setText(f"Mode: {mode.replace('_', ' ').title()}")
200
+
201
+ # Delegate methods for sub-widgets
202
+ def populate_models(self, models):
203
+ """Populate the models combo box."""
204
+ self.model_widget.populate_models(models)
205
+
206
+ def set_current_model(self, model_name):
207
+ """Set the current model display."""
208
+ self.model_widget.set_current_model(model_name)
209
+
210
+ def get_settings(self):
211
+ """Get current settings from the settings widget."""
212
+ return self.settings_widget.get_settings()
213
+
214
+ def set_settings(self, settings):
215
+ """Set settings in the settings widget."""
216
+ self.settings_widget.set_settings(settings)
217
+
218
+ def get_annotation_size(self):
219
+ """Get current annotation size."""
220
+ return self.adjustments_widget.get_annotation_size()
221
+
222
+ def set_annotation_size(self, value):
223
+ """Set annotation size."""
224
+ self.adjustments_widget.set_annotation_size(value)
225
+
226
+ def set_join_threshold(self, value):
227
+ """Set join threshold."""
228
+ self.adjustments_widget.set_join_threshold(value)
229
+
230
+ def set_sam_mode_enabled(self, enabled: bool):
231
+ """Enable or disable the SAM mode button."""
232
+ self.btn_sam_mode.setEnabled(enabled)
233
+ if not enabled:
234
+ self.btn_sam_mode.setToolTip("Point Mode (SAM model not available)")
235
+ else:
236
+ self.btn_sam_mode.setToolTip("Switch to Point Mode for AI segmentation (1)")
237
+
238
+ def set_popout_mode(self, is_popped_out: bool):
239
+ """Update the pop-out button based on panel state."""
240
+ if is_popped_out:
241
+ self.btn_popout.setText("")
242
+ self.btn_popout.setToolTip("Return panel to main window")
243
+ else:
244
+ self.btn_popout.setText("⋯")
245
+ self.btn_popout.setToolTip("Pop out panel to separate window")
@@ -1,64 +1,64 @@
1
- from PyQt6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem
2
- from PyQt6.QtCore import Qt
3
- from PyQt6.QtGui import QBrush, QPen, QColor
4
-
5
-
6
- class EditableVertexItem(QGraphicsEllipseItem):
7
- def __init__(self, main_window, segment_index, vertex_index, x, y, w, h):
8
- super().__init__(x, y, w, h)
9
- self.main_window = main_window
10
- self.segment_index = segment_index
11
- self.vertex_index = vertex_index
12
- self.initial_pos = None
13
-
14
- self.setZValue(200)
15
-
16
- color = QColor(Qt.GlobalColor.cyan)
17
- color.setAlpha(180)
18
- self.setBrush(QBrush(color))
19
-
20
- self.setPen(QPen(Qt.GlobalColor.transparent))
21
-
22
- # Set flags for dragging - use the original working approach
23
- self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
24
- self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
25
- self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
26
-
27
- # Accept mouse events
28
- self.setAcceptHoverEvents(True)
29
-
30
- def itemChange(self, change, value):
31
- if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
32
- new_pos = value
33
- if hasattr(self.main_window, "update_vertex_pos"):
34
- self.main_window.update_vertex_pos(
35
- self.segment_index, self.vertex_index, new_pos, record_undo=False
36
- )
37
- return super().itemChange(change, value)
38
-
39
- def mousePressEvent(self, event):
40
- """Handle mouse press events."""
41
- self.initial_pos = self.pos()
42
- super().mousePressEvent(event)
43
- event.accept()
44
-
45
- def mouseMoveEvent(self, event):
46
- """Handle mouse move events."""
47
- super().mouseMoveEvent(event)
48
- event.accept()
49
-
50
- def mouseReleaseEvent(self, event):
51
- """Handle mouse release events."""
52
- if self.initial_pos and self.initial_pos != self.pos():
53
- self.main_window.action_history.append(
54
- {
55
- "type": "move_vertex",
56
- "segment_index": self.segment_index,
57
- "vertex_index": self.vertex_index,
58
- "old_pos": self.initial_pos,
59
- "new_pos": self.pos(),
60
- }
61
- )
62
- self.initial_pos = None
63
- super().mouseReleaseEvent(event)
64
- event.accept()
1
+ from PyQt6.QtCore import Qt
2
+ from PyQt6.QtGui import QBrush, QColor, QPen
3
+ from PyQt6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem
4
+
5
+
6
+ class EditableVertexItem(QGraphicsEllipseItem):
7
+ def __init__(self, main_window, segment_index, vertex_index, x, y, w, h):
8
+ super().__init__(x, y, w, h)
9
+ self.main_window = main_window
10
+ self.segment_index = segment_index
11
+ self.vertex_index = vertex_index
12
+ self.initial_pos = None
13
+
14
+ self.setZValue(200)
15
+
16
+ color = QColor(Qt.GlobalColor.cyan)
17
+ color.setAlpha(180)
18
+ self.setBrush(QBrush(color))
19
+
20
+ self.setPen(QPen(Qt.GlobalColor.transparent))
21
+
22
+ # Set flags for dragging - use the original working approach
23
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
24
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
25
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
26
+
27
+ # Accept mouse events
28
+ self.setAcceptHoverEvents(True)
29
+
30
+ def itemChange(self, change, value):
31
+ if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
32
+ new_pos = value
33
+ if hasattr(self.main_window, "update_vertex_pos"):
34
+ self.main_window.update_vertex_pos(
35
+ self.segment_index, self.vertex_index, new_pos, record_undo=False
36
+ )
37
+ return super().itemChange(change, value)
38
+
39
+ def mousePressEvent(self, event):
40
+ """Handle mouse press events."""
41
+ self.initial_pos = self.pos()
42
+ super().mousePressEvent(event)
43
+ event.accept()
44
+
45
+ def mouseMoveEvent(self, event):
46
+ """Handle mouse move events."""
47
+ super().mouseMoveEvent(event)
48
+ event.accept()
49
+
50
+ def mouseReleaseEvent(self, event):
51
+ """Handle mouse release events."""
52
+ if self.initial_pos and self.initial_pos != self.pos():
53
+ self.main_window.action_history.append(
54
+ {
55
+ "type": "move_vertex",
56
+ "segment_index": self.segment_index,
57
+ "vertex_index": self.vertex_index,
58
+ "old_pos": self.initial_pos,
59
+ "new_pos": self.pos(),
60
+ }
61
+ )
62
+ self.initial_pos = None
63
+ super().mouseReleaseEvent(event)
64
+ event.accept()