summaryrefslogtreecommitdiff
path: root/ui/windows
diff options
context:
space:
mode:
Diffstat (limited to 'ui/windows')
-rw-r--r--ui/windows/settings_window.py145
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):