Skip to content

Commit e9b5d52

Browse files
committed
Make controls dockable and refine UI layout
Replace the previous side-panel layout with a QDockWidget-based controls panel to allow docking/undocking and prevent UI shifting. Extract stats layout into _build_stats_layout and enable selectable stats text. Add sizing/shrink options and placeholder for processor and camera combo boxes and call update_shrink_width at key points so combo widths adapt. Add controls toggle to the View menu, set dock features/options, and give the dock a stable objectName for state saving. Also stop the display timer on shutdown and perform minor UI/layout cleanups and refactors (imports and button/preview layout adjustments).
1 parent 81b0f4e commit e9b5d52

1 file changed

Lines changed: 78 additions & 50 deletions

File tree

dlclivegui/gui/main_window.py

Lines changed: 78 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from PySide6.QtWidgets import (
3232
QCheckBox,
3333
QComboBox,
34+
QDockWidget,
3435
QFileDialog,
3536
QFormLayout,
3637
QGridLayout,
@@ -231,28 +232,86 @@ def _load_icons(self):
231232
self.setWindowIcon(QIcon(LOGO))
232233

233234
def _setup_ui(self) -> None:
234-
central = QWidget()
235-
layout = QHBoxLayout(central)
235+
# central = QWidget()
236+
# layout = QHBoxLayout(central)
236237

237238
# Video panel with display and performance stats
238239
video_panel = QWidget()
239240
video_layout = QVBoxLayout(video_panel)
240241
video_layout.setContentsMargins(0, 0, 0, 0)
241-
242-
# Video display widget
242+
## Video display widget
243243
self.video_label = QLabel()
244244
self.video_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
245245
self.video_label.setMinimumSize(640, 360)
246246
self.video_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
247247
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
250249
stats_widget = QWidget()
251250
stats_widget.setStyleSheet("padding: 5px;")
252251
# stats_widget.setMinimumWidth(800) # Prevent excessive line breaks
253252
stats_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
254253
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)
255313

314+
def _build_stats_layout(self, stats_widget: QWidget) -> QGridLayout:
256315
stats_layout = QGridLayout(stats_widget)
257316
stats_layout.setContentsMargins(5, 5, 5, 5)
258317
stats_layout.setHorizontalSpacing(8) # tighten horizontal gap between title and value
@@ -295,49 +354,8 @@ def _setup_ui(self) -> None:
295354
# Critical: make column 1 (values) eat the width, keep column 0 tight
296355
stats_layout.setColumnStretch(0, 0)
297356
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)
303357

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)
341359

342360
def _build_menus(self) -> None:
343361
# File menu
@@ -365,6 +383,7 @@ def _build_menus(self) -> None:
365383

366384
# View menu
367385
view_menu = self.menuBar().addMenu("&View")
386+
view_menu.addAction(self.controls_dock.toggleViewAction())
368387
appearance_menu = view_menu.addMenu("Appearance")
369388
## Style actions
370389
self.action_dark_mode = QAction("Dark theme", self, checkable=True)
@@ -437,24 +456,26 @@ def _build_dlc_group(self) -> QGroupBox:
437456
processor_path_layout.addWidget(self.refresh_processors_button)
438457
form.addRow("Processor folder", processor_path_layout)
439458

440-
self.processor_combo = color_ui.ShrinkCurrentWidePopupComboBox()
459+
self.processor_combo = color_ui.ShrinkCurrentWidePopupComboBox(sizing=color_ui.ComboSizing(max_width=100))
441460
self.processor_combo.addItem("No Processor", None)
442461
# form.addRow("Processor", self.processor_combo)
443462

444463
# self.additional_options_edit = QPlainTextEdit()
445464
# self.additional_options_edit.setPlaceholderText("")
446465
# self.additional_options_edit.setFixedHeight(40)
447466
# 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))
449468
self.dlc_camera_combo.setToolTip("Select which camera to use for pose inference")
450469
# form.addRow("Inference camera", self.dlc_camera_combo)
470+
self.dlc_camera_combo.setPlaceholderText("None")
451471
processing_sttgs = lyts.make_two_field_row(
452472
"Inference camera",
453473
self.dlc_camera_combo,
454474
"Processor",
455475
self.processor_combo,
456476
key_width=None,
457477
)
478+
self.dlc_camera_combo.update_shrink_width()
458479
form.addRow(processing_sttgs)
459480

460481
# Wrap inference buttons in a widget to prevent shifting
@@ -751,6 +772,7 @@ def _connect_signals(self) -> None:
751772
self._dlc.error.connect(self._on_dlc_error)
752773
self._dlc.initialized.connect(self._on_dlc_initialised)
753774
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)
754776

755777
# Recording settings
756778
## Session name persistence + preview updates
@@ -1041,6 +1063,7 @@ def _refresh_processors(self) -> None:
10411063
display_name = f"{info['name']} ({info['file']})"
10421064
self.processor_combo.addItem(display_name, key)
10431065

1066+
self.processor_combo.update_shrink_width()
10441067
self.statusBar().showMessage(
10451068
f"Found {len(self._processor_keys)} processor(s) in package dlclivegui.processors", 3000
10461069
)
@@ -1250,10 +1273,12 @@ def _refresh_dlc_camera_list(self) -> None:
12501273
self._inference_camera_id = self.dlc_camera_combo.currentData()
12511274

12521275
self.dlc_camera_combo.blockSignals(False)
1276+
self.dlc_camera_combo.update_shrink_width()
12531277

12541278
def _on_dlc_camera_changed(self, _index: int) -> None:
12551279
"""Track user selection of the inference camera."""
12561280
self._inference_camera_id = self.dlc_camera_combo.currentData()
1281+
self.dlc_camera_combo.update_shrink_width()
12571282
# Force redraw so bbox/pose overlays switch to the new tile immediately
12581283
if self._current_frame is not None:
12591284
self._display_frame(self._current_frame, force=True)
@@ -1973,6 +1998,9 @@ def closeEvent(self, event: QCloseEvent) -> None: # pragma: no cover - GUI beha
19731998
if hasattr(self, "_metrics_timer"):
19741999
self._metrics_timer.stop()
19752000

2001+
if hasattr(self, "_display_timer"):
2002+
self._display_timer.stop()
2003+
19762004
# Remember model path on exit
19772005
self._model_path_store.save_if_valid(self.model_path_edit.text().strip())
19782006

0 commit comments

Comments
 (0)