lazylabel-gui 1.1.2__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 (38) hide show
  1. lazylabel/__init__.py +9 -9
  2. lazylabel/config/__init__.py +7 -7
  3. lazylabel/config/hotkeys.py +207 -169
  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 +170 -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 +239 -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 +1787 -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 +314 -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.3.dist-info}/METADATA +243 -197
  33. lazylabel_gui-1.1.3.dist-info/RECORD +37 -0
  34. {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.3.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.3.dist-info}/WHEEL +0 -0
  37. {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.3.dist-info}/entry_points.txt +0 -0
  38. {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.3.dist-info}/top_level.txt +0 -0
@@ -1,315 +1,314 @@
1
- """Right panel with file explorer and segment management."""
2
-
3
- from PyQt6.QtWidgets import (
4
- QWidget,
5
- QVBoxLayout,
6
- QPushButton,
7
- QLabel,
8
- QHBoxLayout,
9
- QTableWidget,
10
- QTreeView,
11
- QComboBox,
12
- QSplitter,
13
- QSpacerItem,
14
- QHeaderView,
15
- )
16
- from PyQt6.QtCore import Qt, pyqtSignal
17
- from PyQt6.QtGui import QBrush, QColor
18
-
19
- from .reorderable_class_table import ReorderableClassTable
20
- from .numeric_table_widget_item import NumericTableWidgetItem
21
-
22
-
23
- class RightPanel(QWidget):
24
- """Right panel with file explorer and segment management."""
25
-
26
- # Signals
27
- open_folder_requested = pyqtSignal()
28
- image_selected = pyqtSignal("QModelIndex")
29
- merge_selection_requested = pyqtSignal()
30
- delete_selection_requested = pyqtSignal()
31
- segments_selection_changed = pyqtSignal()
32
- class_alias_changed = pyqtSignal(int, str) # class_id, alias
33
- reassign_classes_requested = pyqtSignal()
34
- class_filter_changed = pyqtSignal()
35
- class_toggled = pyqtSignal(int) # class_id
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 = 350 # 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
- self.v_layout = QVBoxLayout(self)
48
-
49
- # Top button row
50
- toggle_layout = QHBoxLayout()
51
-
52
- self.btn_popout = QPushButton("⋯")
53
- self.btn_popout.setToolTip("Pop out panel to separate window")
54
- self.btn_popout.setMaximumWidth(30)
55
- toggle_layout.addWidget(self.btn_popout)
56
-
57
- toggle_layout.addStretch()
58
-
59
- self.v_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
- # Vertical splitter for sections
67
- v_splitter = QSplitter(Qt.Orientation.Vertical)
68
-
69
- # File explorer section
70
- self._setup_file_explorer(v_splitter)
71
-
72
- # Segment management section
73
- self._setup_segment_management(v_splitter)
74
-
75
- # Class management section
76
- self._setup_class_management(v_splitter)
77
-
78
- main_layout.addWidget(v_splitter)
79
-
80
- # Status label
81
- self.status_label = QLabel("")
82
- self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
83
- main_layout.addWidget(self.status_label)
84
-
85
- self.v_layout.addWidget(self.main_controls_widget)
86
-
87
- def _setup_file_explorer(self, splitter):
88
- """Setup file explorer section."""
89
- file_explorer_widget = QWidget()
90
- layout = QVBoxLayout(file_explorer_widget)
91
- layout.setContentsMargins(0, 0, 0, 0)
92
-
93
- self.btn_open_folder = QPushButton("Open Image Folder")
94
- self.btn_open_folder.setToolTip("Open a directory of images")
95
- layout.addWidget(self.btn_open_folder)
96
-
97
- self.file_tree = QTreeView()
98
- layout.addWidget(self.file_tree)
99
-
100
- splitter.addWidget(file_explorer_widget)
101
-
102
- def _setup_segment_management(self, splitter):
103
- """Setup segment management section."""
104
- segment_widget = QWidget()
105
- layout = QVBoxLayout(segment_widget)
106
- layout.setContentsMargins(0, 0, 0, 0)
107
-
108
- # Class filter
109
- filter_layout = QHBoxLayout()
110
- filter_layout.addWidget(QLabel("Filter Class:"))
111
- self.class_filter_combo = QComboBox()
112
- self.class_filter_combo.setToolTip("Filter segments list by class")
113
- filter_layout.addWidget(self.class_filter_combo)
114
- layout.addLayout(filter_layout)
115
-
116
- # Segment table
117
- self.segment_table = QTableWidget()
118
- self.segment_table.setColumnCount(3)
119
- self.segment_table.setHorizontalHeaderLabels(
120
- ["Segment ID", "Class ID", "Alias"]
121
- )
122
- self.segment_table.horizontalHeader().setSectionResizeMode(
123
- QHeaderView.ResizeMode.Stretch
124
- )
125
- self.segment_table.setSelectionBehavior(
126
- QTableWidget.SelectionBehavior.SelectRows
127
- )
128
- self.segment_table.setSortingEnabled(True)
129
- self.segment_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
130
- layout.addWidget(self.segment_table)
131
-
132
- # Action buttons
133
- action_layout = QHBoxLayout()
134
- self.btn_merge_selection = QPushButton("Merge to Class")
135
- self.btn_merge_selection.setToolTip(
136
- "Merge selected segments into a single class (M)"
137
- )
138
- self.btn_delete_selection = QPushButton("Delete")
139
- self.btn_delete_selection.setToolTip(
140
- "Delete selected segments (Delete/Backspace)"
141
- )
142
- action_layout.addWidget(self.btn_merge_selection)
143
- action_layout.addWidget(self.btn_delete_selection)
144
- layout.addLayout(action_layout)
145
-
146
- splitter.addWidget(segment_widget)
147
-
148
- def _setup_class_management(self, splitter):
149
- """Setup class management section."""
150
- class_widget = QWidget()
151
- layout = QVBoxLayout(class_widget)
152
- layout.setContentsMargins(0, 0, 0, 0)
153
-
154
- layout.addWidget(QLabel("Class Order:"))
155
-
156
- self.class_table = ReorderableClassTable()
157
- self.class_table.setToolTip(
158
- "Double-click to set class aliases and drag to reorder channels for saving.\nClick once to toggle as active class for new segments."
159
- )
160
- self.class_table.setColumnCount(2)
161
- self.class_table.setHorizontalHeaderLabels(["Alias", "Class ID"])
162
- self.class_table.horizontalHeader().setSectionResizeMode(
163
- 0, QHeaderView.ResizeMode.Stretch
164
- )
165
- self.class_table.horizontalHeader().setSectionResizeMode(
166
- 1, QHeaderView.ResizeMode.ResizeToContents
167
- )
168
- self.class_table.setEditTriggers(QTableWidget.EditTrigger.DoubleClicked)
169
- layout.addWidget(self.class_table)
170
-
171
- self.btn_reassign_classes = QPushButton("Reassign Class IDs")
172
- self.btn_reassign_classes.setToolTip(
173
- "Re-index class channels based on the current order in this table"
174
- )
175
- layout.addWidget(self.btn_reassign_classes)
176
-
177
- splitter.addWidget(class_widget)
178
-
179
- def _connect_signals(self):
180
- """Connect internal signals."""
181
- self.btn_open_folder.clicked.connect(self.open_folder_requested)
182
- self.file_tree.doubleClicked.connect(self.image_selected)
183
- self.btn_merge_selection.clicked.connect(self.merge_selection_requested)
184
- self.btn_delete_selection.clicked.connect(self.delete_selection_requested)
185
- self.segment_table.itemSelectionChanged.connect(self.segments_selection_changed)
186
- self.class_table.itemChanged.connect(self._handle_class_alias_change)
187
- self.class_table.cellClicked.connect(self._handle_class_toggle)
188
- self.btn_reassign_classes.clicked.connect(self.reassign_classes_requested)
189
- self.class_filter_combo.currentIndexChanged.connect(self.class_filter_changed)
190
- self.btn_popout.clicked.connect(self.pop_out_requested)
191
-
192
- def mouseDoubleClickEvent(self, event):
193
- """Handle double-click to expand collapsed panel."""
194
- if self.width() < 50: # If panel is collapsed
195
- # Request expansion by calling parent method
196
- if self.parent() and hasattr(self.parent(), "_expand_right_panel"):
197
- self.parent()._expand_right_panel()
198
- super().mouseDoubleClickEvent(event)
199
-
200
- def _handle_class_alias_change(self, item):
201
- """Handle class alias change in table."""
202
- if item.column() != 0: # Only handle alias column
203
- return
204
-
205
- class_table = self.class_table
206
- id_item = class_table.item(item.row(), 1)
207
- if id_item:
208
- try:
209
- class_id = int(id_item.text())
210
- self.class_alias_changed.emit(class_id, item.text())
211
- except (ValueError, AttributeError):
212
- pass
213
-
214
- def _handle_class_toggle(self, row, column):
215
- """Handle class table cell click for toggling active class."""
216
- # Get the class ID from the clicked row
217
- id_item = self.class_table.item(row, 1)
218
- if id_item:
219
- try:
220
- class_id = int(id_item.text())
221
- self.class_toggled.emit(class_id)
222
- except (ValueError, AttributeError):
223
- pass
224
-
225
- def update_active_class_display(self, active_class_id):
226
- """Update the visual display to show which class is active."""
227
- # Block signals to prevent triggering change events during update
228
- self.class_table.blockSignals(True)
229
-
230
- for row in range(self.class_table.rowCount()):
231
- id_item = self.class_table.item(row, 1)
232
- alias_item = self.class_table.item(row, 0)
233
- if id_item and alias_item:
234
- try:
235
- class_id = int(id_item.text())
236
- if class_id == active_class_id:
237
- # Make active class bold and add indicator
238
- font = alias_item.font()
239
- font.setBold(True)
240
- alias_item.setFont(font)
241
- id_item.setFont(font)
242
- # Add visual indicator
243
- if not alias_item.text().startswith("🔸 "):
244
- alias_item.setText(f"🔸 {alias_item.text()}")
245
- else:
246
- # Make inactive classes normal
247
- font = alias_item.font()
248
- font.setBold(False)
249
- alias_item.setFont(font)
250
- id_item.setFont(font)
251
- # Remove visual indicator
252
- if alias_item.text().startswith("🔸 "):
253
- alias_item.setText(alias_item.text()[2:])
254
- except (ValueError, AttributeError):
255
- pass
256
-
257
- # Re-enable signals
258
- self.class_table.blockSignals(False)
259
-
260
- def setup_file_model(self, file_model):
261
- """Setup the file model for the tree view."""
262
- self.file_tree.setModel(file_model)
263
- self.file_tree.setColumnWidth(0, 200)
264
-
265
- def set_folder(self, folder_path, file_model):
266
- """Set the folder for file browsing."""
267
- self.file_tree.setRootIndex(file_model.setRootPath(folder_path))
268
-
269
- def get_selected_segment_indices(self):
270
- """Get indices of selected segments."""
271
- selected_items = self.segment_table.selectedItems()
272
- selected_rows = sorted(list({item.row() for item in selected_items}))
273
- return [
274
- self.segment_table.item(row, 0).data(Qt.ItemDataRole.UserRole)
275
- for row in selected_rows
276
- if self.segment_table.item(row, 0)
277
- ]
278
-
279
- def get_class_order(self):
280
- """Get the current class order from the class table."""
281
- ordered_ids = []
282
- for row in range(self.class_table.rowCount()):
283
- id_item = self.class_table.item(row, 1)
284
- if id_item and id_item.text():
285
- try:
286
- ordered_ids.append(int(id_item.text()))
287
- except ValueError:
288
- continue
289
- return ordered_ids
290
-
291
- def clear_selections(self):
292
- """Clear all selections."""
293
- self.segment_table.clearSelection()
294
- self.class_table.clearSelection()
295
-
296
- def select_all_segments(self):
297
- """Select all segments."""
298
- self.segment_table.selectAll()
299
-
300
- def set_status(self, message):
301
- """Set status message."""
302
- self.status_label.setText(message)
303
-
304
- def clear_status(self):
305
- """Clear status message."""
306
- self.status_label.clear()
307
-
308
- def set_popout_mode(self, is_popped_out: bool):
309
- """Update the pop-out button based on panel state."""
310
- if is_popped_out:
311
- self.btn_popout.setText("")
312
- self.btn_popout.setToolTip("Return panel to main window")
313
- else:
314
- self.btn_popout.setText("")
315
- self.btn_popout.setToolTip("Pop out panel to separate window")
1
+ """Right panel with file explorer and segment management."""
2
+
3
+ from PyQt6.QtCore import Qt, pyqtSignal
4
+ from PyQt6.QtWidgets import (
5
+ QComboBox,
6
+ QHBoxLayout,
7
+ QHeaderView,
8
+ QLabel,
9
+ QPushButton,
10
+ QSplitter,
11
+ QTableWidget,
12
+ QTreeView,
13
+ QVBoxLayout,
14
+ QWidget,
15
+ )
16
+
17
+ from .reorderable_class_table import ReorderableClassTable
18
+
19
+
20
+ class RightPanel(QWidget):
21
+ """Right panel with file explorer and segment management."""
22
+
23
+ # Signals
24
+ open_folder_requested = pyqtSignal()
25
+ image_selected = pyqtSignal("QModelIndex")
26
+ merge_selection_requested = pyqtSignal()
27
+ delete_selection_requested = pyqtSignal()
28
+ segments_selection_changed = pyqtSignal()
29
+ class_alias_changed = pyqtSignal(int, str) # class_id, alias
30
+ reassign_classes_requested = pyqtSignal()
31
+ class_filter_changed = pyqtSignal()
32
+ class_toggled = pyqtSignal(int) # class_id
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 = 350 # 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
+ self.v_layout = QVBoxLayout(self)
45
+
46
+ # Top button row
47
+ toggle_layout = QHBoxLayout()
48
+
49
+ self.btn_popout = QPushButton("⋯")
50
+ self.btn_popout.setToolTip("Pop out panel to separate window")
51
+ self.btn_popout.setMaximumWidth(30)
52
+ toggle_layout.addWidget(self.btn_popout)
53
+
54
+ toggle_layout.addStretch()
55
+
56
+ self.v_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
+ # Vertical splitter for sections
64
+ v_splitter = QSplitter(Qt.Orientation.Vertical)
65
+
66
+ # File explorer section
67
+ self._setup_file_explorer(v_splitter)
68
+
69
+ # Segment management section
70
+ self._setup_segment_management(v_splitter)
71
+
72
+ # Class management section
73
+ self._setup_class_management(v_splitter)
74
+
75
+ main_layout.addWidget(v_splitter)
76
+
77
+ # Status label
78
+ self.status_label = QLabel("")
79
+ self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
80
+ main_layout.addWidget(self.status_label)
81
+
82
+ self.v_layout.addWidget(self.main_controls_widget)
83
+
84
+ def _setup_file_explorer(self, splitter):
85
+ """Setup file explorer section."""
86
+ file_explorer_widget = QWidget()
87
+ layout = QVBoxLayout(file_explorer_widget)
88
+ layout.setContentsMargins(0, 0, 0, 0)
89
+
90
+ self.btn_open_folder = QPushButton("Open Image Folder")
91
+ self.btn_open_folder.setToolTip("Open a directory of images")
92
+ layout.addWidget(self.btn_open_folder)
93
+
94
+ self.file_tree = QTreeView()
95
+ layout.addWidget(self.file_tree)
96
+
97
+ splitter.addWidget(file_explorer_widget)
98
+
99
+ def _setup_segment_management(self, splitter):
100
+ """Setup segment management section."""
101
+ segment_widget = QWidget()
102
+ layout = QVBoxLayout(segment_widget)
103
+ layout.setContentsMargins(0, 0, 0, 0)
104
+
105
+ # Class filter
106
+ filter_layout = QHBoxLayout()
107
+ filter_layout.addWidget(QLabel("Filter Class:"))
108
+ self.class_filter_combo = QComboBox()
109
+ self.class_filter_combo.setToolTip("Filter segments list by class")
110
+ filter_layout.addWidget(self.class_filter_combo)
111
+ layout.addLayout(filter_layout)
112
+
113
+ # Segment table
114
+ self.segment_table = QTableWidget()
115
+ self.segment_table.setColumnCount(3)
116
+ self.segment_table.setHorizontalHeaderLabels(
117
+ ["Segment ID", "Class ID", "Alias"]
118
+ )
119
+ self.segment_table.horizontalHeader().setSectionResizeMode(
120
+ QHeaderView.ResizeMode.Stretch
121
+ )
122
+ self.segment_table.setSelectionBehavior(
123
+ QTableWidget.SelectionBehavior.SelectRows
124
+ )
125
+ self.segment_table.setSortingEnabled(True)
126
+ self.segment_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
127
+ layout.addWidget(self.segment_table)
128
+
129
+ # Action buttons
130
+ action_layout = QHBoxLayout()
131
+ self.btn_merge_selection = QPushButton("Merge to Class")
132
+ self.btn_merge_selection.setToolTip(
133
+ "Merge selected segments into a single class (M)"
134
+ )
135
+ self.btn_delete_selection = QPushButton("Delete")
136
+ self.btn_delete_selection.setToolTip(
137
+ "Delete selected segments (Delete/Backspace)"
138
+ )
139
+ action_layout.addWidget(self.btn_merge_selection)
140
+ action_layout.addWidget(self.btn_delete_selection)
141
+ layout.addLayout(action_layout)
142
+
143
+ splitter.addWidget(segment_widget)
144
+
145
+ def _setup_class_management(self, splitter):
146
+ """Setup class management section."""
147
+ class_widget = QWidget()
148
+ layout = QVBoxLayout(class_widget)
149
+ layout.setContentsMargins(0, 0, 0, 0)
150
+
151
+ layout.addWidget(QLabel("Class Order:"))
152
+
153
+ self.class_table = ReorderableClassTable()
154
+ self.class_table.setToolTip(
155
+ "Double-click to set class aliases and drag to reorder channels for saving.\nClick once to toggle as active class for new segments."
156
+ )
157
+ self.class_table.setColumnCount(2)
158
+ self.class_table.setHorizontalHeaderLabels(["Alias", "Class ID"])
159
+ self.class_table.horizontalHeader().setSectionResizeMode(
160
+ 0, QHeaderView.ResizeMode.Stretch
161
+ )
162
+ self.class_table.horizontalHeader().setSectionResizeMode(
163
+ 1, QHeaderView.ResizeMode.ResizeToContents
164
+ )
165
+ self.class_table.setEditTriggers(QTableWidget.EditTrigger.DoubleClicked)
166
+ layout.addWidget(self.class_table)
167
+
168
+ self.btn_reassign_classes = QPushButton("Reassign Class IDs")
169
+ self.btn_reassign_classes.setToolTip(
170
+ "Re-index class channels based on the current order in this table"
171
+ )
172
+ layout.addWidget(self.btn_reassign_classes)
173
+
174
+ splitter.addWidget(class_widget)
175
+
176
+ def _connect_signals(self):
177
+ """Connect internal signals."""
178
+ self.btn_open_folder.clicked.connect(self.open_folder_requested)
179
+ self.file_tree.doubleClicked.connect(self.image_selected)
180
+ self.btn_merge_selection.clicked.connect(self.merge_selection_requested)
181
+ self.btn_delete_selection.clicked.connect(self.delete_selection_requested)
182
+ self.segment_table.itemSelectionChanged.connect(self.segments_selection_changed)
183
+ self.class_table.itemChanged.connect(self._handle_class_alias_change)
184
+ self.class_table.cellClicked.connect(self._handle_class_toggle)
185
+ self.btn_reassign_classes.clicked.connect(self.reassign_classes_requested)
186
+ self.class_filter_combo.currentIndexChanged.connect(self.class_filter_changed)
187
+ self.btn_popout.clicked.connect(self.pop_out_requested)
188
+
189
+ def mouseDoubleClickEvent(self, event):
190
+ """Handle double-click to expand collapsed panel."""
191
+ if (
192
+ self.width() < 50
193
+ and self.parent()
194
+ and hasattr(self.parent(), "_expand_right_panel")
195
+ ):
196
+ self.parent()._expand_right_panel()
197
+ super().mouseDoubleClickEvent(event)
198
+
199
+ def _handle_class_alias_change(self, item):
200
+ """Handle class alias change in table."""
201
+ if item.column() != 0: # Only handle alias column
202
+ return
203
+
204
+ class_table = self.class_table
205
+ id_item = class_table.item(item.row(), 1)
206
+ if id_item:
207
+ try:
208
+ class_id = int(id_item.text())
209
+ self.class_alias_changed.emit(class_id, item.text())
210
+ except (ValueError, AttributeError):
211
+ pass
212
+
213
+ def _handle_class_toggle(self, row, column):
214
+ """Handle class table cell click for toggling active class."""
215
+ # Get the class ID from the clicked row
216
+ id_item = self.class_table.item(row, 1)
217
+ if id_item:
218
+ try:
219
+ class_id = int(id_item.text())
220
+ self.class_toggled.emit(class_id)
221
+ except (ValueError, AttributeError):
222
+ pass
223
+
224
+ def update_active_class_display(self, active_class_id):
225
+ """Update the visual display to show which class is active."""
226
+ # Block signals to prevent triggering change events during update
227
+ self.class_table.blockSignals(True)
228
+
229
+ for row in range(self.class_table.rowCount()):
230
+ id_item = self.class_table.item(row, 1)
231
+ alias_item = self.class_table.item(row, 0)
232
+ if id_item and alias_item:
233
+ try:
234
+ class_id = int(id_item.text())
235
+ if class_id == active_class_id:
236
+ # Make active class bold and add indicator
237
+ font = alias_item.font()
238
+ font.setBold(True)
239
+ alias_item.setFont(font)
240
+ id_item.setFont(font)
241
+ # Add visual indicator
242
+ if not alias_item.text().startswith("🔸 "):
243
+ alias_item.setText(f"🔸 {alias_item.text()}")
244
+ else:
245
+ # Make inactive classes normal
246
+ font = alias_item.font()
247
+ font.setBold(False)
248
+ alias_item.setFont(font)
249
+ id_item.setFont(font)
250
+ # Remove visual indicator
251
+ if alias_item.text().startswith("🔸 "):
252
+ alias_item.setText(alias_item.text()[2:])
253
+ except (ValueError, AttributeError):
254
+ pass
255
+
256
+ # Re-enable signals
257
+ self.class_table.blockSignals(False)
258
+
259
+ def setup_file_model(self, file_model):
260
+ """Setup the file model for the tree view."""
261
+ self.file_tree.setModel(file_model)
262
+ self.file_tree.setColumnWidth(0, 200)
263
+
264
+ def set_folder(self, folder_path, file_model):
265
+ """Set the folder for file browsing."""
266
+ self.file_tree.setRootIndex(file_model.setRootPath(folder_path))
267
+
268
+ def get_selected_segment_indices(self):
269
+ """Get indices of selected segments."""
270
+ selected_items = self.segment_table.selectedItems()
271
+ selected_rows = sorted({item.row() for item in selected_items})
272
+ return [
273
+ self.segment_table.item(row, 0).data(Qt.ItemDataRole.UserRole)
274
+ for row in selected_rows
275
+ if self.segment_table.item(row, 0)
276
+ ]
277
+
278
+ def get_class_order(self):
279
+ """Get the current class order from the class table."""
280
+ ordered_ids = []
281
+ for row in range(self.class_table.rowCount()):
282
+ id_item = self.class_table.item(row, 1)
283
+ if id_item and id_item.text():
284
+ try:
285
+ ordered_ids.append(int(id_item.text()))
286
+ except ValueError:
287
+ continue
288
+ return ordered_ids
289
+
290
+ def clear_selections(self):
291
+ """Clear all selections."""
292
+ self.segment_table.clearSelection()
293
+ self.class_table.clearSelection()
294
+
295
+ def select_all_segments(self):
296
+ """Select all segments."""
297
+ self.segment_table.selectAll()
298
+
299
+ def set_status(self, message):
300
+ """Set status message."""
301
+ self.status_label.setText(message)
302
+
303
+ def clear_status(self):
304
+ """Clear status message."""
305
+ self.status_label.clear()
306
+
307
+ def set_popout_mode(self, is_popped_out: bool):
308
+ """Update the pop-out button based on panel state."""
309
+ if is_popped_out:
310
+ self.btn_popout.setText("⇤")
311
+ self.btn_popout.setToolTip("Return panel to main window")
312
+ else:
313
+ self.btn_popout.setText("⋯")
314
+ self.btn_popout.setToolTip("Pop out panel to separate window")