summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
authorChristian Kolset <ckolset@colostate.edu>2026-07-29 13:28:24 -0600
committerChristian Kolset <ckolset@colostate.edu>2026-07-29 13:28:24 -0600
commitcf33095f77e6ff902fb69932fa704eb6192b3418 (patch)
treea6a4629adb61190b74352a894e37f8efd585c6c1 /ui
parentb407a079000f6a4b04ecf38eca17c57719e7a7ec (diff)
Add Developer Mode setting, Debug window, and gate simulate behind it
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 <noreply@anthropic.com>
Diffstat (limited to 'ui')
-rw-r--r--ui/add_device_dialog.py2
-rw-r--r--ui/main_window.py31
-rw-r--r--ui/windows/debug_window.py65
-rw-r--r--ui/windows/settings_window.py11
4 files changed, 108 insertions, 1 deletions
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(),