diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-09-20 22:30:13 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-09-20 22:30:13 -0600 |
| commit | 006366ef8099c8313ae596bf6267e9f98dfbeb5e (patch) | |
| tree | cc3f76db630c21ec6422a6fd4f3053903a47d186 /ui/windows/config_window.py | |
| parent | bb8be051ffffa56a01093c9f3aaa80e84748568f (diff) | |
Make Channel the single source of truth for a reading's name
Device tab stays hardware-only (pins, wiring, may vary per workstation);
the Channels window owns the profile-facing identity. Concretely:
- Fix a real regression: SerialDevice generic-format Apply & Reconnect
nulled out custom channel_names/units every time (no fallback, unlike
every other optional field in reconfigure()), silently reverting to
sig0/sig1/... ids.
- Plot legend now live-tracks the current Signal/Channel name instead of
freezing tr.label at add-time, so a rename shows up without re-adding
the trace.
- Add an inline quick-reconnect (combo + button, pre-selects the actual
current source) directly on each Channel block in the Channels window,
for the common single-source case — rewiring hardware no longer means
touching every Plot/pipeline that referenced it.
- Collapse the Channel ID field behind an "ID ▸" advanced toggle;
channel_id is already auto-derived from the name (_slug_id) and rarely
needs to be seen.
- Devices tab's Signal "Name" column relabeled "Label" with a tooltip —
de-emphasized now that Channel name is the real identity.
- Move "create a Channel from this Signal" from a column on the Devices
tab's signal table to a ⊕ button on each Signal row in the Channels
tab's Namespace panel — the action now lives where the identity is
created, not on the hardware tab. Only that instance opts in
(on_create_channel); the read-only Namespace dock on the main window
is unaffected.
- Floor the Namespace panel's width at its columns' natural size so
dragging the Channels-tab splitter narrower can't push the ⊕ column
behind the horizontal scrollbar.
- Breadcrumb log when a profile-saved Signal override can't be applied
because the signal_id no longer exists.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'ui/windows/config_window.py')
| -rw-r--r-- | ui/windows/config_window.py | 93 |
1 files changed, 40 insertions, 53 deletions
diff --git a/ui/windows/config_window.py b/ui/windows/config_window.py index 62789f1..7ec0355 100644 --- a/ui/windows/config_window.py +++ b/ui/windows/config_window.py @@ -5,10 +5,15 @@ Unified Configuration window — Devices | Channels | Plot in one tabbed window. Devices tab layout (2 columns): Left — physical devices (add/configure/remove) - Right — signal table (Device / Signal ID / Dir / On / Name / Unit / Min / Max) + Right — signal table (Device / Signal ID / Dir / On / Label / Unit / Min / Max) Channels tab layout (2 columns): - Left — Namespace browser (read-only reference: Signals / Channels / vars) + Left — Namespace browser (Signals / Channels / vars). Mostly a read-only + reference, with one exception: each Signal row has a ⊕ button to + create a passthrough Channel from it directly — the quick "make + this Signal a Channel" path. See ui/namespace_panel.py — only + this instance opts into that button (on_create_channel), not the + read-only dockable Namespace panel on the main window. Right — derived / virtual channels — operations (filters/calibration/math/script) are configured per Channel here, not on the Signals side. """ @@ -18,7 +23,7 @@ from copy import deepcopy from typing import List, Optional, Set, Tuple from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QToolButton, + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QScrollArea, QFrame, QTabWidget, QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox, QMessageBox, QAbstractItemView, QSplitter, QDoubleSpinBox, @@ -55,32 +60,31 @@ class _SignalsTab(SignalsTab): _C_UNIT = 5 _C_MIN = 6 _C_MAX = 7 - _C_MKCH = 8 # "+ Channel" button - channel_created = pyqtSignal() channel_renamed = pyqtSignal(str, str, str, str) # old_dev, old_id, new_dev, new_id - def __init__(self, registry: DeviceRegistry, processor: SignalProcessor): - self._processor = processor - super().__init__(registry) - def _build(self): lay = QVBoxLayout(self) lay.setContentsMargins(0, 0, 0, 0) self._table = QTableWidget() self._table.setObjectName("channelTable") - self._table.setColumnCount(9) + self._table.setColumnCount(8) self._table.setHorizontalHeaderLabels( - ["Device", "Signal ID", "Dir", "On", "Name", "Unit", "Min", "Max", ""] + ["Device", "Signal ID", "Dir", "On", "Label", "Unit", "Min", "Max"] ) + name_hdr_item = self._table.horizontalHeaderItem(self._C_NAME) + if name_hdr_item is not None: + name_hdr_item.setToolTip( + "Technical label only — for the profile-facing name, create a Channel " + "in the Channels window." + ) 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_DIR, QHeaderView.ResizeMode.ResizeToContents) hdr.setSectionResizeMode(self._C_ENABLED, QHeaderView.ResizeMode.ResizeToContents) - hdr.setSectionResizeMode(self._C_MKCH, QHeaderView.ResizeMode.ResizeToContents) self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self._table.setAlternatingRowColors(True) self._table.verticalHeader().setVisible(False) @@ -139,25 +143,6 @@ class _SignalsTab(SignalsTab): self._table.setItem(row, self._C_MIN, QTableWidgetItem(str(ch.min_value))) self._table.setItem(row, self._C_MAX, QTableWidgetItem(str(ch.max_value))) - mkch_w = QWidget() - mkch_l = QHBoxLayout(mkch_w) - mkch_l.setContentsMargins(2, 0, 2, 0) - mkch_l.setAlignment(Qt.AlignmentFlag.AlignCenter) - mkch_btn = QToolButton() - mkch_btn.setText("⊕") - mkch_btn.setObjectName("addTraceBtn") - mkch_btn.setFixedSize(24, 22) - mkch_btn.setToolTip("Create a Channel from this Signal") - d_cap, c_cap, n_cap, u_cap, col_cap = ( - dev.info.device_id, ch.signal_id, ch.name, ch.unit, ch.color - ) - mkch_btn.clicked.connect( - lambda _=False, d=d_cap, c=c_cap, n=n_cap, u=u_cap, col=col_cap: - self._on_create_channel(d, c, n, u, col) - ) - mkch_l.addWidget(mkch_btn) - self._table.setCellWidget(row, self._C_MKCH, mkch_w) - row += 1 self._table.blockSignals(False) @@ -205,25 +190,6 @@ class _SignalsTab(SignalsTab): name_item.setData(Qt.ItemDataRole.UserRole, (dev_id, new_id)) self.channel_renamed.emit(dev_id, old_id, dev_id, new_id) - def _on_create_channel(self, dev_id: str, ch_id: str, ch_name: str, unit: str, color: str): - """Create a passthrough Channel (kind="none") from this Signal, - named after it — the "1 Signal -> 1 Channel" quick path. No-ops if - a passthrough Channel for this exact Signal already exists.""" - existing = self._processor.get_derived() - for dc in existing: - if dc.kind == "none" and dc.sources == [(dev_id, ch_id)]: - return - existing_names = {d.name for d in existing} - existing_ids = {d.channel_id for d in existing} - name = _dedupe_name(ch_name, existing_names) - channel_id = _slug_id(name, existing_ids) - dc = Channel(channel_id=channel_id, name=name, kind="none", - sources=[(dev_id, ch_id)], source_names=[ch_name], - unit=unit, color=color or "#00d4ff") - err = self._processor.add_derived(dc) - if err is None: - self.channel_created.emit() - # ══════════════════════════════════════════════════════════════════════════════ # Plot tab — PlotWindow embedded without floating chrome @@ -401,7 +367,7 @@ class ConfigWindow(QWidget): sig_div = QFrame(); sig_div.setFrameShape(QFrame.Shape.HLine) sig_div.setObjectName("devWindowDivider"); sig_l.addWidget(sig_div) - self._signals_tab = _SignalsTab(self.registry, self.processor) + self._signals_tab = _SignalsTab(self.registry) self._signals_tab.channel_visibility_changed.connect(self._on_sig_visibility) self._signals_tab.channel_name_changed.connect(self._on_sig_name) self._signals_tab.channel_unit_changed.connect(self._on_sig_unit) @@ -505,7 +471,8 @@ class ConfigWindow(QWidget): ns_div = QFrame(); ns_div.setFrameShape(QFrame.Shape.HLine) ns_div.setObjectName("devWindowDivider"); ns_l.addWidget(ns_div) - self._namespace_panel = NamespacePanel(self.registry, self.processor) + self._namespace_panel = NamespacePanel(self.registry, self.processor, + on_create_channel=self._on_create_channel) self.processor.processed_data.connect(self._namespace_panel.on_processed_data) ns_l.addWidget(self._namespace_panel, 1) splitter.addWidget(ns_w) @@ -521,7 +488,6 @@ class ConfigWindow(QWidget): if inner_splitters: inner_splitters[0].widget(0).setVisible(False) splitter.addWidget(self._pipeline_tab) - self._signals_tab.channel_created.connect(self._pipeline_tab.refresh_derived) # Channels side larger by default splitter.setStretchFactor(0, 1) @@ -529,6 +495,27 @@ class ConfigWindow(QWidget): self.tabs.addTab(w, " Channels ") + def _on_create_channel(self, dev_id: str, ch_id: str, ch_name: str, unit: str, color: str): + """Create a passthrough Channel (kind="none") from this Signal, + named after it — the "1 Signal -> 1 Channel" quick path, triggered + from the ⊕ button on a Signal row in the Channels tab's Namespace + panel (ui/namespace_panel.py). No-ops if a passthrough Channel for + this exact Signal already exists.""" + existing = self.processor.get_derived() + for dc in existing: + if dc.kind == "none" and dc.sources == [(dev_id, ch_id)]: + return + existing_names = {d.name for d in existing} + existing_ids = {d.channel_id for d in existing} + name = _dedupe_name(ch_name, existing_names) + channel_id = _slug_id(name, existing_ids) + dc = Channel(channel_id=channel_id, name=name, kind="none", + sources=[(dev_id, ch_id)], source_names=[ch_name], + unit=unit, color=color or "#00d4ff") + err = self.processor.add_derived(dc) + if err is None: + self._pipeline_tab.refresh_derived() + def _on_sig_visibility(self, dev_id: str, ch_id: str, enabled: bool): self._pipeline_tab.on_channel_enabled_changed(dev_id, ch_id, enabled) self.channel_visibility_changed.emit(dev_id, ch_id, enabled) |
