summaryrefslogtreecommitdiff
path: root/ui/main_window.py
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-09-20 23:45:18 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-09-20 23:45:18 -0600
commita0473245f1f453bfc87ea1bc9bd479311e8a944b (patch)
tree5c317d6b39c436fb58824e0553d7ba4603cc0829 /ui/main_window.py
parent006366ef8099c8313ae596bf6267e9f98dfbeb5e (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.py36
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