summaryrefslogtreecommitdiff
path: root/ui/main_window.py
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-04-21 13:43:52 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-04-21 13:57:46 -0600
commit2a634dca0c7962b90004f75c4cdac6225201bb5e (patch)
tree3e1f0cd4ff165e77d17ebec5278c9bed885bb1da /ui/main_window.py
parentb1a61fd29e2282110bc4f4bc4616c55ed9d88dbb (diff)
V13
This version contains the profile feature. Allowins users to save the channels, signals and plots for specific labs.
Diffstat (limited to 'ui/main_window.py')
-rw-r--r--ui/main_window.py136
1 files changed, 84 insertions, 52 deletions
diff --git a/ui/main_window.py b/ui/main_window.py
index e69ab1a..a82a389 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -1,21 +1,15 @@
"""
ui/main_window.py
-Toolbar sections (left→right):
- [▶ RUN] [⬤ LOG] | [⚙ DEVICES] | [⚗ SIGNALS] | [📐 PLOT] | [⚙ SETTINGS]
- spacer | timer
-
-Central:
- Left: ControlPanel (output widgets only)
- Center: StripChartWidget
-
-All secondary features live in floating windows.
+Toolbar (left→right):
+ [▶ RUN] [⬤ LOG] | [⊞ Devices] | [⚗ Signals] | [📐 Plot] | [⚙ Settings]
+ spacer | timer | [📁 File]
"""
from PyQt6.QtWidgets import (
QMainWindow, QWidget, QHBoxLayout, QVBoxLayout,
QSplitter, QStatusBar, QLabel, QPushButton,
- QToolBar, QSizePolicy, QApplication,
+ QToolBar, QSizePolicy, QApplication, QFrame,
)
from PyQt6.QtCore import Qt, QTimer, pyqtSlot
import os
@@ -26,13 +20,15 @@ from devices.serial_device import SerialDevice
from devices.device_registry import DeviceRegistry
from core.acquisition import AcquisitionEngine
from core.signal_processor import SignalProcessor
+from core.profile import Profile, ProfileManager
-from ui.control_panel import ControlPanel
-from ui.strip_chart import StripChartWidget
-from ui.windows.devices_window import DevicesWindow
-from ui.windows.signals_window import SignalsWindow
-from ui.windows.plot_window import PlotWindow, build_default_layout
-from ui.windows.settings_window import SettingsWindow
+from ui.control_panel import ControlPanel
+from ui.strip_chart import StripChartWidget
+from ui.profile_manager_ui import ProfileButton
+from ui.windows.devices_window import DevicesWindow
+from ui.windows.signals_window import SignalsWindow
+from ui.windows.plot_window import PlotWindow, build_default_layout
+from ui.windows.settings_window import SettingsWindow
_DARK_QSS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "style_dark.qss")
@@ -50,11 +46,11 @@ class MainWindow(QMainWindow):
self.processor = SignalProcessor()
self._settings = dict(SettingsWindow._defaults)
- self._elapsed = 0
- self._win_devices = None
- self._win_signals = None
- self._win_plot = None
- self._win_settings = None
+ self._elapsed = 0
+ self._win_devices = None
+ self._win_signals = None
+ self._win_plot = None
+ self._win_settings = None
self._init_demo_devices()
self._build_ui()
@@ -76,7 +72,6 @@ class MainWindow(QMainWindow):
# ── UI ────────────────────────────────────────────────────────────────
def _build_ui(self):
- # ── Toolbar ──────────────────────────────────────────────────────
tb = QToolBar(); tb.setObjectName("mainToolbar"); tb.setMovable(False)
self.addToolBar(tb)
@@ -84,7 +79,6 @@ class MainWindow(QMainWindow):
s = QFrame(); s.setFrameShape(QFrame.Shape.VLine)
s.setObjectName("toolbarSep"); return s
- # RUN / LOG
self._run_btn = QPushButton("▶ RUN")
self._run_btn.setObjectName("runButton"); self._run_btn.setCheckable(True)
self._run_btn.clicked.connect(self._toggle_run); tb.addWidget(self._run_btn)
@@ -96,46 +90,50 @@ class MainWindow(QMainWindow):
tb.addWidget(_sep())
- # DEVICES
dev_btn = QPushButton("⊞ Devices")
- dev_btn.setObjectName("toolbarSectionBtn")
- dev_btn.setCheckable(True)
+ dev_btn.setObjectName("toolbarSectionBtn"); dev_btn.setCheckable(True)
dev_btn.clicked.connect(lambda c: self._toggle_win("devices", c, dev_btn))
tb.addWidget(dev_btn); self._btn_devices = dev_btn
tb.addWidget(_sep())
- # SIGNALS
sig_btn = QPushButton("⚗ Signals")
- sig_btn.setObjectName("toolbarSectionBtn")
- sig_btn.setCheckable(True)
+ sig_btn.setObjectName("toolbarSectionBtn"); sig_btn.setCheckable(True)
sig_btn.clicked.connect(lambda c: self._toggle_win("signals", c, sig_btn))
tb.addWidget(sig_btn); self._btn_signals = sig_btn
tb.addWidget(_sep())
- # PLOT
plot_btn = QPushButton("📐 Plot")
- plot_btn.setObjectName("toolbarSectionBtn")
- plot_btn.setCheckable(True)
+ plot_btn.setObjectName("toolbarSectionBtn"); plot_btn.setCheckable(True)
plot_btn.clicked.connect(lambda c: self._toggle_win("plot", c, plot_btn))
tb.addWidget(plot_btn); self._btn_plot = plot_btn
tb.addWidget(_sep())
- # SETTINGS
set_btn = QPushButton("⚙ Settings")
- set_btn.setObjectName("toolbarSectionBtn")
- set_btn.setCheckable(True)
+ set_btn.setObjectName("toolbarSectionBtn"); set_btn.setCheckable(True)
set_btn.clicked.connect(lambda c: self._toggle_win("settings", c, set_btn))
tb.addWidget(set_btn); self._btn_settings = set_btn
- spacer = QWidget(); spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
+ # Spacer + clock
+ spacer = QWidget()
+ spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
tb.addWidget(spacer)
self._time_lbl = QLabel("00:00:00"); self._time_lbl.setObjectName("timeLabel")
tb.addWidget(self._time_lbl)
+ tb.addWidget(_sep())
+
+ # ── 📁 File button (profiles) ─────────────────────────────────────
+ self._file_btn = ProfileButton(
+ on_new=self._profile_new,
+ get_profile=self._profile_capture,
+ apply_profile=self._profile_apply,
+ )
+ tb.addWidget(self._file_btn)
+
# ── Central ───────────────────────────────────────────────────────
central = QWidget(); self.setCentralWidget(central)
root = QHBoxLayout(central); root.setContentsMargins(0,0,0,0); root.setSpacing(0)
@@ -143,16 +141,16 @@ class MainWindow(QMainWindow):
hsplit = QSplitter(Qt.Orientation.Horizontal); hsplit.setHandleWidth(3)
self._ctrl = ControlPanel(self.registry)
- self._ctrl.setMinimumWidth(200); self._ctrl.setMaximumWidth(320)
+ self._ctrl.setMinimumWidth(200); self._ctrl.setMaximumWidth(340)
+ self._ctrl._add_demo_widgets()
hsplit.addWidget(self._ctrl)
self._chart = StripChartWidget(self.engine, self.registry, self.processor)
hsplit.addWidget(self._chart)
- hsplit.setSizes([240, 1000])
+ hsplit.setSizes([260, 1000])
root.addWidget(hsplit)
- # ── Status bar ─────────────────────────────────────────────────
sb = QStatusBar(); self.setStatusBar(sb)
self._status = QLabel("Ready"); sb.addWidget(self._status)
self._log_lbl = QLabel(""); sb.addPermanentWidget(self._log_lbl)
@@ -229,11 +227,51 @@ class MainWindow(QMainWindow):
if not win.isVisible():
if position == "right":
win.move(geo.right() + 8, geo.top() + 40)
- elif position == "below":
+ else:
win.move(geo.left(), geo.bottom() + 8)
win.show(); win.raise_(); win.activateWindow()
- # ── Device/signal events ──────────────────────────────────────────────
+ # ── Profile callbacks ─────────────────────────────────────────────────
+
+ def _profile_new(self):
+ """Reset to a blank slate."""
+ self._ctrl.clear_widgets()
+ # Clear derived channels
+ for dc in self.processor.get_derived():
+ self.processor.remove_derived(dc.channel_id)
+ self._chart.refresh()
+ self._status.setText("New profile — blank slate.")
+
+ def _profile_capture(self, name: str = "Profile") -> Profile:
+ """Serialise current state into a Profile object."""
+ return ProfileManager.capture(
+ registry=self.registry,
+ processor=self.processor,
+ plot_cfg=self._chart._cfg,
+ control_specs=self._ctrl.get_specs(),
+ settings=self._settings,
+ profile_name=name,
+ )
+
+ def _profile_apply(self, profile: Profile):
+ """Restore state from a Profile object."""
+ plot_cfg = ProfileManager.apply(
+ profile=profile,
+ registry=self.registry,
+ processor=self.processor,
+ control_panel=self._ctrl,
+ settings_ref=self._settings,
+ )
+ if plot_cfg:
+ self._chart.apply_layout(plot_cfg)
+ else:
+ self._chart.refresh()
+ # Refresh open windows
+ if self._win_plot:
+ self._win_plot.refresh_channels()
+ self._status.setText(f"Profile loaded: {profile.name}")
+
+ # ── Device / signal events ────────────────────────────────────────────
def _on_device_added(self):
self._chart.refresh()
@@ -245,10 +283,8 @@ class MainWindow(QMainWindow):
self._status.setText(f"Device '{dev_id}' removed.")
def _on_device_reconfigured(self, dev_id: str):
- """Called after Configure dialog closes — rebuild chart curves."""
self._chart.refresh()
- if self._win_plot:
- self._win_plot.refresh_channels()
+ if self._win_plot: self._win_plot.refresh_channels()
self._status.setText(f"Device '{dev_id}' reconfigured.")
def _on_derived_changed(self):
@@ -271,22 +307,20 @@ class MainWindow(QMainWindow):
def _toggle_log(self, c: bool):
if c:
- os.makedirs(self._settings.get("log_dir","logs"), exist_ok=True)
p = self.engine.start_logging(
- os.path.join(self._settings.get("log_dir","logs"), ""))
+ os.path.join(self._settings.get("log_dir", "logs"), ""))
self._log_btn.setText("⏹ LOGGING")
self._status.setText(f"Logging → {p}")
else:
self.engine.stop_logging(); self._log_btn.setText("⬤ LOG")
- # ── Settings ──────────────────────────────────────────────────────────
+ # ── Theme / settings ──────────────────────────────────────────────────
def _apply_theme(self, theme: str):
qss_file = _DARK_QSS if theme == "dark" else _LIGHT_QSS
if os.path.exists(qss_file):
with open(qss_file) as f:
QApplication.instance().setStyleSheet(f.read())
- # Update pyqtgraph plot colours independently of QSS
self._chart.set_theme(theme)
def _on_settings(self, cfg: dict):
@@ -299,9 +333,7 @@ class MainWindow(QMainWindow):
self._time_lbl.setText(f"{h:02d}:{m:02d}:{s:02d}")
def closeEvent(self, event):
- for w in (self._win_devices,self._win_signals,self._win_plot,self._win_settings):
+ for w in (self._win_devices, self._win_signals,
+ self._win_plot, self._win_settings):
if w: w.close()
self.engine.stop(); event.accept()
-
-
-from PyQt6.QtWidgets import QFrame