diff options
Diffstat (limited to 'ui/windows')
| -rw-r--r-- | ui/windows/channels_window.py | 20 | ||||
| -rw-r--r-- | ui/windows/debug_window.py | 65 | ||||
| -rw-r--r-- | ui/windows/devices_window.py | 27 | ||||
| -rw-r--r-- | ui/windows/plot_window.py | 16 | ||||
| -rw-r--r-- | ui/windows/settings_window.py | 45 |
5 files changed, 125 insertions, 48 deletions
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 1afbca5..09b7a1a 100644 --- a/ui/windows/settings_window.py +++ b/ui/windows/settings_window.py @@ -51,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) @@ -78,7 +79,6 @@ class SettingsWindow(QWidget): tabs.addTab(self._acquisition_tab(), " Acquisition ") tabs.addTab(self._display_tab(), " Display ") tabs.addTab(self._plugins_tab(), " Plugins ") - tabs.addTab(self._advanced_tab(), " Advanced ") # Bottom bar btm = QWidget(); btm.setObjectName("cfgBottomBar") @@ -110,6 +110,15 @@ 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, and enables verbose DEBUG output in\n" + "the terminal. 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) @@ -122,13 +131,13 @@ 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"]) self._tw_sp.setValue(self.cfg["time_window_s"]) self._legend_chk.setChecked(self.cfg["show_legend"]) self._grid_chk.setChecked(self.cfg["show_grid"]) - self._dev_mode_chk.setChecked(self.cfg.get("developer_mode", False)) def _acquisition_tab(self): w = QWidget() @@ -236,6 +245,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) @@ -259,32 +269,21 @@ class SettingsWindow(QWidget): return card - def _advanced_tab(self): - w = QWidget() - scroll = QScrollArea(); scroll.setWidgetResizable(True) - scroll.setObjectName("deviceScroll") - cont = QWidget(); lay = QFormLayout(cont) - lay.setContentsMargins(16, 14, 16, 14); lay.setSpacing(10) - - self._dev_mode_chk = QCheckBox() - self._dev_mode_chk.setChecked(self.cfg.get("developer_mode", False)) - lay.addRow("Developer mode:", self._dev_mode_chk) - - note = QLabel("Enables verbose DEBUG output in the terminal.\nNo effect when running as a packaged app.") - note.setObjectName("traceSource"); note.setWordWrap(True) - lay.addRow("", note) - - scroll.setWidget(cont) - root = QVBoxLayout(w); root.setContentsMargins(0, 0, 0, 0); root.addWidget(scroll) - return w - 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: + # 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 ─────────────────────────────────────────────────────────── @@ -310,13 +309,13 @@ 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(), "time_window_s": self._tw_sp.value(), "show_legend": self._legend_chk.isChecked(), "show_grid": self._grid_chk.isChecked(), - "developer_mode": self._dev_mode_chk.isChecked(), }) self.theme_changed.emit(theme) self.settings_changed.emit(dict(self.cfg)) |
