""" ui/windows/settings_window.py SETTINGS window — toolbar section 5. Tabs: General — theme (dark/light/system), font sizes, polling rate Acquisition — sample rate, buffer size, CSV log directory Display — default time window, legend, grid defaults """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTabWidget, QFrame, QFormLayout, QComboBox, QSpinBox, QDoubleSpinBox, QCheckBox, QLineEdit, QFileDialog, QScrollArea, ) from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QCloseEvent, QFont from devices.device_registry import DeviceRegistry from core.acquisition import AcquisitionEngine class SettingsWindow(QWidget): theme_changed = pyqtSignal(str) # "dark" | "light" settings_changed = pyqtSignal(dict) closed = pyqtSignal() # Shared settings dict — written on Apply, read by consumers _defaults = { "theme": "dark", "poll_ms": 100, "buffer_size": 20000, "log_dir": "logs", "time_window_s": 30.0, "show_legend": True, "show_grid": True, "font_size": 12, "antialias": True, } def __init__(self, registry: DeviceRegistry, engine: AcquisitionEngine, current: dict = None, parent=None): super().__init__(parent, Qt.WindowType.Window | Qt.WindowType.Tool) self.registry = registry self.engine = engine self.cfg = dict(self._defaults) if current: self.cfg.update(current) self.setWindowTitle("Settings") self.setMinimumSize(520, 440) self.resize(560, 520) self._build() def _build(self): root = QVBoxLayout(self); root.setContentsMargins(0,0,0,0); root.setSpacing(0) # Header hdr = QWidget(); hdr.setObjectName("devWindowTitleBar"); hdr.setFixedHeight(44) hl = QHBoxLayout(hdr); hl.setContentsMargins(14,0,14,0) hl.addWidget(QLabel("SETTINGS").also(lambda w: w.setObjectName("devWindowTitle"))) root.addWidget(hdr) div = QFrame(); div.setFrameShape(QFrame.Shape.HLine) div.setObjectName("devWindowDivider"); root.addWidget(div) tabs = QTabWidget(); tabs.setObjectName("signalBuilderTabs") root.addWidget(tabs, 1) tabs.addTab(self._general_tab(), " General ") tabs.addTab(self._acquisition_tab(), " Acquisition ") tabs.addTab(self._display_tab(), " Display ") # Bottom bar btm = QWidget(); btm.setObjectName("cfgBottomBar") bl = QHBoxLayout(btm); bl.setContentsMargins(12,8,12,8) bl.addStretch() ap = QPushButton("✓ Apply & Close"); ap.setObjectName("applyButton") ap.clicked.connect(self._apply); bl.addWidget(ap) root.addWidget(btm) # ── Tabs ────────────────────────────────────────────────────────────── def _general_tab(self): w = QWidget() scroll = QScrollArea(); scroll.setWidgetResizable(True) scroll.setObjectName("deviceScroll") cont = QWidget(); lay = QFormLayout(cont) lay.setContentsMargins(16,14,16,14); lay.setSpacing(10) self._theme_cb = QComboBox(); self._theme_cb.setObjectName("channelPickerCb") self._theme_cb.addItems(["Dark", "Light"]) self._theme_cb.setCurrentText(self.cfg["theme"].title()) lay.addRow("Theme:", self._theme_cb) self._font_sp = QSpinBox(); self._font_sp.setRange(8,18) self._font_sp.setValue(self.cfg["font_size"]); self._font_sp.setSuffix(" pt") self._font_sp.setObjectName("traceWidthSpin") lay.addRow("Base font size:", self._font_sp) self._aa_chk = QCheckBox(); self._aa_chk.setChecked(self.cfg["antialias"]) lay.addRow("Anti-alias plots:", self._aa_chk) scroll.setWidget(cont) root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0); root.addWidget(scroll) return w def _acquisition_tab(self): w = QWidget() scroll = QScrollArea(); scroll.setWidgetResizable(True) scroll.setObjectName("deviceScroll") cont = QWidget(); lay = QFormLayout(cont) lay.setContentsMargins(16,14,16,14); lay.setSpacing(10) self._poll_sp = QSpinBox(); self._poll_sp.setRange(10,5000) self._poll_sp.setValue(self.cfg["poll_ms"]); self._poll_sp.setSuffix(" ms") self._poll_sp.setObjectName("traceWidthSpin") lay.addRow("Poll interval:", self._poll_sp) self._buf_sp = QSpinBox(); self._buf_sp.setRange(1000,500000) self._buf_sp.setValue(self.cfg["buffer_size"]) self._buf_sp.setObjectName("traceWidthSpin") lay.addRow("Buffer size (samples):", self._buf_sp) log_row = QHBoxLayout() self._log_edit = QLineEdit(self.cfg["log_dir"]) self._log_edit.setObjectName("traceLabel"); log_row.addWidget(self._log_edit,1) br = QPushButton("Browse"); br.setObjectName("configButton") br.clicked.connect(self._browse_log); log_row.addWidget(br) lay.addRow("Log directory:", log_row) scroll.setWidget(cont) root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0); root.addWidget(scroll) return w def _display_tab(self): w = QWidget() scroll = QScrollArea(); scroll.setWidgetResizable(True) scroll.setObjectName("deviceScroll") cont = QWidget(); lay = QFormLayout(cont) lay.setContentsMargins(16,14,16,14); lay.setSpacing(10) self._tw_sp = QDoubleSpinBox(); self._tw_sp.setRange(1,3600) self._tw_sp.setValue(self.cfg["time_window_s"]); self._tw_sp.setSuffix(" s") self._tw_sp.setObjectName("cfgGlobalSpin") lay.addRow("Default time window:", self._tw_sp) self._legend_chk = QCheckBox(); self._legend_chk.setChecked(self.cfg["show_legend"]) lay.addRow("Show legend by default:", self._legend_chk) self._grid_chk = QCheckBox(); self._grid_chk.setChecked(self.cfg["show_grid"]) lay.addRow("Show grid by default:", self._grid_chk) scroll.setWidget(cont) root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0); root.addWidget(scroll) return w # ── Actions ─────────────────────────────────────────────────────────── def _browse_log(self): path = QFileDialog.getExistingDirectory(self, "Select Log Directory") if path: self._log_edit.setText(path) def _apply(self): theme = self._theme_cb.currentText().lower() self.cfg.update({ "theme": theme, "font_size": self._font_sp.value(), "antialias": self._aa_chk.isChecked(), "poll_ms": self._poll_sp.value(), "buffer_size": self._buf_sp.value(), "log_dir": self._log_edit.text(), "time_window_s": self._tw_sp.value(), "show_legend": self._legend_chk.isChecked(), "show_grid": self._grid_chk.isChecked(), }) self.theme_changed.emit(theme) self.settings_changed.emit(dict(self.cfg)) self.hide() def closeEvent(self, e: QCloseEvent): self.closed.emit(); e.accept() # monkey-patch QLabel.also from PyQt6.QtWidgets import QLabel as _QL def _also3(self, fn): fn(self); return self if not hasattr(_QL, "also"): _QL.also = _also3