lazylabel-gui 1.1.0__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.
- lazylabel/core/model_manager.py +22 -19
- lazylabel/core/segment_manager.py +65 -34
- lazylabel/main.py +17 -3
- lazylabel/models/sam_model.py +72 -31
- lazylabel/ui/control_panel.py +83 -66
- lazylabel/ui/main_window.py +322 -40
- lazylabel/ui/right_panel.py +149 -73
- lazylabel/ui/widgets/__init__.py +2 -1
- lazylabel/ui/widgets/status_bar.py +109 -0
- {lazylabel_gui-1.1.0.dist-info → lazylabel_gui-1.1.1.dist-info}/METADATA +1 -1
- {lazylabel_gui-1.1.0.dist-info → lazylabel_gui-1.1.1.dist-info}/RECORD +15 -14
- {lazylabel_gui-1.1.0.dist-info → lazylabel_gui-1.1.1.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.1.0.dist-info → lazylabel_gui-1.1.1.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.1.0.dist-info → lazylabel_gui-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.1.0.dist-info → lazylabel_gui-1.1.1.dist-info}/top_level.txt +0 -0
lazylabel/ui/main_window.py
CHANGED
@@ -7,6 +7,7 @@ from PyQt6.QtWidgets import (
|
|
7
7
|
QMainWindow,
|
8
8
|
QWidget,
|
9
9
|
QHBoxLayout,
|
10
|
+
QVBoxLayout,
|
10
11
|
QFileDialog,
|
11
12
|
QApplication,
|
12
13
|
QGraphicsEllipseItem,
|
@@ -15,6 +16,8 @@ from PyQt6.QtWidgets import (
|
|
15
16
|
QTableWidgetItem,
|
16
17
|
QTableWidgetSelectionRange,
|
17
18
|
QHeaderView,
|
19
|
+
QSplitter,
|
20
|
+
QDialog,
|
18
21
|
)
|
19
22
|
from PyQt6.QtGui import (
|
20
23
|
QIcon,
|
@@ -27,7 +30,7 @@ from PyQt6.QtGui import (
|
|
27
30
|
QPolygonF,
|
28
31
|
QImage,
|
29
32
|
)
|
30
|
-
from PyQt6.QtCore import Qt, QTimer, QModelIndex, QPointF
|
33
|
+
from PyQt6.QtCore import Qt, QTimer, QModelIndex, QPointF, pyqtSignal
|
31
34
|
|
32
35
|
from .control_panel import ControlPanel
|
33
36
|
from .right_panel import RightPanel
|
@@ -40,6 +43,36 @@ from ..core import SegmentManager, ModelManager, FileManager
|
|
40
43
|
from ..config import Settings, Paths, HotkeyManager
|
41
44
|
from ..utils import CustomFileSystemModel, mask_to_pixmap
|
42
45
|
from .hotkey_dialog import HotkeyDialog
|
46
|
+
from .widgets import StatusBar
|
47
|
+
|
48
|
+
|
49
|
+
class PanelPopoutWindow(QDialog):
|
50
|
+
"""Pop-out window for draggable panels."""
|
51
|
+
|
52
|
+
panel_closed = pyqtSignal(QWidget) # Signal emitted when panel window is closed
|
53
|
+
|
54
|
+
def __init__(self, panel_widget, title="Panel", parent=None):
|
55
|
+
super().__init__(parent)
|
56
|
+
self.panel_widget = panel_widget
|
57
|
+
self.setWindowTitle(title)
|
58
|
+
self.setWindowFlags(Qt.WindowType.Window) # Allow moving to other monitors
|
59
|
+
|
60
|
+
# Make window resizable
|
61
|
+
self.setMinimumSize(200, 300)
|
62
|
+
self.resize(400, 600)
|
63
|
+
|
64
|
+
# Set up layout
|
65
|
+
layout = QVBoxLayout(self)
|
66
|
+
layout.setContentsMargins(5, 5, 5, 5)
|
67
|
+
layout.addWidget(panel_widget)
|
68
|
+
|
69
|
+
# Store original parent for restoration
|
70
|
+
self.original_parent = parent
|
71
|
+
|
72
|
+
def closeEvent(self, event):
|
73
|
+
"""Handle window close - emit signal to return panel to main window."""
|
74
|
+
self.panel_closed.emit(self.panel_widget)
|
75
|
+
super().closeEvent(event)
|
43
76
|
|
44
77
|
|
45
78
|
class MainWindow(QMainWindow):
|
@@ -48,22 +81,33 @@ class MainWindow(QMainWindow):
|
|
48
81
|
def __init__(self):
|
49
82
|
super().__init__()
|
50
83
|
|
84
|
+
print("[3/20] Starting LazyLabel...")
|
85
|
+
print("[4/20] Loading configuration and settings...")
|
86
|
+
|
51
87
|
# Initialize configuration
|
52
88
|
self.paths = Paths()
|
53
89
|
self.settings = Settings.load_from_file(str(self.paths.settings_file))
|
54
90
|
self.hotkey_manager = HotkeyManager(str(self.paths.config_dir))
|
55
91
|
|
92
|
+
print("[5/20] Initializing core managers...")
|
93
|
+
|
56
94
|
# Initialize managers
|
57
95
|
self.segment_manager = SegmentManager()
|
58
96
|
self.model_manager = ModelManager(self.paths)
|
59
97
|
self.file_manager = FileManager(self.segment_manager)
|
60
98
|
|
99
|
+
print("[6/20] Setting up user interface...")
|
100
|
+
|
61
101
|
# Initialize UI state
|
62
102
|
self.mode = "sam_points"
|
63
103
|
self.previous_mode = "sam_points"
|
64
104
|
self.current_image_path = None
|
65
105
|
self.current_file_index = QModelIndex()
|
66
106
|
|
107
|
+
# Panel pop-out state
|
108
|
+
self.left_panel_popout = None
|
109
|
+
self.right_panel_popout = None
|
110
|
+
|
67
111
|
# Annotation state
|
68
112
|
self.point_radius = self.settings.point_radius
|
69
113
|
self.line_thickness = self.settings.line_thickness
|
@@ -83,12 +127,19 @@ class MainWindow(QMainWindow):
|
|
83
127
|
{},
|
84
128
|
)
|
85
129
|
|
130
|
+
# Update state flags to prevent recursion
|
131
|
+
self._updating_lists = False
|
132
|
+
|
86
133
|
self._setup_ui()
|
87
134
|
self._setup_model()
|
135
|
+
|
136
|
+
print("[17/20] Connecting UI signals and shortcuts...")
|
88
137
|
self._setup_connections()
|
89
138
|
self._setup_shortcuts()
|
90
139
|
self._load_settings()
|
91
140
|
|
141
|
+
print("[18/20] LazyLabel initialization complete!")
|
142
|
+
|
92
143
|
def _setup_ui(self):
|
93
144
|
"""Setup the user interface."""
|
94
145
|
self.setWindowTitle("LazyLabel by DNC")
|
@@ -110,11 +161,37 @@ class MainWindow(QMainWindow):
|
|
110
161
|
self.file_model = CustomFileSystemModel()
|
111
162
|
self.right_panel.setup_file_model(self.file_model)
|
112
163
|
|
113
|
-
#
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
164
|
+
# Create status bar
|
165
|
+
self.status_bar = StatusBar()
|
166
|
+
self.setStatusBar(self.status_bar)
|
167
|
+
|
168
|
+
# Create horizontal splitter for main panels
|
169
|
+
self.main_splitter = QSplitter(Qt.Orientation.Horizontal)
|
170
|
+
self.main_splitter.addWidget(self.control_panel)
|
171
|
+
self.main_splitter.addWidget(self.viewer)
|
172
|
+
self.main_splitter.addWidget(self.right_panel)
|
173
|
+
|
174
|
+
# Set minimum sizes for panels to prevent shrinking below preferred width
|
175
|
+
self.control_panel.setMinimumWidth(self.control_panel.preferred_width)
|
176
|
+
self.right_panel.setMinimumWidth(self.right_panel.preferred_width)
|
177
|
+
|
178
|
+
# Set splitter sizes - give most space to viewer
|
179
|
+
self.main_splitter.setSizes([250, 800, 350])
|
180
|
+
self.main_splitter.setStretchFactor(0, 0) # Control panel doesn't stretch
|
181
|
+
self.main_splitter.setStretchFactor(1, 1) # Viewer stretches
|
182
|
+
self.main_splitter.setStretchFactor(2, 0) # Right panel doesn't stretch
|
183
|
+
|
184
|
+
# Set splitter child sizes policy
|
185
|
+
self.main_splitter.setChildrenCollapsible(True)
|
186
|
+
|
187
|
+
# Connect splitter signals for intelligent expand/collapse
|
188
|
+
self.main_splitter.splitterMoved.connect(self._handle_splitter_moved)
|
189
|
+
|
190
|
+
# Main vertical layout to accommodate status bar
|
191
|
+
main_layout = QVBoxLayout()
|
192
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
193
|
+
main_layout.setSpacing(0)
|
194
|
+
main_layout.addWidget(self.main_splitter, 1)
|
118
195
|
|
119
196
|
central_widget = QWidget()
|
120
197
|
central_widget.setLayout(main_layout)
|
@@ -122,17 +199,29 @@ class MainWindow(QMainWindow):
|
|
122
199
|
|
123
200
|
def _setup_model(self):
|
124
201
|
"""Setup the SAM model."""
|
202
|
+
print("[7/20] Initializing SAM model (this may take a moment)...")
|
203
|
+
|
125
204
|
sam_model = self.model_manager.initialize_default_model(
|
126
205
|
self.settings.default_model_type
|
127
206
|
)
|
128
207
|
|
129
208
|
if sam_model and sam_model.is_loaded:
|
130
|
-
|
209
|
+
device_text = str(sam_model.device).upper()
|
210
|
+
print(f"[14/20] SAM model loaded successfully on {device_text}")
|
211
|
+
self.status_bar.set_permanent_message(f"Device: {device_text}")
|
131
212
|
self._enable_sam_functionality(True)
|
213
|
+
elif sam_model is None:
|
214
|
+
print(
|
215
|
+
"[14/20] SAM model initialization failed. Point mode will be disabled."
|
216
|
+
)
|
217
|
+
self.status_bar.set_permanent_message("Model initialization failed")
|
218
|
+
self._enable_sam_functionality(False)
|
132
219
|
else:
|
133
|
-
|
220
|
+
print("[14/20] SAM model failed to load. Point mode will be disabled.")
|
221
|
+
self.status_bar.set_permanent_message("Model loading failed")
|
134
222
|
self._enable_sam_functionality(False)
|
135
|
-
|
223
|
+
|
224
|
+
print("[15/20] Scanning available models...")
|
136
225
|
|
137
226
|
# Setup model change callback
|
138
227
|
self.model_manager.on_model_changed = self.control_panel.set_current_model
|
@@ -141,6 +230,9 @@ class MainWindow(QMainWindow):
|
|
141
230
|
models = self.model_manager.get_available_models(str(self.paths.models_dir))
|
142
231
|
self.control_panel.populate_models(models)
|
143
232
|
|
233
|
+
if models:
|
234
|
+
print(f"[16/20] Found {len(models)} model(s) in models directory")
|
235
|
+
|
144
236
|
def _enable_sam_functionality(self, enabled: bool):
|
145
237
|
"""Enable or disable SAM point functionality."""
|
146
238
|
self.control_panel.set_sam_mode_enabled(enabled)
|
@@ -183,14 +275,11 @@ class MainWindow(QMainWindow):
|
|
183
275
|
self.right_panel.class_alias_changed.connect(self._handle_alias_change)
|
184
276
|
self.right_panel.reassign_classes_requested.connect(self._reassign_class_ids)
|
185
277
|
self.right_panel.class_filter_changed.connect(self._update_segment_table)
|
278
|
+
self.right_panel.class_toggled.connect(self._handle_class_toggle)
|
186
279
|
|
187
|
-
# Panel
|
188
|
-
self.control_panel.
|
189
|
-
|
190
|
-
)
|
191
|
-
self.right_panel.btn_toggle_visibility.clicked.connect(
|
192
|
-
self.right_panel.toggle_visibility
|
193
|
-
)
|
280
|
+
# Panel pop-out functionality
|
281
|
+
self.control_panel.pop_out_requested.connect(self._pop_out_left_panel)
|
282
|
+
self.right_panel.pop_out_requested.connect(self._pop_out_right_panel)
|
194
283
|
|
195
284
|
# Mouse events (will be implemented in a separate handler)
|
196
285
|
self._setup_mouse_events()
|
@@ -355,9 +444,9 @@ class MainWindow(QMainWindow):
|
|
355
444
|
if folder and os.path.exists(folder):
|
356
445
|
models = self.model_manager.get_available_models(folder)
|
357
446
|
self.control_panel.populate_models(models)
|
358
|
-
self.
|
447
|
+
self._show_success_notification("Models list refreshed.")
|
359
448
|
else:
|
360
|
-
self.
|
449
|
+
self._show_warning_notification("No models folder selected.")
|
361
450
|
|
362
451
|
def _load_selected_model(self, model_text):
|
363
452
|
"""Load the selected model."""
|
@@ -367,7 +456,7 @@ class MainWindow(QMainWindow):
|
|
367
456
|
|
368
457
|
model_path = self.control_panel.model_widget.get_selected_model_path()
|
369
458
|
if not model_path or not os.path.exists(model_path):
|
370
|
-
self.
|
459
|
+
self._show_error_notification("Selected model file not found.")
|
371
460
|
return
|
372
461
|
|
373
462
|
self.control_panel.set_current_model("Loading model...")
|
@@ -379,17 +468,18 @@ class MainWindow(QMainWindow):
|
|
379
468
|
# Re-enable SAM functionality if model loaded successfully
|
380
469
|
self._enable_sam_functionality(True)
|
381
470
|
if self.model_manager.sam_model:
|
382
|
-
self.
|
383
|
-
|
384
|
-
)
|
471
|
+
device_text = str(self.model_manager.sam_model.device).upper()
|
472
|
+
self.status_bar.set_permanent_message(f"Device: {device_text}")
|
385
473
|
else:
|
386
474
|
self.control_panel.set_current_model("Current: Default SAM Model")
|
387
|
-
self.
|
475
|
+
self._show_error_notification(
|
476
|
+
"Failed to load selected model. Using default."
|
477
|
+
)
|
388
478
|
self.control_panel.model_widget.reset_to_default()
|
389
479
|
self._enable_sam_functionality(False)
|
390
480
|
except Exception as e:
|
391
481
|
self.control_panel.set_current_model("Current: Default SAM Model")
|
392
|
-
self.
|
482
|
+
self._show_error_notification(f"Error loading model: {str(e)}")
|
393
483
|
self.control_panel.model_widget.reset_to_default()
|
394
484
|
self._enable_sam_functionality(False)
|
395
485
|
|
@@ -539,6 +629,8 @@ class MainWindow(QMainWindow):
|
|
539
629
|
|
540
630
|
def _handle_alias_change(self, class_id, alias):
|
541
631
|
"""Handle class alias change."""
|
632
|
+
if self._updating_lists:
|
633
|
+
return # Prevent recursion
|
542
634
|
self.segment_manager.set_class_alias(class_id, alias)
|
543
635
|
self._update_all_lists()
|
544
636
|
|
@@ -624,16 +716,27 @@ class MainWindow(QMainWindow):
|
|
624
716
|
table.blockSignals(False)
|
625
717
|
self.viewer.setFocus()
|
626
718
|
|
719
|
+
# Update active class display
|
720
|
+
active_class = self.segment_manager.get_active_class()
|
721
|
+
self.right_panel.update_active_class_display(active_class)
|
722
|
+
|
627
723
|
def _update_all_lists(self):
|
628
724
|
"""Update all UI lists."""
|
629
|
-
self.
|
630
|
-
|
631
|
-
|
632
|
-
self.
|
633
|
-
|
634
|
-
self.
|
635
|
-
|
636
|
-
self.
|
725
|
+
if self._updating_lists:
|
726
|
+
return # Prevent recursion
|
727
|
+
|
728
|
+
self._updating_lists = True
|
729
|
+
try:
|
730
|
+
self._update_class_list()
|
731
|
+
self._update_segment_table()
|
732
|
+
self._update_class_filter()
|
733
|
+
self._display_all_segments()
|
734
|
+
if self.mode == "edit":
|
735
|
+
self._display_edit_handles()
|
736
|
+
else:
|
737
|
+
self._clear_edit_handles()
|
738
|
+
finally:
|
739
|
+
self._updating_lists = False
|
637
740
|
|
638
741
|
def _update_class_list(self):
|
639
742
|
"""Update the class list in the right panel."""
|
@@ -658,6 +761,10 @@ class MainWindow(QMainWindow):
|
|
658
761
|
class_table.setItem(row, 0, alias_item)
|
659
762
|
class_table.setItem(row, 1, id_item)
|
660
763
|
|
764
|
+
# Update active class display BEFORE re-enabling signals
|
765
|
+
active_class = self.segment_manager.get_active_class()
|
766
|
+
self.right_panel.update_active_class_display(active_class)
|
767
|
+
|
661
768
|
class_table.blockSignals(False)
|
662
769
|
|
663
770
|
def _update_class_filter(self):
|
@@ -790,7 +897,7 @@ class MainWindow(QMainWindow):
|
|
790
897
|
def _save_output_to_npz(self):
|
791
898
|
"""Save output to NPZ and TXT files as enabled, and update file list tickboxes/highlight. If no segments, delete associated files."""
|
792
899
|
if not self.current_image_path:
|
793
|
-
self.
|
900
|
+
self._show_warning_notification("No image loaded.")
|
794
901
|
return
|
795
902
|
|
796
903
|
# If no segments, delete associated files
|
@@ -805,13 +912,15 @@ class MainWindow(QMainWindow):
|
|
805
912
|
deleted_files.append(file_path)
|
806
913
|
self.file_model.update_cache_for_path(file_path)
|
807
914
|
except Exception as e:
|
808
|
-
self.
|
915
|
+
self._show_error_notification(
|
916
|
+
f"Error deleting {file_path}: {e}"
|
917
|
+
)
|
809
918
|
if deleted_files:
|
810
919
|
self._show_notification(
|
811
920
|
f"Deleted: {', '.join(os.path.basename(f) for f in deleted_files)}"
|
812
921
|
)
|
813
922
|
else:
|
814
|
-
self.
|
923
|
+
self._show_warning_notification("No segments to save.")
|
815
924
|
return
|
816
925
|
|
817
926
|
try:
|
@@ -828,9 +937,11 @@ class MainWindow(QMainWindow):
|
|
828
937
|
npz_path = self.file_manager.save_npz(
|
829
938
|
self.current_image_path, (h, w), class_order
|
830
939
|
)
|
831
|
-
self.
|
940
|
+
self._show_success_notification(
|
941
|
+
f"Saved: {os.path.basename(npz_path)}"
|
942
|
+
)
|
832
943
|
else:
|
833
|
-
self.
|
944
|
+
self._show_warning_notification("No classes defined for saving.")
|
834
945
|
if settings.get("save_txt", True):
|
835
946
|
h, w = (
|
836
947
|
self.viewer._pixmap_item.pixmap().height(),
|
@@ -861,7 +972,7 @@ class MainWindow(QMainWindow):
|
|
861
972
|
),
|
862
973
|
)
|
863
974
|
except Exception as e:
|
864
|
-
self.
|
975
|
+
self._show_error_notification(f"Error saving: {str(e)}")
|
865
976
|
|
866
977
|
def _handle_merge_press(self):
|
867
978
|
"""Handle merge key press."""
|
@@ -897,8 +1008,19 @@ class MainWindow(QMainWindow):
|
|
897
1008
|
|
898
1009
|
def _show_notification(self, message, duration=3000):
|
899
1010
|
"""Show notification message."""
|
900
|
-
self.
|
901
|
-
|
1011
|
+
self.status_bar.show_message(message, duration)
|
1012
|
+
|
1013
|
+
def _show_error_notification(self, message, duration=8000):
|
1014
|
+
"""Show error notification message."""
|
1015
|
+
self.status_bar.show_error_message(message, duration)
|
1016
|
+
|
1017
|
+
def _show_success_notification(self, message, duration=3000):
|
1018
|
+
"""Show success notification message."""
|
1019
|
+
self.status_bar.show_success_message(message, duration)
|
1020
|
+
|
1021
|
+
def _show_warning_notification(self, message, duration=5000):
|
1022
|
+
"""Show warning notification message."""
|
1023
|
+
self.status_bar.show_warning_message(message, duration)
|
902
1024
|
|
903
1025
|
def _show_hotkey_dialog(self):
|
904
1026
|
"""Show the hotkey configuration dialog."""
|
@@ -945,6 +1067,12 @@ class MainWindow(QMainWindow):
|
|
945
1067
|
|
946
1068
|
def closeEvent(self, event):
|
947
1069
|
"""Handle application close."""
|
1070
|
+
# Close any popped-out panels first
|
1071
|
+
if self.left_panel_popout is not None:
|
1072
|
+
self.left_panel_popout.close()
|
1073
|
+
if self.right_panel_popout is not None:
|
1074
|
+
self.right_panel_popout.close()
|
1075
|
+
|
948
1076
|
# Save settings
|
949
1077
|
self.settings.save_to_file(str(self.paths.settings_file))
|
950
1078
|
super().closeEvent(event)
|
@@ -1262,3 +1390,157 @@ class MainWindow(QMainWindow):
|
|
1262
1390
|
QPolygonF(self.segment_manager.segments[segment_index]["vertices"])
|
1263
1391
|
)
|
1264
1392
|
return
|
1393
|
+
|
1394
|
+
def _handle_class_toggle(self, class_id):
|
1395
|
+
"""Handle class toggle."""
|
1396
|
+
is_active = self.segment_manager.toggle_active_class(class_id)
|
1397
|
+
|
1398
|
+
if is_active:
|
1399
|
+
self._show_notification(f"Class {class_id} activated for new segments")
|
1400
|
+
# Update visual display
|
1401
|
+
self.right_panel.update_active_class_display(class_id)
|
1402
|
+
else:
|
1403
|
+
self._show_notification(
|
1404
|
+
"No active class - new segments will create new classes"
|
1405
|
+
)
|
1406
|
+
# Update visual display to clear active class
|
1407
|
+
self.right_panel.update_active_class_display(None)
|
1408
|
+
|
1409
|
+
def _pop_out_left_panel(self):
|
1410
|
+
"""Pop out the left control panel into a separate window."""
|
1411
|
+
if self.left_panel_popout is not None:
|
1412
|
+
# Panel is already popped out, return it to main window
|
1413
|
+
self._return_left_panel(self.control_panel)
|
1414
|
+
return
|
1415
|
+
|
1416
|
+
# Remove panel from main splitter
|
1417
|
+
self.control_panel.setParent(None)
|
1418
|
+
|
1419
|
+
# Create pop-out window
|
1420
|
+
self.left_panel_popout = PanelPopoutWindow(
|
1421
|
+
self.control_panel, "Control Panel", self
|
1422
|
+
)
|
1423
|
+
self.left_panel_popout.panel_closed.connect(self._return_left_panel)
|
1424
|
+
self.left_panel_popout.show()
|
1425
|
+
|
1426
|
+
# Update panel's pop-out button
|
1427
|
+
self.control_panel.set_popout_mode(True)
|
1428
|
+
|
1429
|
+
# Make pop-out window resizable
|
1430
|
+
self.left_panel_popout.setMinimumSize(200, 400)
|
1431
|
+
self.left_panel_popout.resize(self.control_panel.preferred_width + 20, 600)
|
1432
|
+
|
1433
|
+
def _pop_out_right_panel(self):
|
1434
|
+
"""Pop out the right panel into a separate window."""
|
1435
|
+
if self.right_panel_popout is not None:
|
1436
|
+
# Panel is already popped out, return it to main window
|
1437
|
+
self._return_right_panel(self.right_panel)
|
1438
|
+
return
|
1439
|
+
|
1440
|
+
# Remove panel from main splitter
|
1441
|
+
self.right_panel.setParent(None)
|
1442
|
+
|
1443
|
+
# Create pop-out window
|
1444
|
+
self.right_panel_popout = PanelPopoutWindow(
|
1445
|
+
self.right_panel, "File Explorer & Segments", self
|
1446
|
+
)
|
1447
|
+
self.right_panel_popout.panel_closed.connect(self._return_right_panel)
|
1448
|
+
self.right_panel_popout.show()
|
1449
|
+
|
1450
|
+
# Update panel's pop-out button
|
1451
|
+
self.right_panel.set_popout_mode(True)
|
1452
|
+
|
1453
|
+
# Make pop-out window resizable
|
1454
|
+
self.right_panel_popout.setMinimumSize(250, 400)
|
1455
|
+
self.right_panel_popout.resize(self.right_panel.preferred_width + 20, 600)
|
1456
|
+
|
1457
|
+
def _return_left_panel(self, panel_widget):
|
1458
|
+
"""Return the left panel to the main window."""
|
1459
|
+
if self.left_panel_popout is not None:
|
1460
|
+
# Close the pop-out window
|
1461
|
+
self.left_panel_popout.close()
|
1462
|
+
|
1463
|
+
# Return panel to main splitter
|
1464
|
+
self.main_splitter.insertWidget(0, self.control_panel)
|
1465
|
+
self.left_panel_popout = None
|
1466
|
+
|
1467
|
+
# Update panel's pop-out button
|
1468
|
+
self.control_panel.set_popout_mode(False)
|
1469
|
+
|
1470
|
+
# Restore splitter sizes
|
1471
|
+
self.main_splitter.setSizes([250, 800, 350])
|
1472
|
+
|
1473
|
+
def _handle_splitter_moved(self, pos, index):
|
1474
|
+
"""Handle splitter movement for intelligent expand/collapse behavior."""
|
1475
|
+
sizes = self.main_splitter.sizes()
|
1476
|
+
|
1477
|
+
# Left panel (index 0) - expand/collapse logic
|
1478
|
+
if index == 1: # Splitter between left panel and viewer
|
1479
|
+
left_size = sizes[0]
|
1480
|
+
# Only snap to collapsed if user drags very close to collapse
|
1481
|
+
if left_size < 50: # Collapsed threshold
|
1482
|
+
# Panel is being collapsed, snap to collapsed state
|
1483
|
+
new_sizes = [0] + sizes[1:]
|
1484
|
+
new_sizes[1] = new_sizes[1] + left_size # Give space back to viewer
|
1485
|
+
self.main_splitter.setSizes(new_sizes)
|
1486
|
+
# Temporarily override minimum width to allow collapsing
|
1487
|
+
self.control_panel.setMinimumWidth(0)
|
1488
|
+
|
1489
|
+
# Right panel (index 2) - expand/collapse logic
|
1490
|
+
elif index == 2: # Splitter between viewer and right panel
|
1491
|
+
right_size = sizes[2]
|
1492
|
+
# Only snap to collapsed if user drags very close to collapse
|
1493
|
+
if right_size < 50: # Collapsed threshold
|
1494
|
+
# Panel is being collapsed, snap to collapsed state
|
1495
|
+
new_sizes = sizes[:-1] + [0]
|
1496
|
+
new_sizes[1] = new_sizes[1] + right_size # Give space back to viewer
|
1497
|
+
self.main_splitter.setSizes(new_sizes)
|
1498
|
+
# Temporarily override minimum width to allow collapsing
|
1499
|
+
self.right_panel.setMinimumWidth(0)
|
1500
|
+
|
1501
|
+
def _expand_left_panel(self):
|
1502
|
+
"""Expand the left panel to its preferred width."""
|
1503
|
+
sizes = self.main_splitter.sizes()
|
1504
|
+
if sizes[0] < 50: # Only expand if currently collapsed
|
1505
|
+
# Restore minimum width first
|
1506
|
+
self.control_panel.setMinimumWidth(self.control_panel.preferred_width)
|
1507
|
+
|
1508
|
+
space_needed = self.control_panel.preferred_width
|
1509
|
+
viewer_width = sizes[1] - space_needed
|
1510
|
+
if viewer_width > 400: # Ensure viewer has minimum space
|
1511
|
+
new_sizes = [self.control_panel.preferred_width, viewer_width] + sizes[
|
1512
|
+
2:
|
1513
|
+
]
|
1514
|
+
self.main_splitter.setSizes(new_sizes)
|
1515
|
+
|
1516
|
+
def _expand_right_panel(self):
|
1517
|
+
"""Expand the right panel to its preferred width."""
|
1518
|
+
sizes = self.main_splitter.sizes()
|
1519
|
+
if sizes[2] < 50: # Only expand if currently collapsed
|
1520
|
+
# Restore minimum width first
|
1521
|
+
self.right_panel.setMinimumWidth(self.right_panel.preferred_width)
|
1522
|
+
|
1523
|
+
space_needed = self.right_panel.preferred_width
|
1524
|
+
viewer_width = sizes[1] - space_needed
|
1525
|
+
if viewer_width > 400: # Ensure viewer has minimum space
|
1526
|
+
new_sizes = sizes[:-1] + [
|
1527
|
+
viewer_width,
|
1528
|
+
self.right_panel.preferred_width,
|
1529
|
+
]
|
1530
|
+
self.main_splitter.setSizes(new_sizes)
|
1531
|
+
|
1532
|
+
def _return_right_panel(self, panel_widget):
|
1533
|
+
"""Return the right panel to the main window."""
|
1534
|
+
if self.right_panel_popout is not None:
|
1535
|
+
# Close the pop-out window
|
1536
|
+
self.right_panel_popout.close()
|
1537
|
+
|
1538
|
+
# Return panel to main splitter
|
1539
|
+
self.main_splitter.addWidget(self.right_panel)
|
1540
|
+
self.right_panel_popout = None
|
1541
|
+
|
1542
|
+
# Update panel's pop-out button
|
1543
|
+
self.right_panel.set_popout_mode(False)
|
1544
|
+
|
1545
|
+
# Restore splitter sizes
|
1546
|
+
self.main_splitter.setSizes([250, 800, 350])
|