From 9360dea6e6327004b905eb6d7c782cc7c0d3ba13 Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Wed, 29 Jul 2026 12:59:10 -0600 Subject: Hide non-writable channels from Controls picker, gate writes on enabled+writable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ChannelConfig.writable so the Controls editor can only bind to channels with a real write mapping (VS/MA/ME/MD/ST for CML, do*/DO for digital I/O, write_cmd-configured SCPI channels, holding-register Modbus channels) instead of read-only telemetry — this is exactly the class of mistake hit earlier (binding a motor-speed control to M1_TV instead of M1_VS). Also carries forward the enabled-channel gating for controls (hide disabled channels from the picker, block writes to them at _write() time) done alongside this investigation. Co-Authored-By: Claude Sonnet 5 --- devices/digital_io.py | 1 + 1 file changed, 1 insertion(+) (limited to 'devices/digital_io.py') diff --git a/devices/digital_io.py b/devices/digital_io.py index 53395dc..111224d 100644 --- a/devices/digital_io.py +++ b/devices/digital_io.py @@ -60,6 +60,7 @@ class DigitalIODevice(BaseDevice): channel_id=f"do{i}", name=f"DO {i}", unit="", min_value=0.0, max_value=1.0, color=_OUT_COLORS[i % len(_OUT_COLORS)], + writable=True, )) info = DeviceInfo( -- cgit v1.2.3 From d9ca58e48cf92438087ac24ee261c2c4911316b1 Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Wed, 29 Jul 2026 13:03:24 -0600 Subject: Add editable device display name, persist it across profile save/load DeviceInfo.name existed but was hardcoded per device type and never operator-editable. Adds a Display Name field to Add Device and makes the Info-tab Name field in the device config dialog editable, and threads the name through get_save_config()/ProfileManager.apply() so a custom name survives a .labdaq save/reload instead of reverting to the type default. Name is applied post-construction rather than as a constructor kwarg, since no device factory declares a "name" param. Co-Authored-By: Claude Sonnet 5 --- core/profile.py | 9 ++++++++- devices/analog_input.py | 1 + devices/arduino_device.py | 1 + devices/digital_io.py | 1 + devices/nidaqmx_device.py | 1 + devices/serial_device.py | 1 + plugins/motion_capture/device.py | 1 + ui/add_device_dialog.py | 8 ++++++++ ui/config_dialog.py | 9 ++++++++- ui/windows/devices_window.py | 2 +- 10 files changed, 31 insertions(+), 3 deletions(-) (limited to 'devices/digital_io.py') diff --git a/core/profile.py b/core/profile.py index d3c7e3a..7ede859 100644 --- a/core/profile.py +++ b/core/profile.py @@ -302,8 +302,15 @@ class ProfileManager: print(f"[Profile] Unknown device type: {dev_type}") continue try: - kwargs = {k: v for k, v in dev_cfg.items() if k != "device_type"} + # "name" is a display label, not a constructor arg — every device + # factory builds its own default name internally, so apply it + # after construction instead of passing it through. + custom_name = dev_cfg.get("name") + kwargs = {k: v for k, v in dev_cfg.items() + if k not in ("device_type", "name")} dev = factory(**kwargs) + if custom_name: + dev.info.name = custom_name registry.add_instance(dev) dev.connect() if engine is not None: diff --git a/devices/analog_input.py b/devices/analog_input.py index c9e5d2a..1a7fefa 100644 --- a/devices/analog_input.py +++ b/devices/analog_input.py @@ -175,6 +175,7 @@ class AnalogInputDevice(BaseDevice): return { "device_type": self.DEVICE_TYPE, "device_id": self.info.device_id, + "name": self.info.name, "num_channels": self._num_channels, "simulate": self.simulate, "backend": self.backend, diff --git a/devices/arduino_device.py b/devices/arduino_device.py index 7c1d21b..e7fb537 100644 --- a/devices/arduino_device.py +++ b/devices/arduino_device.py @@ -203,6 +203,7 @@ class ArduinoDevice(BaseDevice): return { "device_type": self.DEVICE_TYPE, "device_id": self.info.device_id, + "name": self.info.name, "analog_pins": self._analog_pins, "di_pins": self._di_pins, "do_pins": self._do_pins, diff --git a/devices/digital_io.py b/devices/digital_io.py index 53395dc..954a8ac 100644 --- a/devices/digital_io.py +++ b/devices/digital_io.py @@ -258,6 +258,7 @@ class DigitalIODevice(BaseDevice): return { "device_type": self.DEVICE_TYPE, "device_id": self.info.device_id, + "name": self.info.name, "num_inputs": self._num_inputs, "num_outputs": self._num_outputs, "simulate": self.simulate, diff --git a/devices/nidaqmx_device.py b/devices/nidaqmx_device.py index 88bb932..ed19aeb 100644 --- a/devices/nidaqmx_device.py +++ b/devices/nidaqmx_device.py @@ -219,6 +219,7 @@ class NidaqmxDevice(BaseDevice): return { "device_type": self.DEVICE_TYPE, "device_id": self.info.device_id, + "name": self.info.name, "num_analog": self._num_analog, "min_v": self._min_v, "max_v": self._max_v, diff --git a/devices/serial_device.py b/devices/serial_device.py index 067656a..57faee8 100644 --- a/devices/serial_device.py +++ b/devices/serial_device.py @@ -168,6 +168,7 @@ class SerialDevice(BaseDevice): cfg: Dict[str, Any] = { "device_type": self.DEVICE_TYPE, "device_id": self.info.device_id, + "name": self.info.name, "port": self._port, "baud_rate": self._baud, "parse_format": self._fmt, diff --git a/plugins/motion_capture/device.py b/plugins/motion_capture/device.py index 58f89ce..3369962 100644 --- a/plugins/motion_capture/device.py +++ b/plugins/motion_capture/device.py @@ -102,6 +102,7 @@ class CameraDevice(BaseDevice): return { "device_type": self.DEVICE_TYPE, "device_id": self.info.device_id, + "name": self.info.name, "camera_index": self._camera_index, "simulate": self._simulate, "resolution": list(self._resolution) if self._resolution else None, diff --git a/ui/add_device_dialog.py b/ui/add_device_dialog.py index 2956a96..68ec3ad 100644 --- a/ui/add_device_dialog.py +++ b/ui/add_device_dialog.py @@ -299,6 +299,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 +453,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/windows/devices_window.py b/ui/windows/devices_window.py index ac9d13c..2c484e8 100644 --- a/ui/windows/devices_window.py +++ b/ui/windows/devices_window.py @@ -349,7 +349,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): -- cgit v1.2.3 From cf33095f77e6ff902fb69932fa704eb6192b3418 Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Wed, 29 Jul 2026 13:28:24 -0600 Subject: Add Developer Mode setting, Debug window, and gate simulate behind it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Settings > General > "Developer mode" checkbox (default on, so this repo's simulate-by-default workflow is unaffected out of the box) persisted through the existing core/app_settings.py load/save functions. Exposed to code that can't easily receive the settings dict via core.app_settings.is_developer_mode()/set_developer_mode(), a small in-memory cache MainWindow keeps in sync whenever settings are loaded or applied. Debug window (ui/windows/debug_window.py): minimal first pass per the plan — a live console. core/debug_log.py tees stdout/stderr into a ring buffer + Qt signal (installed once in main.py, before anything prints), so the window shows everything printed since app start, including from background poll threads, not just what's printed while it happens to be open. Its toolbar button is hidden unless developer mode is on. Simulate gating: every device's Simulation Mode checkbox/dropdown is now hidden when developer mode is off — ui/add_device_dialog.py (also covers the motion_capture camera panel, which shares this same checkbox rather than having its own), and the per-device config widgets in analog_input.py, arduino_device.py, digital_io.py, nidaqmx_device.py, serial_device.py. Hiding rather than force-clearing an already-simulating device's state — an existing simulated device keeps working if developer mode is turned off later; the option is just not offered again until it's back on. Co-Authored-By: Claude Sonnet 5 --- core/app_settings.py | 19 +++++++++++++ core/debug_log.py | 65 +++++++++++++++++++++++++++++++++++++++++++ devices/analog_input.py | 2 ++ devices/arduino_device.py | 2 ++ devices/digital_io.py | 2 ++ devices/nidaqmx_device.py | 2 ++ devices/serial_device.py | 6 +++- main.py | 2 ++ ui/add_device_dialog.py | 2 ++ ui/main_window.py | 31 ++++++++++++++++++++- ui/windows/debug_window.py | 65 +++++++++++++++++++++++++++++++++++++++++++ ui/windows/settings_window.py | 11 ++++++++ 12 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 core/debug_log.py create mode 100644 ui/windows/debug_window.py (limited to 'devices/digital_io.py') diff --git a/core/app_settings.py b/core/app_settings.py index e8bc7ae..ea3ac49 100644 --- a/core/app_settings.py +++ b/core/app_settings.py @@ -51,3 +51,22 @@ def save_settings(cfg: dict) -> None: json.dump(cfg, f, indent=2) except Exception: pass + + +# ── Developer mode ──────────────────────────────────────────────────────── +# +# In-memory cache so widgets that build device/config UI (Add Device dialog, +# per-device config panels, plugin panels) can check this without needing +# the full settings dict threaded through their constructors. MainWindow +# keeps it in sync with the persisted setting whenever settings are +# loaded/applied — see set_developer_mode() calls in ui/main_window.py. +_dev_mode = True + + +def is_developer_mode() -> bool: + return _dev_mode + + +def set_developer_mode(value: bool) -> None: + global _dev_mode + _dev_mode = value diff --git a/core/debug_log.py b/core/debug_log.py new file mode 100644 index 0000000..86a8a59 --- /dev/null +++ b/core/debug_log.py @@ -0,0 +1,65 @@ +""" +core/debug_log.py + +Tees stdout/stderr into an in-memory ring buffer + Qt signal so the Debug +window can show everything the app has printed since startup — including +messages from background poll threads (e.g. "[CMLLayer] poll error: ...") — +not just whatever gets printed while the window happens to be open. + +install() should be called once, early, before anything prints. The real +streams are still written to, so running from a terminal is unaffected. +""" + +from __future__ import annotations + +import sys +from collections import deque +from typing import Optional + +from PyQt6.QtCore import QObject, pyqtSignal + + +class _Broadcaster(QObject): + line_written = pyqtSignal(str) + + +class _StreamTee: + def __init__(self, real_stream, lines: deque, broadcaster: _Broadcaster): + self._real = real_stream + self._lines = lines + self._broadcaster = broadcaster + + def write(self, text: str) -> None: + self._real.write(text) + if text: + self._lines.append(text) + self._broadcaster.line_written.emit(text) + + def flush(self) -> None: + self._real.flush() + + def isatty(self) -> bool: + return False + + +_lines: Optional[deque] = None +_broadcaster: Optional[_Broadcaster] = None + + +def install(max_lines: int = 2000) -> None: + """Redirect sys.stdout/sys.stderr through the tee. Safe to call once.""" + global _lines, _broadcaster + if _broadcaster is not None: + return + _lines = deque(maxlen=max_lines) + _broadcaster = _Broadcaster() + sys.stdout = _StreamTee(sys.stdout, _lines, _broadcaster) + sys.stderr = _StreamTee(sys.stderr, _lines, _broadcaster) + + +def get_broadcaster() -> Optional[_Broadcaster]: + return _broadcaster + + +def get_history() -> str: + return "".join(_lines) if _lines is not None else "" diff --git a/devices/analog_input.py b/devices/analog_input.py index c9e5d2a..6ac82d4 100644 --- a/devices/analog_input.py +++ b/devices/analog_input.py @@ -250,6 +250,8 @@ class AnalogInputConfigWidget(QWidget): self.sim_check = QCheckBox("Simulation Mode (no hardware)") self.sim_check.setChecked(self.device.simulate) + from core.app_settings import is_developer_mode + self.sim_check.setVisible(is_developer_mode()) be_form.addRow(self.sim_check) root.addWidget(be_grp) diff --git a/devices/arduino_device.py b/devices/arduino_device.py index 7c1d21b..954d387 100644 --- a/devices/arduino_device.py +++ b/devices/arduino_device.py @@ -287,6 +287,8 @@ class ArduinoConfigWidget(QWidget): self.sim_chk = QCheckBox("Simulation Mode (no hardware)") self.sim_chk.setChecked(self.device.simulate) + from core.app_settings import is_developer_mode + self.sim_chk.setVisible(is_developer_mode()) ser_form.addRow(self.sim_chk) scan_row = QHBoxLayout() diff --git a/devices/digital_io.py b/devices/digital_io.py index 53395dc..2bd0243 100644 --- a/devices/digital_io.py +++ b/devices/digital_io.py @@ -290,6 +290,8 @@ class DigitalIOConfigWidget(QWidget): be_form.addRow("Backend:", self.be_cb) self.sim_chk = QCheckBox("Simulate") self.sim_chk.setChecked(self.device.simulate) + from core.app_settings import is_developer_mode + self.sim_chk.setVisible(is_developer_mode()) be_form.addRow(self.sim_chk) self.ni_dev_edit = QLineEdit(self.device._ni_device) diff --git a/devices/nidaqmx_device.py b/devices/nidaqmx_device.py index 88bb932..3f64998 100644 --- a/devices/nidaqmx_device.py +++ b/devices/nidaqmx_device.py @@ -294,6 +294,8 @@ class NidaqmxConfigWidget(QWidget): self.sim_chk = QCheckBox("Simulation Mode (no hardware)") self.sim_chk.setChecked(self.device.simulate) + from core.app_settings import is_developer_mode + self.sim_chk.setVisible(is_developer_mode()) ni_form.addRow(self.sim_chk) scan_row = QHBoxLayout() diff --git a/devices/serial_device.py b/devices/serial_device.py index 067656a..bd953b8 100644 --- a/devices/serial_device.py +++ b/devices/serial_device.py @@ -448,7 +448,11 @@ class SerialConfigWidget(QWidget): self._sim_cb = QComboBox() self._sim_cb.addItems(["Simulate", "Real Hardware"]) self._sim_cb.setCurrentIndex(0 if self.device.simulate else 1) - conn_form.addRow("Mode:", self._sim_cb) + from core.app_settings import is_developer_mode + mode_lbl = QLabel("Mode:") + dev_mode = is_developer_mode() + mode_lbl.setVisible(dev_mode); self._sim_cb.setVisible(dev_mode) + conn_form.addRow(mode_lbl, self._sim_cb) root.addWidget(conn_grp) diff --git a/main.py b/main.py index b023b99..ede6277 100644 --- a/main.py +++ b/main.py @@ -13,11 +13,13 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from PyQt6.QtWidgets import QApplication from ui.main_window import MainWindow +from core.debug_log import install as install_debug_log def main(): app = QApplication(sys.argv) app.setApplicationName("LabDAQ") + install_debug_log() # tee stdout/stderr for the Debug window, before anything prints qss = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ui", "style_dark.qss") if os.path.exists(qss): diff --git a/ui/add_device_dialog.py b/ui/add_device_dialog.py index 2956a96..4b2b373 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) diff --git a/ui/main_window.py b/ui/main_window.py index d0e0ac4..ca4274c 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) @@ -314,12 +321,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]() @@ -370,6 +379,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()) @@ -554,10 +579,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/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/settings_window.py b/ui/windows/settings_window.py index 1630efa..e24da7f 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, @@ -108,6 +109,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 +129,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"]) @@ -288,6 +298,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(), -- cgit v1.2.3