|
31 | 31 | from PySide6.QtWidgets import ( |
32 | 32 | QCheckBox, |
33 | 33 | QComboBox, |
| 34 | + QDockWidget, |
34 | 35 | QFileDialog, |
35 | 36 | QFormLayout, |
36 | 37 | QGridLayout, |
@@ -231,28 +232,86 @@ def _load_icons(self): |
231 | 232 | self.setWindowIcon(QIcon(LOGO)) |
232 | 233 |
|
233 | 234 | def _setup_ui(self) -> None: |
234 | | - central = QWidget() |
235 | | - layout = QHBoxLayout(central) |
| 235 | + # central = QWidget() |
| 236 | + # layout = QHBoxLayout(central) |
236 | 237 |
|
237 | 238 | # Video panel with display and performance stats |
238 | 239 | video_panel = QWidget() |
239 | 240 | video_layout = QVBoxLayout(video_panel) |
240 | 241 | video_layout.setContentsMargins(0, 0, 0, 0) |
241 | | - |
242 | | - # Video display widget |
| 242 | + ## Video display widget |
243 | 243 | self.video_label = QLabel() |
244 | 244 | self.video_label.setAlignment(Qt.AlignmentFlag.AlignCenter) |
245 | 245 | self.video_label.setMinimumSize(640, 360) |
246 | 246 | self.video_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) |
247 | 247 | video_layout.addWidget(self.video_label, stretch=1) |
248 | | - |
249 | | - # Stats panel below video with clear labels |
| 248 | + ## Stats panel below video with clear labels |
250 | 249 | stats_widget = QWidget() |
251 | 250 | stats_widget.setStyleSheet("padding: 5px;") |
252 | 251 | # stats_widget.setMinimumWidth(800) # Prevent excessive line breaks |
253 | 252 | stats_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) |
254 | 253 | stats_widget.setMinimumHeight(80) |
| 254 | + self._build_stats_layout(stats_widget) |
| 255 | + |
| 256 | + video_layout.addWidget(stats_widget, stretch=0) |
| 257 | + |
| 258 | + # Central widget is just the video panel (video + stats) |
| 259 | + video_panel.setLayout(video_layout) |
| 260 | + self.setCentralWidget(video_panel) |
| 261 | + |
| 262 | + # Allow user to select stats text |
| 263 | + for lbl in (self.camera_stats_label, self.dlc_stats_label, self.recording_stats_label): |
| 264 | + lbl.setTextInteractionFlags(Qt.TextSelectableByMouse) |
| 265 | + |
| 266 | + # Controls panel with fixed width to prevent shifting |
| 267 | + controls_widget = QWidget() |
| 268 | + # controls_widget.setMaximumWidth(500) |
| 269 | + controls_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) |
| 270 | + controls_layout = QVBoxLayout(controls_widget) |
| 271 | + controls_layout.setContentsMargins(5, 5, 5, 5) |
| 272 | + controls_layout.addWidget(self._build_camera_group()) |
| 273 | + controls_layout.addWidget(self._build_dlc_group()) |
| 274 | + controls_layout.addWidget(self._build_recording_group()) |
| 275 | + controls_layout.addWidget(self._build_viz_group()) |
| 276 | + |
| 277 | + # Preview/Stop buttons at bottom of controls - wrap in widget |
| 278 | + button_bar_widget = QWidget() |
| 279 | + button_bar = QHBoxLayout(button_bar_widget) |
| 280 | + button_bar.setContentsMargins(0, 5, 0, 5) |
| 281 | + self.preview_button = QPushButton("Start Preview") |
| 282 | + self.preview_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay)) |
| 283 | + self.preview_button.setMinimumWidth(150) |
| 284 | + self.stop_preview_button = QPushButton("Stop Preview") |
| 285 | + self.stop_preview_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaStop)) |
| 286 | + self.stop_preview_button.setEnabled(False) |
| 287 | + self.stop_preview_button.setMinimumWidth(150) |
| 288 | + button_bar.addWidget(self.preview_button) |
| 289 | + button_bar.addWidget(self.stop_preview_button) |
| 290 | + controls_layout.addWidget(button_bar_widget) |
| 291 | + controls_layout.addStretch(1) |
| 292 | + |
| 293 | + # Add controls and video panel to main layout |
| 294 | + ## Dock widget for controls |
| 295 | + self.controls_dock = QDockWidget("Controls", self) |
| 296 | + self.controls_dock.setObjectName("ControlsDock") # important for state saving |
| 297 | + self.controls_dock.setWidget(controls_widget) |
| 298 | + ### Dock features |
| 299 | + self.controls_dock.setFeatures( # must not close independently |
| 300 | + QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable |
| 301 | + ) |
| 302 | + self.addDockWidget(Qt.LeftDockWidgetArea, self.controls_dock) |
| 303 | + self.setDockOptions( |
| 304 | + self.dockOptions() |
| 305 | + | QMainWindow.DockOption.AllowTabbedDocks |
| 306 | + | QMainWindow.DockOption.GroupedDragging |
| 307 | + | QMainWindow.DockOption.AnimatedDocks |
| 308 | + ) |
| 309 | + |
| 310 | + self.setStatusBar(QStatusBar()) |
| 311 | + self._build_menus() |
| 312 | + QTimer.singleShot(0, self._show_logo_and_text) |
255 | 313 |
|
| 314 | + def _build_stats_layout(self, stats_widget: QWidget) -> QGridLayout: |
256 | 315 | stats_layout = QGridLayout(stats_widget) |
257 | 316 | stats_layout.setContentsMargins(5, 5, 5, 5) |
258 | 317 | stats_layout.setHorizontalSpacing(8) # tighten horizontal gap between title and value |
@@ -295,49 +354,8 @@ def _setup_ui(self) -> None: |
295 | 354 | # Critical: make column 1 (values) eat the width, keep column 0 tight |
296 | 355 | stats_layout.setColumnStretch(0, 0) |
297 | 356 | stats_layout.setColumnStretch(1, 1) |
298 | | - video_layout.addWidget(stats_widget, stretch=0) |
299 | | - |
300 | | - # Allow user to select stats text |
301 | | - for lbl in (self.camera_stats_label, self.dlc_stats_label, self.recording_stats_label): |
302 | | - lbl.setTextInteractionFlags(Qt.TextSelectableByMouse) |
303 | 357 |
|
304 | | - # Controls panel with fixed width to prevent shifting |
305 | | - controls_widget = QWidget() |
306 | | - controls_widget.setMaximumWidth(500) |
307 | | - controls_widget.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding) |
308 | | - controls_layout = QVBoxLayout(controls_widget) |
309 | | - controls_layout.setContentsMargins(5, 5, 5, 5) |
310 | | - controls_layout.addWidget(self._build_camera_group()) |
311 | | - controls_layout.addWidget(self._build_dlc_group()) |
312 | | - controls_layout.addWidget(self._build_recording_group()) |
313 | | - controls_layout.addWidget(self._build_viz_group()) |
314 | | - |
315 | | - # Preview/Stop buttons at bottom of controls - wrap in widget |
316 | | - button_bar_widget = QWidget() |
317 | | - button_bar = QHBoxLayout(button_bar_widget) |
318 | | - button_bar.setContentsMargins(0, 5, 0, 5) |
319 | | - self.preview_button = QPushButton("Start Preview") |
320 | | - self.preview_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay)) |
321 | | - self.preview_button.setMinimumWidth(150) |
322 | | - self.stop_preview_button = QPushButton("Stop Preview") |
323 | | - self.stop_preview_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaStop)) |
324 | | - self.stop_preview_button.setEnabled(False) |
325 | | - self.stop_preview_button.setMinimumWidth(150) |
326 | | - button_bar.addWidget(self.preview_button) |
327 | | - button_bar.addWidget(self.stop_preview_button) |
328 | | - controls_layout.addWidget(button_bar_widget) |
329 | | - controls_layout.addStretch(1) |
330 | | - |
331 | | - # Add controls and video panel to main layout |
332 | | - layout.addWidget(controls_widget, stretch=0) |
333 | | - layout.addWidget(video_panel, stretch=1) |
334 | | - layout.setStretch(0, 0) |
335 | | - layout.setStretch(1, 1) |
336 | | - |
337 | | - self.setCentralWidget(central) |
338 | | - self.setStatusBar(QStatusBar()) |
339 | | - self._build_menus() |
340 | | - QTimer.singleShot(0, self._show_logo_and_text) |
| 358 | + stats_widget.setLayout(stats_layout) |
341 | 359 |
|
342 | 360 | def _build_menus(self) -> None: |
343 | 361 | # File menu |
@@ -365,6 +383,7 @@ def _build_menus(self) -> None: |
365 | 383 |
|
366 | 384 | # View menu |
367 | 385 | view_menu = self.menuBar().addMenu("&View") |
| 386 | + view_menu.addAction(self.controls_dock.toggleViewAction()) |
368 | 387 | appearance_menu = view_menu.addMenu("Appearance") |
369 | 388 | ## Style actions |
370 | 389 | self.action_dark_mode = QAction("Dark theme", self, checkable=True) |
@@ -437,24 +456,26 @@ def _build_dlc_group(self) -> QGroupBox: |
437 | 456 | processor_path_layout.addWidget(self.refresh_processors_button) |
438 | 457 | form.addRow("Processor folder", processor_path_layout) |
439 | 458 |
|
440 | | - self.processor_combo = color_ui.ShrinkCurrentWidePopupComboBox() |
| 459 | + self.processor_combo = color_ui.ShrinkCurrentWidePopupComboBox(sizing=color_ui.ComboSizing(max_width=100)) |
441 | 460 | self.processor_combo.addItem("No Processor", None) |
442 | 461 | # form.addRow("Processor", self.processor_combo) |
443 | 462 |
|
444 | 463 | # self.additional_options_edit = QPlainTextEdit() |
445 | 464 | # self.additional_options_edit.setPlaceholderText("") |
446 | 465 | # self.additional_options_edit.setFixedHeight(40) |
447 | 466 | # form.addRow("Additional options", self.additional_options_edit) |
448 | | - self.dlc_camera_combo = color_ui.ShrinkCurrentWidePopupComboBox() |
| 467 | + self.dlc_camera_combo = color_ui.ShrinkCurrentWidePopupComboBox(sizing=color_ui.ComboSizing(max_width=180)) |
449 | 468 | self.dlc_camera_combo.setToolTip("Select which camera to use for pose inference") |
450 | 469 | # form.addRow("Inference camera", self.dlc_camera_combo) |
| 470 | + self.dlc_camera_combo.setPlaceholderText("None") |
451 | 471 | processing_sttgs = lyts.make_two_field_row( |
452 | 472 | "Inference camera", |
453 | 473 | self.dlc_camera_combo, |
454 | 474 | "Processor", |
455 | 475 | self.processor_combo, |
456 | 476 | key_width=None, |
457 | 477 | ) |
| 478 | + self.dlc_camera_combo.update_shrink_width() |
458 | 479 | form.addRow(processing_sttgs) |
459 | 480 |
|
460 | 481 | # Wrap inference buttons in a widget to prevent shifting |
@@ -751,6 +772,7 @@ def _connect_signals(self) -> None: |
751 | 772 | self._dlc.error.connect(self._on_dlc_error) |
752 | 773 | self._dlc.initialized.connect(self._on_dlc_initialised) |
753 | 774 | self.dlc_camera_combo.currentIndexChanged.connect(self._on_dlc_camera_changed) |
| 775 | + self.dlc_camera_combo.currentTextChanged.connect(self.dlc_camera_combo.update_shrink_width) |
754 | 776 |
|
755 | 777 | # Recording settings |
756 | 778 | ## Session name persistence + preview updates |
@@ -1041,6 +1063,7 @@ def _refresh_processors(self) -> None: |
1041 | 1063 | display_name = f"{info['name']} ({info['file']})" |
1042 | 1064 | self.processor_combo.addItem(display_name, key) |
1043 | 1065 |
|
| 1066 | + self.processor_combo.update_shrink_width() |
1044 | 1067 | self.statusBar().showMessage( |
1045 | 1068 | f"Found {len(self._processor_keys)} processor(s) in package dlclivegui.processors", 3000 |
1046 | 1069 | ) |
@@ -1250,10 +1273,12 @@ def _refresh_dlc_camera_list(self) -> None: |
1250 | 1273 | self._inference_camera_id = self.dlc_camera_combo.currentData() |
1251 | 1274 |
|
1252 | 1275 | self.dlc_camera_combo.blockSignals(False) |
| 1276 | + self.dlc_camera_combo.update_shrink_width() |
1253 | 1277 |
|
1254 | 1278 | def _on_dlc_camera_changed(self, _index: int) -> None: |
1255 | 1279 | """Track user selection of the inference camera.""" |
1256 | 1280 | self._inference_camera_id = self.dlc_camera_combo.currentData() |
| 1281 | + self.dlc_camera_combo.update_shrink_width() |
1257 | 1282 | # Force redraw so bbox/pose overlays switch to the new tile immediately |
1258 | 1283 | if self._current_frame is not None: |
1259 | 1284 | self._display_frame(self._current_frame, force=True) |
@@ -1973,6 +1998,9 @@ def closeEvent(self, event: QCloseEvent) -> None: # pragma: no cover - GUI beha |
1973 | 1998 | if hasattr(self, "_metrics_timer"): |
1974 | 1999 | self._metrics_timer.stop() |
1975 | 2000 |
|
| 2001 | + if hasattr(self, "_display_timer"): |
| 2002 | + self._display_timer.stop() |
| 2003 | + |
1976 | 2004 | # Remember model path on exit |
1977 | 2005 | self._model_path_store.save_if_valid(self.model_path_edit.text().strip()) |
1978 | 2006 |
|
|
0 commit comments