From d0e128709219834e5737a3dd8b44368ba64db830 Mon Sep 17 00:00:00 2001 From: Stevenson Chittumuri Date: Mon, 22 Jun 2026 00:26:10 -0400 Subject: [PATCH] feat(ui): quick-collapse the left and right side panels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a fast way to hide/show the two side panels, requested for tighter multi-camera layouts: - View menu: checkable "Show Left Panel" (Ctrl+Alt+[) and "Show Right Panel" (Ctrl+Alt+]). The right panel is the tabbed Camera Info / People / Services group, toggled as one unit; the left is Properties. - Status bar: ◧ / ◨ toggle buttons next to the Logs button. - Menu checkmarks and status-bar buttons stay in sync with the docks however they're toggled (menu, button, shortcut, or a panel's close box), via each dock's visibilityChanged. Co-Authored-By: Claude Opus 4.8 --- autoptz/ui/widgets/main_window.py | 68 +++++++++++++++++++++++++++++++ autoptz/ui/widgets/status_bar.py | 36 ++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/autoptz/ui/widgets/main_window.py b/autoptz/ui/widgets/main_window.py index f6c14818..09ba1ad9 100644 --- a/autoptz/ui/widgets/main_window.py +++ b/autoptz/ui/widgets/main_window.py @@ -348,6 +348,23 @@ def _build_menus(self) -> None: act.toggled.connect(lambda on, k=key: self._client.setOverlay(k, on)) overlays.addAction(act) + # Quick collapse/expand of the two side panels (mirrored on the status bar). + view.addSeparator() + self._act_toggle_left = QAction("Show Left Panel", self, checkable=True) + self._act_toggle_left.setShortcut("Ctrl+Alt+[") + self._act_toggle_left.setToolTip("Show or hide the left Properties panel.") + self._act_toggle_left.setStatusTip(self._act_toggle_left.toolTip()) + self._act_toggle_left.toggled.connect(self._set_left_panel_visible) + view.addAction(self._act_toggle_left) + self._act_toggle_right = QAction("Show Right Panel", self, checkable=True) + self._act_toggle_right.setShortcut("Ctrl+Alt+]") + self._act_toggle_right.setToolTip( + "Show or hide the right Camera Info / People / Services panel." + ) + self._act_toggle_right.setStatusTip(self._act_toggle_right.toolTip()) + self._act_toggle_right.toggled.connect(self._set_right_panel_visible) + view.addAction(self._act_toggle_right) + # Panels (dock toggles) and Layouts are view concerns, so they live under # View rather than as separate top-level menus. view.addSeparator() @@ -837,6 +854,8 @@ def _build_status_bar(self) -> None: self._client, logs_toggle=self._toggle_logs, cameras_popup=self._open_cameras_menu, + left_toggle=self._set_left_panel_visible, + right_toggle=self._set_right_panel_visible, ) self.statusBar().addPermanentWidget(self._status, 1) self.statusBar().setSizeGripEnabled(False) @@ -844,6 +863,13 @@ def _build_status_bar(self) -> None: if logs is not None: logs.visibilityChanged.connect(self._status.set_logs_visible) self._status.set_logs_visible(logs.isVisible()) + # Keep both the menu checkmarks and the status-bar buttons in sync with the + # side panels however they're toggled (menu, button, shortcut, or close box). + for key in ("properties", *self._RIGHT_PANEL_KEYS): + dock = self._docks.get(key) + if dock is not None: + dock.visibilityChanged.connect(lambda _v: self._sync_panel_toggles()) + self._sync_panel_toggles() def _toggle_logs(self, shown: bool | None = None) -> None: dock = self._docks.get("logs") @@ -856,6 +882,48 @@ def _toggle_logs(self, shown: bool | None = None) -> None: else: dock.hide() + # ── side-panel collapse ────────────────────────────────────────────────────── + + #: The right-hand dock group is three tabbed panels toggled as one unit. + _RIGHT_PANEL_KEYS = ("camera_info", "people", "services") + + def _set_left_panel_visible(self, shown: bool) -> None: + dock = self._docks.get("properties") + if dock is None: + return + if shown: + dock.show() + dock.raise_() + else: + dock.hide() + + def _set_right_panel_visible(self, shown: bool) -> None: + docks = [d for k in self._RIGHT_PANEL_KEYS if (d := self._docks.get(k)) is not None] + for dock in docks: + dock.setVisible(shown) + if shown and docks: + docks[0].raise_() + + def _sync_panel_toggles(self) -> None: + """Keep the menu checkmarks and status-bar buttons in step with the docks.""" + left = self._docks.get("properties") + left_on = bool(left is not None and left.isVisible()) + right_on = any( + (d := self._docks.get(k)) is not None and d.isVisible() for k in self._RIGHT_PANEL_KEYS + ) + for act, on in ( + (getattr(self, "_act_toggle_left", None), left_on), + (getattr(self, "_act_toggle_right", None), right_on), + ): + if act is not None: + blocked = act.blockSignals(True) + act.setChecked(on) + act.blockSignals(blocked) + status = getattr(self, "_status", None) + if status is not None: + status.set_left_visible(left_on) + status.set_right_visible(right_on) + # ── selection routing ────────────────────────────────────────────────────── def _on_camera_selected(self, camera_id: str) -> None: diff --git a/autoptz/ui/widgets/status_bar.py b/autoptz/ui/widgets/status_bar.py index e151efad..8de084ff 100644 --- a/autoptz/ui/widgets/status_bar.py +++ b/autoptz/ui/widgets/status_bar.py @@ -28,6 +28,8 @@ def __init__( client: Any, logs_toggle: Callable[[bool], None] | None = None, cameras_popup: Callable[..., None] | None = None, + left_toggle: Callable[[bool], None] | None = None, + right_toggle: Callable[[bool], None] | None = None, parent: QWidget | None = None, ) -> None: super().__init__(parent) @@ -65,6 +67,26 @@ def __init__( self._summary = QLabel("") row.addWidget(self._summary) + # Quick collapse/expand of the side panels (also in View and on shortcuts). + self._left_btn: QPushButton | None = None + self._right_btn: QPushButton | None = None + if left_toggle is not None or right_toggle is not None: + row.addWidget(self._make_sep()) + if left_toggle is not None: + self._left_btn = QPushButton("◧") + self._left_btn.setCheckable(True) + self._left_btn.setToolTip("Show or hide the left Properties panel.") + self._left_btn.toggled.connect(left_toggle) + row.addWidget(self._left_btn) + if right_toggle is not None: + self._right_btn = QPushButton("◨") + self._right_btn.setCheckable(True) + self._right_btn.setToolTip( + "Show or hide the right Camera Info / People / Services panel." + ) + self._right_btn.toggled.connect(right_toggle) + row.addWidget(self._right_btn) + self._logs_btn: QPushButton | None = None if logs_toggle is not None: row.addWidget(self._make_sep()) @@ -116,6 +138,12 @@ def _on_logs_toggled(self, shown: bool) -> None: if self._logs_toggle is not None: self._logs_toggle(shown) + def set_left_visible(self, shown: bool) -> None: + _set_checked(self._left_btn, shown) + + def set_right_visible(self, shown: bool) -> None: + _set_checked(self._right_btn, shown) + # ── refreshers ─────────────────────────────────────────────────────────────── def refresh(self) -> None: @@ -202,3 +230,11 @@ def _safe(fn: Any, default: Any) -> Any: return fn() except Exception: # noqa: BLE001 return default + + +def _set_checked(btn: QPushButton | None, checked: bool) -> None: + """Set a checkable button's state without re-emitting ``toggled``.""" + if btn is not None: + was = btn.blockSignals(True) + btn.setChecked(checked) + btn.blockSignals(was)