summaryrefslogtreecommitdiff
path: root/ui/main_window.py
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-08-02 01:34:43 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-08-02 01:34:43 -0600
commita3aa1df99df8f413cac2ba6020b7cd0dec6d2390 (patch)
treea4c5feca6b0db326d9e54f417abc132c1270a7a3 /ui/main_window.py
parentf5066a8ca2fb50aa3dddf2c8847e52574cdde6ad (diff)
parentf1aaffbc3eb1e2c154315c556d2555803eea7997 (diff)
Merge origin/main: reconcile ConfigWindow consolidation with Debug window + protocol updates
origin/main (23 commits) added a Debug window/log system on top of the old separate Devices/Channels/Plot windows, plus protocol fixes (cml, mark10, modbus_rtu, scpi) and device/profile changes. Local main (3 commits) replaced the separate windows with a unified ConfigWindow + dock panel. Kept local's ConfigWindow/dock architecture and ported the Debug window onto it: new toolbar button + _open_debug(), gated by developer_mode same as origin's version. Deduped the two independent "developer mode" toggles that had collided in SettingsWindow (origin's General-tab checkbox gating Debug window + device sim-mode visibility, local's Advanced-tab checkbox setting verbose logging) into one General-tab checkbox that does both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'ui/main_window.py')
-rw-r--r--ui/main_window.py96
1 files changed, 93 insertions, 3 deletions
diff --git a/ui/main_window.py b/ui/main_window.py
index 93fbef2..01e0505 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -108,10 +108,11 @@ from ui.profile_manager_ui import ProfileButton
from ui.windows.config_window import ConfigWindow
from ui.windows.plot_window import build_default_layout
from ui.windows.settings_window import SettingsWindow
+from ui.windows.debug_window import DebugWindow
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")
@@ -132,6 +133,7 @@ class MainWindow(QMainWindow):
self._elapsed = 0
self._win_config = 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")
@@ -252,6 +254,12 @@ class MainWindow(QMainWindow):
set_btn.clicked.connect(lambda c: self._open_settings())
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._open_debug())
+ tb.addWidget(debug_btn); self._btn_debug = debug_btn
+ debug_btn.setVisible(False) # shown/hidden by _update_debug_btn_visibility per developer-mode setting
+
# ── Central ───────────────────────────────────────────────────────
central = QWidget(); self.setCentralWidget(central)
@@ -294,6 +302,9 @@ class MainWindow(QMainWindow):
self._clock = QTimer(self); self._clock.setInterval(1000)
self._clock.timeout.connect(self._tick)
+ self._rec_blink = QTimer(self); self._rec_blink.setInterval(600)
+ self._rec_blink.timeout.connect(self._tick_rec_blink)
+
def _on_ctrl_dock_visibility(self, visible: bool):
self._ctrl_tab.setVisible(not visible)
if hasattr(self, "_act_ctrl_panel"):
@@ -409,11 +420,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)
@@ -440,6 +500,12 @@ class MainWindow(QMainWindow):
self._win_config.tabs.setCurrentIndex(tab)
self._show_win(self._win_config, "right")
+ def _open_debug(self):
+ 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 _on_config_closed(self):
self._act_devices.setChecked(False)
self._act_channels.setChecked(False)
@@ -603,9 +669,12 @@ class MainWindow(QMainWindow):
self._run_btn.setText("⏹ STOP"); self._log_btn.setEnabled(True)
self._clock.start(); self._status.setText("Acquiring…")
else:
+ self._ctrl.safe_stop_all() # master switch β€” stop outputs before halting acquisition
self.engine.stop()
self._run_btn.setText("β–Ά RUN")
- if self._log_btn.isChecked(): self._log_btn.setChecked(False)
+ if self._log_btn.isChecked():
+ self._log_btn.setChecked(False)
+ self._toggle_log(False) # setChecked() alone won't fire clicked β€” stop blink/logging explicitly
self._log_btn.setEnabled(False); self._clock.stop()
self._status.setText("Stopped")
@@ -621,8 +690,18 @@ class MainWindow(QMainWindow):
os.path.join(self._settings.get("log_dir", "logs"), ""))
self._log_btn.setText("⏹ LOGGING")
self._status.setText(f"Logging β†’ {p}")
+ self._rec_blink.start()
else:
self.engine.stop_logging(); self._log_btn.setText("⬀ LOG")
+ self._rec_blink.stop()
+ self._log_btn.setProperty("recording", False)
+ self._log_btn.style().unpolish(self._log_btn); self._log_btn.style().polish(self._log_btn)
+
+ def _tick_rec_blink(self):
+ """Pulse the Log button's background while a recording is active."""
+ on = not self._log_btn.property("recording")
+ self._log_btn.setProperty("recording", on)
+ self._log_btn.style().unpolish(self._log_btn); self._log_btn.style().polish(self._log_btn)
# ── Theme / settings ──────────────────────────────────────────────────
@@ -641,6 +720,17 @@ class MainWindow(QMainWindow):
def _apply_developer_mode(self, enabled: bool):
level = logging.DEBUG if enabled else logging.WARNING
logging.getLogger().setLevel(level)
+ set_developer_mode(enabled)
+ self._update_debug_btn_visibility()
+
+ 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 _on_settings(self, cfg: dict):
self._settings.update(cfg)
@@ -654,7 +744,7 @@ class MainWindow(QMainWindow):
self._time_lbl.setText(f"{h:02d}:{m:02d}:{s:02d}")
def closeEvent(self, event):
- for w in (self._win_config, self._win_settings):
+ for w in (self._win_config, self._win_settings, self._win_debug):
if w: w.close()
for plugin in list(self._plugin_mgr.get_loaded()):
try: