diff options
Diffstat (limited to 'ui/windows')
| -rw-r--r-- | ui/windows/settings_window.py | 145 |
1 files changed, 138 insertions, 7 deletions
diff --git a/ui/windows/settings_window.py b/ui/windows/settings_window.py index 9d7b7bc..046cfae 100644 --- a/ui/windows/settings_window.py +++ b/ui/windows/settings_window.py @@ -23,9 +23,11 @@ from core.acquisition import AcquisitionEngine class SettingsWindow(QWidget): - theme_changed = pyqtSignal(str) # "dark" | "light" - settings_changed = pyqtSignal(dict) - closed = pyqtSignal() + theme_changed = pyqtSignal(str) # "dark" | "light" + settings_changed = pyqtSignal(dict) + plugin_enable_requested = pyqtSignal(str) # plugin_id + plugin_disable_requested= pyqtSignal(str) # plugin_id + closed = pyqtSignal() # Shared settings dict — written on Apply, read by consumers _defaults = { @@ -42,11 +44,13 @@ class SettingsWindow(QWidget): def __init__(self, registry: DeviceRegistry, engine: AcquisitionEngine, - current: dict = None, parent=None): + current: dict = None, parent=None, *, + plugin_manager=None): super().__init__(parent, Qt.WindowType.Window | Qt.WindowType.Tool) - self.registry = registry - self.engine = engine - self.cfg = dict(self._defaults) + self.registry = registry + self.engine = engine + self._plugin_mgr = plugin_manager + self.cfg = dict(self._defaults) if current: self.cfg.update(current) @@ -72,6 +76,8 @@ class SettingsWindow(QWidget): tabs.addTab(self._general_tab(), " General ") tabs.addTab(self._acquisition_tab(), " Acquisition ") tabs.addTab(self._display_tab(), " Display ") + tabs.addTab(self._controls_tab(), " Controls ") + tabs.addTab(self._plugins_tab(), " Plugins ") # Bottom bar btm = QWidget(); btm.setObjectName("cfgBottomBar") @@ -157,6 +163,131 @@ class SettingsWindow(QWidget): root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0); root.addWidget(scroll) return w + def _controls_tab(self): + """Output channel assignments — which device channel each control widget drives.""" + w = QWidget() + scroll = QScrollArea(); scroll.setWidgetResizable(True) + scroll.setObjectName("deviceScroll") + cont = QWidget(); lay = QVBoxLayout(cont) + lay.setContentsMargins(14,12,14,12); lay.setSpacing(8) + + info = QLabel( + "Configure which physical output channels are driven by each control widget.\n" + "Add output widget mappings below. Changes take effect on next app start." + ) + info.setObjectName("traceSource"); info.setWordWrap(True) + lay.addWidget(info) + + grp = QGroupBox("Output Assignments") + g_lay = QFormLayout(grp); g_lay.setSpacing(6) + + # Collect digital output channels + out_channels = ["— none —"] + for dev in self.registry.all_instances(): + for ch in dev.info.channels: + if ch.channel_id.startswith("do") or "out" in ch.channel_id.lower(): + out_channels.append(f"{dev.info.device_id} / {ch.channel_id} ({ch.name})") + + self._out_combos = {} + for label in ["Pump Power", "Heater", "Motor Enable", "PWM Ch 1"]: + cb = QComboBox(); cb.setObjectName("channelPickerCb") + cb.addItems(out_channels) + g_lay.addRow(f"{label}:", cb) + self._out_combos[label] = cb + + lay.addWidget(grp) + lay.addStretch() + scroll.setWidget(cont) + root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0); root.addWidget(scroll) + return w + + def _plugins_tab(self): + w = QWidget() + scroll = QScrollArea(); scroll.setWidgetResizable(True) + scroll.setObjectName("deviceScroll") + cont = QWidget(); lay = QVBoxLayout(cont) + lay.setContentsMargins(14, 12, 14, 12); lay.setSpacing(10) + + if self._plugin_mgr is None: + lay.addWidget(QLabel("Plugin manager not available.")) + lay.addStretch() + scroll.setWidget(cont) + root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0) + root.addWidget(scroll); return w + + manifests = self._plugin_mgr.get_manifests() + + if not manifests: + info = QLabel( + "No plugins found.\n\n" + "Drop a plugin folder into the plugins/ directory next to main.py.\n" + "Each plugin needs a manifest.json and a plugin.py." + ) + info.setObjectName("traceSource"); info.setWordWrap(True) + lay.addWidget(info) + lay.addStretch() + scroll.setWidget(cont) + root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0) + root.addWidget(scroll); return w + + for manifest in manifests: + lay.addWidget(self._plugin_card(manifest)) + + lay.addStretch() + scroll.setWidget(cont) + root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0) + root.addWidget(scroll); return w + + def _plugin_card(self, manifest): + """One card per discovered plugin.""" + card = QGroupBox() + card.setObjectName("pluginCard") + cl = QVBoxLayout(card); cl.setContentsMargins(10, 8, 10, 8); cl.setSpacing(4) + + # Header row: name + version + enable toggle + hdr = QHBoxLayout() + name_lbl = QLabel(f"<b>{manifest.name}</b> <small>v{manifest.version}</small>") + name_lbl.setObjectName("traceLabel") + hdr.addWidget(name_lbl, 1) + + enabled = self._plugin_mgr.is_enabled(manifest.plugin_id) + toggle = QPushButton("Disable" if enabled else "Enable") + toggle.setObjectName("configButton") + toggle.setFixedWidth(72) + toggle.clicked.connect( + lambda _, pid=manifest.plugin_id, btn=toggle: self._toggle_plugin(pid, btn) + ) + hdr.addWidget(toggle) + cl.addLayout(hdr) + + # Description / author + if manifest.description: + desc = QLabel(manifest.description) + desc.setObjectName("traceSource"); desc.setWordWrap(True) + cl.addWidget(desc) + + if manifest.author: + author = QLabel(f"Author: {manifest.author}") + author.setObjectName("traceSource") + cl.addWidget(author) + + # Plugin-specific settings widget (only when loaded) + plugin = self._plugin_mgr.get_plugin(manifest.plugin_id) + if plugin: + sw = plugin.get_settings_widget() + if sw is not None: + cl.addWidget(sw) + + return card + + def _toggle_plugin(self, plugin_id: str, btn: QPushButton): + if self._plugin_mgr.is_enabled(plugin_id): + self.plugin_disable_requested.emit(plugin_id) + btn.setText("Enable") + else: + self.plugin_enable_requested.emit(plugin_id) + btn.setText("Disable") + # ── Actions ─────────────────────────────────────────────────────────── def _browse_log(self): |
