diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-04-21 13:43:52 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-04-21 13:57:46 -0600 |
| commit | 2a634dca0c7962b90004f75c4cdac6225201bb5e (patch) | |
| tree | 3e1f0cd4ff165e77d17ebec5278c9bed885bb1da /ui/main_window.py | |
| parent | b1a61fd29e2282110bc4f4bc4616c55ed9d88dbb (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.py | 136 |
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 |
