summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/add_device_dialog.py10
-rw-r--r--ui/config_dialog.py9
-rw-r--r--ui/control_editor.py2
-rw-r--r--ui/main_window.py80
-rw-r--r--ui/plot_builder.py8
-rw-r--r--ui/plot_config.py4
-rw-r--r--ui/windows/channels_window.py20
-rw-r--r--ui/windows/debug_window.py65
-rw-r--r--ui/windows/devices_window.py27
-rw-r--r--ui/windows/plot_window.py16
-rw-r--r--ui/windows/settings_window.py23
11 files changed, 232 insertions, 32 deletions
diff --git a/ui/add_device_dialog.py b/ui/add_device_dialog.py
index 2956a96..2bf0233 100644
--- a/ui/add_device_dialog.py
+++ b/ui/add_device_dialog.py
@@ -17,6 +17,7 @@ from devices.device_registry import DeviceRegistry
from devices.arduino_device import ArduinoDevice
from devices.nidaqmx_device import NidaqmxDevice
from devices.serial_device import SerialDevice, _FORMAT_LABELS
+from core.app_settings import is_developer_mode
# ── Background scan threads ───────────────────────────────────────────────────
@@ -276,6 +277,7 @@ class AddDeviceDialog(QDialog):
conn_form.addRow(self._ni_lbl, self._ni_edit)
self._sim_chk = QCheckBox("Simulation mode")
+ self._sim_chk.setVisible(is_developer_mode()) # dev-mode-only escape hatch
conn_form.addRow(self._sim_chk)
root.addLayout(conn_form)
@@ -299,6 +301,10 @@ class AddDeviceDialog(QDialog):
self._id_edit.setPlaceholderText("Leave blank for auto")
cfg_form.addRow("Device ID:", self._id_edit)
+ self._name_edit = QLineEdit()
+ self._name_edit.setPlaceholderText("Leave blank to use the default type name")
+ cfg_form.addRow("Display Name:", self._name_edit)
+
self._fmt_cb = QComboBox()
self._fmt_cb.addItems(list(_FORMAT_LABELS.keys()))
self._fmt_lbl = QLabel("Protocol / Format:")
@@ -449,6 +455,10 @@ class AddDeviceDialog(QDialog):
panel.set_simulate(sim)
dev = panel.build_device(dev_id)
+ display_name = self._name_edit.text().strip()
+ if display_name:
+ dev.info.name = display_name
+
self.created_device = dev
self.accept()
except Exception as e:
diff --git a/ui/config_dialog.py b/ui/config_dialog.py
index d9bc9df..5a95821 100644
--- a/ui/config_dialog.py
+++ b/ui/config_dialog.py
@@ -59,7 +59,14 @@ class DeviceConfigDialog(QDialog):
e = QLineEdit(str(v)); e.setReadOnly(True); return e
form.addRow("Device ID:", _ro(info.device_id))
- form.addRow("Name:", _ro(info.name))
+
+ def _on_name_edited():
+ info.name = name_edit.text().strip() or info.name
+ self.setWindowTitle(f"Configure — {info.name} [{info.device_id}]")
+
+ name_edit = QLineEdit(info.name)
+ name_edit.editingFinished.connect(_on_name_edited)
+ form.addRow("Name:", name_edit)
form.addRow("Type:", _ro(info.device_type))
form.addRow("Description:", _ro(info.description))
form.addRow("Manufacturer:", _ro(info.manufacturer))
diff --git a/ui/control_editor.py b/ui/control_editor.py
index 48de21d..65aa96e 100644
--- a/ui/control_editor.py
+++ b/ui/control_editor.py
@@ -362,7 +362,7 @@ class ControlEditorDialog(QDialog):
return
# Add actual channels
for ch in dev.info.channels:
- self._ch_cb.addItem(f"{ch.channel_id} ({ch.name})",
+ self._ch_cb.addItem(f"{ch.name} ({ch.channel_id})",
userData=ch.channel_id)
# For Arduino backends also suggest digital pins for output
if hasattr(dev, "backend") and dev.backend == "arduino":
diff --git a/ui/main_window.py b/ui/main_window.py
index 268a9d3..90294ac 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -45,7 +45,7 @@ from ui.windows.settings_window import SettingsWindow
from plugins.plugin_manager import PluginManager
from plugins.base_plugin import PluginContext
-from core.app_settings import load_settings, save_settings
+from core.app_settings import load_settings, save_settings, set_developer_mode
_DARK_QSS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "style_dark.qss")
@@ -68,6 +68,7 @@ class MainWindow(QMainWindow):
self._win_channels = None
self._win_plot = None
self._win_settings = None
+ self._win_debug = None
_plugins_dir = os.path.join(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))), "plugins")
@@ -171,6 +172,12 @@ class MainWindow(QMainWindow):
set_btn.clicked.connect(lambda c: self._toggle_win("settings", c, set_btn))
tb.addWidget(set_btn); self._btn_settings = set_btn
+ debug_btn = QPushButton("🐞 Debug")
+ debug_btn.setObjectName("toolbarSectionBtn"); debug_btn.setCheckable(True)
+ debug_btn.clicked.connect(lambda c: self._toggle_win("debug", c, debug_btn))
+ tb.addWidget(debug_btn); self._btn_debug = debug_btn
+ debug_btn.setVisible(False) # shown/hidden per developer-mode setting
+
# ── Central ───────────────────────────────────────────────────────
central = QWidget(); self.setCentralWidget(central)
@@ -299,11 +306,60 @@ class MainWindow(QMainWindow):
def plugin_enable(self, plugin_id: str):
"""Called by SettingsWindow when user enables a plugin."""
+ missing = self._plugin_mgr.get_missing_dependencies(plugin_id)
+ if missing:
+ from PyQt6.QtWidgets import QMessageBox
+ reply = QMessageBox.question(
+ self, "Missing Plugin Dependencies",
+ f"This plugin needs packages that aren't installed:\n\n"
+ f" {', '.join(missing)}\n\n"
+ f"Install them now with pip?",
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
+ QMessageBox.StandardButton.No,
+ )
+ if reply == QMessageBox.StandardButton.Yes:
+ if self._pip_install(missing):
+ self._enable_plugin_now(plugin_id)
+ else:
+ self._enable_plugin_now(plugin_id)
+ if self._win_settings:
+ self._win_settings.sync_plugin_button(plugin_id)
+
+ def _enable_plugin_now(self, plugin_id: str):
ctx = self._make_plugin_context()
plugin = self._plugin_mgr.enable(plugin_id, ctx)
if plugin:
self._install_plugin(plugin)
+ def _pip_install(self, requirements: list) -> bool:
+ """Blocking `pip install` of the given requirement strings.
+ Returns True on success; shows a result dialog either way."""
+ import subprocess
+ import sys
+ from PyQt6.QtWidgets import QMessageBox
+
+ QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
+ try:
+ result = subprocess.run(
+ [sys.executable, "-m", "pip", "install", *requirements],
+ capture_output=True, text=True,
+ )
+ finally:
+ QApplication.restoreOverrideCursor()
+
+ if result.returncode == 0:
+ QMessageBox.information(
+ self, "Install Complete",
+ f"Installed: {', '.join(requirements)}"
+ )
+ return True
+ QMessageBox.critical(
+ self, "Install Failed",
+ f"pip install failed for: {', '.join(requirements)}\n\n"
+ f"{result.stderr.strip()[-1500:]}"
+ )
+ return False
+
def plugin_disable(self, plugin_id: str):
"""Called by SettingsWindow when user disables a plugin."""
self._uninstall_plugin(plugin_id)
@@ -317,12 +373,14 @@ class MainWindow(QMainWindow):
"channels": self._open_channels,
"plot": self._open_plot,
"settings": self._open_settings,
+ "debug": self._open_debug,
}
wins = {
"devices": "_win_devices",
"channels": "_win_channels",
"plot": "_win_plot",
"settings": "_win_settings",
+ "debug": "_win_debug",
}
if checked:
creators[name]()
@@ -373,6 +431,22 @@ class MainWindow(QMainWindow):
self._win_settings.closed.connect(lambda: self._btn_settings.setChecked(False))
self._show_win(self._win_settings, "right")
+ def _open_debug(self):
+ from ui.windows.debug_window import DebugWindow
+ if self._win_debug is None:
+ self._win_debug = DebugWindow(self)
+ self._win_debug.closed.connect(lambda: self._btn_debug.setChecked(False))
+ self._show_win(self._win_debug, "right")
+
+ def _update_debug_btn_visibility(self):
+ from core.app_settings import is_developer_mode
+ on = is_developer_mode()
+ self._btn_debug.setVisible(on)
+ if not on:
+ self._btn_debug.setChecked(False)
+ if self._win_debug:
+ self._win_debug.hide()
+
def _show_win(self, win: QWidget, position: str = "right"):
if not win.isVisible():
screen = QApplication.screenAt(self.geometry().center())
@@ -570,10 +644,14 @@ class MainWindow(QMainWindow):
def _apply_settings_on_startup(self):
self._apply_theme(self._settings.get("theme", "dark"))
self.engine._interval = self._settings.get("poll_ms", 100) / 1000.0
+ set_developer_mode(self._settings.get("developer_mode", True))
+ self._update_debug_btn_visibility()
def _on_settings(self, cfg: dict):
self._settings.update(cfg)
self.engine._interval = cfg.get("poll_ms", 100) / 1000.0
+ set_developer_mode(self._settings.get("developer_mode", True))
+ self._update_debug_btn_visibility()
save_settings(self._settings)
def _tick(self):
diff --git a/ui/plot_builder.py b/ui/plot_builder.py
index ad6d231..7445bda 100644
--- a/ui/plot_builder.py
+++ b/ui/plot_builder.py
@@ -298,10 +298,14 @@ class PlotBlock(QFrame):
cb.setPlaceholderText("Select channel…")
for dev in self.registry.all_instances():
for ch in dev.info.channels:
- cb.addItem(f"{dev.info.device_id} / {ch.channel_id} ({ch.name})",
+ if not ch.enabled:
+ continue
+ cb.addItem(f"{ch.name} ({dev.info.device_id}/{ch.channel_id})",
userData=(dev.info.device_id, ch.channel_id, ch.name, ch.color))
for dc in self.processor.get_derived():
- cb.addItem(f"[derived] {dc.channel_id} ({dc.name})",
+ if not dc.enabled:
+ continue
+ cb.addItem(f"{dc.name} ([derived]/{dc.channel_id})",
userData=("derived", dc.channel_id, dc.name, dc.color))
return cb
diff --git a/ui/plot_config.py b/ui/plot_config.py
index 058915f..c6ecaa4 100644
--- a/ui/plot_config.py
+++ b/ui/plot_config.py
@@ -333,7 +333,9 @@ class PlotBlock(QFrame):
cb.setPlaceholderText("Select channel…")
for dev in self.registry.all_instances():
for ch in dev.info.channels:
- label = f"{dev.info.device_id} / {ch.channel_id} ({ch.name})"
+ if not ch.enabled:
+ continue
+ label = f"{ch.name} ({dev.info.device_id}/{ch.channel_id})"
cb.addItem(label, userData=(dev.info.device_id, ch.channel_id,
ch.name, ch.color))
return cb
diff --git a/ui/windows/channels_window.py b/ui/windows/channels_window.py
index 381800a..38d477b 100644
--- a/ui/windows/channels_window.py
+++ b/ui/windows/channels_window.py
@@ -74,7 +74,7 @@ def _channel_combo(registry: DeviceRegistry,
for ch in dev.info.channels:
if not ch.enabled:
continue
- label = f"{dev.info.device_id} / {ch.channel_id} ({ch.name})"
+ label = f"{ch.name} ({dev.info.device_id}/{ch.channel_id})"
if show_unit and ch.unit:
label += f" [{ch.unit}]"
cb.addItem(label, userData=(dev.info.device_id, ch.channel_id))
@@ -115,7 +115,7 @@ class ChannelPickerDialog(QDialog):
if (dev.info.device_id, ch.channel_id) in already_shown:
continue
any_available = True
- label = f"{dev.info.device_id} / {ch.channel_id} ({ch.name})"
+ label = f"{ch.name} ({dev.info.device_id}/{ch.channel_id})"
if ch.unit:
label += f" [{ch.unit}]"
chk = QCheckBox(label)
@@ -236,7 +236,7 @@ class ChannelPipelineBlock(QFrame):
# Header
hdr = QWidget(); hdr.setObjectName("plotBlockHeader"); hdr.setFixedHeight(32)
hl = QHBoxLayout(hdr); hl.setContentsMargins(8, 0, 6, 0)
- title = f"{self.dev_id} / {self.ch_id} ({ch_name})"
+ title = f"{ch_name} ({self.dev_id}/{self.ch_id})"
if unit:
title += f" [{unit}]"
self._title_lbl = QLabel(title); self._title_lbl.setObjectName("traceSource")
@@ -307,7 +307,7 @@ class ChannelPipelineBlock(QFrame):
self._body.setVisible(False)
def _refresh_title(self):
- title = f"{self.dev_id} / {self.ch_id} ({self._ch_name})"
+ title = f"{self._ch_name} ({self.dev_id}/{self.ch_id})"
if self._unit:
title += f" [{self._unit}]"
self._title_lbl.setText(title)
@@ -444,7 +444,12 @@ class PipelineTab(QWidget):
virt_bar = QWidget(); virt_bar.setObjectName("cfgGlobalBar")
vb_lay = QHBoxLayout(virt_bar); vb_lay.setContentsMargins(10, 7, 10, 7); vb_lay.setSpacing(6)
vb_lbl = QLabel("Channels"); vb_lbl.setObjectName("devWindowTitle")
- vb_lay.addWidget(vb_lbl, 1)
+ vb_lay.addWidget(vb_lbl)
+ self._src_cb = _channel_combo(self.registry, self.processor, include_derived=True)
+ self._src_cb.setObjectName("channelPickerCb")
+ self._src_cb.insertItem(0, "Source: none (empty channel)", userData=None)
+ self._src_cb.setCurrentIndex(0)
+ vb_lay.addWidget(self._src_cb, 1)
add_virt = QPushButton("+ Add Channel"); add_virt.setObjectName("addTraceBtn")
add_virt.clicked.connect(self._add_virtual)
vb_lay.addWidget(add_virt)
@@ -579,6 +584,11 @@ class PipelineTab(QWidget):
kind="expression", color=color)
blk = self._make_derived_block(dc)
self._virt_inner.insertWidget(self._virt_inner.count() - 1, blk)
+ # Pre-seed the source picked in the bar above, if any — otherwise the
+ # channel is created empty and sources can be added manually.
+ src = self._src_cb.currentData()
+ if src:
+ blk._add_src(src)
def _make_derived_block(self, dc: DerivedChannel) -> DerivedBlock:
blk = DerivedBlock(dc, self.registry, self.processor)
diff --git a/ui/windows/debug_window.py b/ui/windows/debug_window.py
new file mode 100644
index 0000000..0e891ed
--- /dev/null
+++ b/ui/windows/debug_window.py
@@ -0,0 +1,65 @@
+"""
+ui/windows/debug_window.py
+
+DEBUG window — developer-mode only.
+
+Minimal first pass: a live console showing everything the app has printed
+via core.debug_log (stdout/stderr tee), so debugging doesn't require a
+terminal. Not wired to any other diagnostics yet — extend as needed.
+"""
+
+from PyQt6.QtWidgets import (
+ QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
+ QTextEdit, QFrame,
+)
+from PyQt6.QtCore import Qt, pyqtSignal
+from PyQt6.QtGui import QFont, QCloseEvent
+
+from core.debug_log import get_broadcaster, get_history
+
+
+class DebugWindow(QWidget):
+ closed = pyqtSignal()
+
+ def __init__(self, parent=None):
+ super().__init__(parent, Qt.WindowType.Window | Qt.WindowType.Tool)
+ self.setWindowTitle("Debug")
+ self.setMinimumSize(560, 420)
+ self.resize(700, 500)
+ self._build()
+
+ broadcaster = get_broadcaster()
+ if broadcaster is not None:
+ broadcaster.line_written.connect(self._append)
+
+ def _build(self):
+ root = QVBoxLayout(self); root.setContentsMargins(0, 0, 0, 0); root.setSpacing(0)
+
+ hdr = QWidget(); hdr.setObjectName("devWindowTitleBar"); hdr.setFixedHeight(44)
+ hl = QHBoxLayout(hdr); hl.setContentsMargins(14, 0, 14, 0)
+ title = QLabel("DEBUG"); title.setObjectName("devWindowTitle")
+ hl.addWidget(title, 1)
+ clear_btn = QPushButton("Clear"); clear_btn.setObjectName("configButton")
+ clear_btn.clicked.connect(lambda: self._console.clear())
+ hl.addWidget(clear_btn)
+ root.addWidget(hdr)
+
+ div = QFrame(); div.setFrameShape(QFrame.Shape.HLine)
+ div.setObjectName("devWindowDivider"); root.addWidget(div)
+
+ self._console = QTextEdit(); self._console.setObjectName("codeEditor")
+ self._console.setReadOnly(True)
+ mono = QFont("IBM Plex Mono, Consolas, Monospace")
+ mono.setStyleHint(QFont.StyleHint.Monospace)
+ self._console.setFont(mono)
+ self._console.setPlainText(get_history())
+ self._console.verticalScrollBar().setValue(self._console.verticalScrollBar().maximum())
+ root.addWidget(self._console, 1)
+
+ def _append(self, text: str):
+ self._console.insertPlainText(text)
+ sb = self._console.verticalScrollBar()
+ sb.setValue(sb.maximum())
+
+ def closeEvent(self, e: QCloseEvent):
+ self.closed.emit(); e.accept()
diff --git a/ui/windows/devices_window.py b/ui/windows/devices_window.py
index ac9d13c..eec65c8 100644
--- a/ui/windows/devices_window.py
+++ b/ui/windows/devices_window.py
@@ -124,12 +124,11 @@ class ChannelsTab(QWidget):
# Col indices
_C_DEVICE = 0
- _C_CH_ID = 1
- _C_ENABLED = 2
- _C_NAME = 3
- _C_UNIT = 4
- _C_MIN = 5
- _C_MAX = 6
+ _C_ENABLED = 1
+ _C_NAME = 2
+ _C_UNIT = 3
+ _C_MIN = 4
+ _C_MAX = 5
def __init__(self, registry: DeviceRegistry):
super().__init__()
@@ -142,14 +141,13 @@ class ChannelsTab(QWidget):
self._table = QTableWidget()
self._table.setObjectName("channelTable")
- self._table.setColumnCount(7)
+ self._table.setColumnCount(6)
self._table.setHorizontalHeaderLabels(
- ["Device", "Signal ID", "On", "Name", "Unit", "Min", "Max"]
+ ["Device", "On", "Name", "Unit", "Min", "Max"]
)
hdr = self._table.horizontalHeader()
hdr.setSectionResizeMode(self._C_NAME, QHeaderView.ResizeMode.Stretch)
hdr.setSectionResizeMode(self._C_DEVICE, QHeaderView.ResizeMode.ResizeToContents)
- hdr.setSectionResizeMode(self._C_CH_ID, QHeaderView.ResizeMode.ResizeToContents)
hdr.setSectionResizeMode(self._C_ENABLED, QHeaderView.ResizeMode.ResizeToContents)
self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self._table.setAlternatingRowColors(True)
@@ -165,16 +163,13 @@ class ChannelsTab(QWidget):
for ch in dev.info.channels:
self._table.insertRow(row)
- dev_item = QTableWidgetItem(f"{dev.info.icon} {dev.info.device_id}")
+ # Device + signal ID folded into one non-editable column —
+ # the separate "Signal ID" column was removed as redundant.
+ dev_item = QTableWidgetItem(f"{dev.info.icon} {dev.info.device_id} / {ch.channel_id}")
dev_item.setFlags(dev_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
dev_item.setForeground(QColor("#64748b"))
self._table.setItem(row, self._C_DEVICE, dev_item)
- ch_item = QTableWidgetItem(ch.channel_id)
- ch_item.setFlags(ch_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- ch_item.setForeground(QColor(ch.color))
- self._table.setItem(row, self._C_CH_ID, ch_item)
-
# Enabled checkbox — centred in cell
chk_container = QWidget()
chk_lay = QHBoxLayout(chk_container)
@@ -349,7 +344,7 @@ class DevicesWindow(QWidget):
dev = self.registry.get_instance(device_id)
if dev:
DeviceConfigDialog(dev, self).exec()
- self._ch_tab.refresh()
+ self.refresh() # rebuilds device rows (picks up a renamed display name) + Signals tab
self.device_reconfigured.emit(device_id)
def _on_remove(self, device_id: str):
diff --git a/ui/windows/plot_window.py b/ui/windows/plot_window.py
index 9edf046..f311e9e 100644
--- a/ui/windows/plot_window.py
+++ b/ui/windows/plot_window.py
@@ -355,10 +355,14 @@ class PaneBlock(QFrame):
self._x_cb.addItem("⏱ Time (elapsed s)", userData="time")
for dev in self.registry.all_instances():
for ch in dev.info.channels:
- self._x_cb.addItem(f"{dev.info.device_id}/{ch.channel_id} ({ch.name})",
+ if not ch.enabled:
+ continue
+ self._x_cb.addItem(f"{ch.name} ({dev.info.device_id}/{ch.channel_id})",
userData=f"{dev.info.device_id}/{ch.channel_id}")
for dc in self.processor.get_derived():
- self._x_cb.addItem(f"[virtual] {dc.channel_id}",
+ if not dc.enabled:
+ continue
+ self._x_cb.addItem(f"{dc.name} ([virtual]/{dc.channel_id})",
userData=f"derived/{dc.channel_id}")
for i in range(self._x_cb.count()):
if self._x_cb.itemData(i) == self.spec.x_source:
@@ -417,10 +421,14 @@ class PaneBlock(QFrame):
cb = QComboBox(); cb.setObjectName("channelPickerCb")
for dev in self.registry.all_instances():
for ch in dev.info.channels:
- cb.addItem(f"{dev.info.device_id} / {ch.channel_id} ({ch.name})",
+ if not ch.enabled:
+ continue
+ cb.addItem(f"{ch.name} ({dev.info.device_id}/{ch.channel_id})",
userData=(dev.info.device_id, ch.channel_id, ch.name, ch.color))
for dc in self.processor.get_derived():
- cb.addItem(f"[virtual] {dc.channel_id} ({dc.name})",
+ if not dc.enabled:
+ continue
+ cb.addItem(f"{dc.name} ([virtual]/{dc.channel_id})",
userData=("derived", dc.channel_id, dc.name, dc.color))
return cb
diff --git a/ui/windows/settings_window.py b/ui/windows/settings_window.py
index 1630efa..290d9e0 100644
--- a/ui/windows/settings_window.py
+++ b/ui/windows/settings_window.py
@@ -40,6 +40,7 @@ class SettingsWindow(QWidget):
"show_grid": True,
"font_size": 12,
"antialias": True,
+ "developer_mode": True,
}
def __init__(self, registry: DeviceRegistry,
@@ -50,6 +51,7 @@ class SettingsWindow(QWidget):
self.registry = registry
self.engine = engine
self._plugin_mgr = plugin_manager
+ self._plugin_buttons: dict = {} # plugin_id -> QPushButton
self.cfg = dict(self._defaults)
if current:
self.cfg.update(current)
@@ -108,6 +110,14 @@ class SettingsWindow(QWidget):
self._aa_chk = QCheckBox(); self._aa_chk.setChecked(self.cfg["antialias"])
lay.addRow("Anti-alias plots:", self._aa_chk)
+ self._dev_chk = QCheckBox()
+ self._dev_chk.setChecked(self.cfg["developer_mode"])
+ self._dev_chk.setToolTip(
+ "Controls whether the Debug window and each device's Simulation\n"
+ "Mode option are available. Off = real-hardware-only, no debug tools."
+ )
+ lay.addRow("Developer mode:", self._dev_chk)
+
rst = QPushButton("Reset to Defaults"); rst.setObjectName("configButton")
rst.clicked.connect(self._reset_to_defaults)
lay.addRow("", rst)
@@ -120,6 +130,7 @@ class SettingsWindow(QWidget):
self._theme_cb.setCurrentText(self.cfg["theme"].title())
self._font_sp.setValue(self.cfg["font_size"])
self._aa_chk.setChecked(self.cfg["antialias"])
+ self._dev_chk.setChecked(self.cfg["developer_mode"])
self._poll_sp.setValue(self.cfg["poll_ms"])
self._buf_sp.setValue(self.cfg["buffer_size"])
self._log_edit.setText(self.cfg["log_dir"])
@@ -233,6 +244,7 @@ class SettingsWindow(QWidget):
toggle.clicked.connect(
lambda _, pid=manifest.plugin_id, btn=toggle: self._toggle_plugin(pid, btn)
)
+ self._plugin_buttons[manifest.plugin_id] = toggle
hdr.addWidget(toggle)
cl.addLayout(hdr)
@@ -261,8 +273,16 @@ class SettingsWindow(QWidget):
self.plugin_disable_requested.emit(plugin_id)
btn.setText("Enable")
else:
+ # Don't flip to "Disable" yet — enabling can fail (missing
+ # dependencies, bad plugin code). main_window confirms the
+ # real outcome via sync_plugin_button() once enable() returns.
self.plugin_enable_requested.emit(plugin_id)
- btn.setText("Disable")
+
+ def sync_plugin_button(self, plugin_id: str):
+ """Refresh one plugin's toggle button to match its actual enabled state."""
+ btn = self._plugin_buttons.get(plugin_id)
+ if btn is not None and self._plugin_mgr is not None:
+ btn.setText("Disable" if self._plugin_mgr.is_enabled(plugin_id) else "Enable")
# ── Actions ───────────────────────────────────────────────────────────
@@ -288,6 +308,7 @@ class SettingsWindow(QWidget):
"theme": theme,
"font_size": self._font_sp.value(),
"antialias": self._aa_chk.isChecked(),
+ "developer_mode": self._dev_chk.isChecked(),
"poll_ms": self._poll_sp.value(),
"buffer_size": self._buf_sp.value(),
"log_dir": self._log_edit.text(),