diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-09-20 23:45:18 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-09-20 23:45:18 -0600 |
| commit | a0473245f1f453bfc87ea1bc9bd479311e8a944b (patch) | |
| tree | 5c317d6b39c436fb58824e0553d7ba4603cc0829 /ui/main_window.py | |
| parent | 006366ef8099c8313ae596bf6267e9f98dfbeb5e (diff) | |
Persist dock layout to profiles; dedupe port names; Channels-tab Create button
- Fix duplicated device names ("FT231X USB UART - FT231X USB UART (...)")
— pyserial on Linux often reports the same udev model string twice for
cheap FTDI/CH340-style adapters. Collapse it at the list_ports() source
so every consumer (device auto-name, port picker) benefits.
- Save Control Panel / Namespace dock area, floating geometry, visibility,
size and order to .labui profiles via QMainWindow.saveState()/
restoreState(), round-tripped through the existing Profile.settings
passthrough (self._settings) — no profile schema change needed.
- Channels tab: replace the per-row Signal ⊕ button (and its pinned/
frozen-column overlay) with a single "Create Channel" button below the
Namespace panel. Select one or more Signal/Channel rows (multi-select
now supported) and click it — selected rows become the new Channel's
sources, or an empty Channel is created if nothing's selected. Fixes a
bug in the process: the multi/zero-source path defaulted to
kind="expression" with no expression text, which fails to compile and
silently dropped the channel; use kind="none" instead, which the user
then edits in the Channels pane.
NamespacePanel itself (ui/namespace_panel.py) goes back to a plain
read-only reference widget — the create-channel button and its logic
live entirely in ConfigWindow (config_window.py), so the shared
dockable Namespace panel on the main window is unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'ui/main_window.py')
| -rw-r--r-- | ui/main_window.py | 36 |
1 files changed, 35 insertions, 1 deletions
diff --git a/ui/main_window.py b/ui/main_window.py index ba93121..6dae1be 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -12,9 +12,10 @@ from PyQt6.QtWidgets import ( QToolBar, QSizePolicy, QApplication, QFrame, QAbstractSpinBox, QComboBox, QMenu, ) -from PyQt6.QtCore import Qt, QTimer, QObject, QEvent, QRect, pyqtSignal, pyqtSlot +from PyQt6.QtCore import Qt, QTimer, QObject, QEvent, QRect, QByteArray, pyqtSignal, pyqtSlot from PyQt6.QtGui import QPainter, QColor, QFont, QAction import logging +import base64 import os @@ -116,6 +117,12 @@ _LIGHT_QSS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "style_lig class MainWindow(QMainWindow): + # Bump when dock/toolbar structure changes in a way that would make an + # old saved dock_state blob meaningless (e.g. a dock renamed/removed) — + # restoreState() rejects a mismatched version and just no-ops instead + # of applying garbage. + _DOCK_STATE_VERSION = 1 + def __init__(self): super().__init__() self.setWindowTitle("LabUI") @@ -622,6 +629,7 @@ class MainWindow(QMainWindow): def _profile_capture(self, name: str = "Profile") -> Profile: """Serialise current state into a Profile object.""" + self._capture_dock_state() return ProfileManager.capture( registry=self.registry, processor=self.processor, @@ -702,6 +710,7 @@ class MainWindow(QMainWindow): self._win_config.refresh_devices() self._win_config.refresh_derived() self._win_config.refresh_plot(self._chart._cfg) + QTimer.singleShot(0, self._apply_dock_state) self._status.setText(f"Profile loaded: {profile.name}") # ── Device / signal events ──────────────────────────────────────────── @@ -819,6 +828,31 @@ class MainWindow(QMainWindow): self._apply_theme(self._settings.get("theme", "dark")) self.engine._interval = self._settings.get("poll_ms", 100) / 1000.0 self._apply_developer_mode(self._settings.get("developer_mode", False)) + QTimer.singleShot(0, self._apply_dock_state) + + def _capture_dock_state(self): + """Snapshot Control Panel / Namespace dock area, floating geometry, + visibility, size and order via Qt's own QMainWindow.saveState() — + one call covers everything a QDockWidget can have moved, instead of + hand-tracking each attribute. Stored as base64 in self._settings, + which both app_settings.json (save_settings()) and .labui profiles + (Profile.settings — see ProfileManager.capture()) already round-trip + verbatim, so this needs no schema changes on either side, just + needs to run before whichever save happens.""" + raw = bytes(self.saveState(self._DOCK_STATE_VERSION)) + self._settings["dock_state"] = base64.b64encode(raw).decode("ascii") + + def _apply_dock_state(self): + """Restore saved dock state — deferred one event-loop tick (via + QTimer.singleShot(0, ...) at call sites) since restoreState() before + the main window's initial layout has settled doesn't stick, the + same reason splitter sizing elsewhere in this codebase (e.g. + plot_window.py's _init_split) defers similarly.""" + encoded = self._settings.get("dock_state") + if not encoded: + return + raw = QByteArray(base64.b64decode(encoded)) + self.restoreState(raw, self._DOCK_STATE_VERSION) def _apply_developer_mode(self, enabled: bool): level = logging.DEBUG if enabled else logging.WARNING |
