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/channels_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/channels_window.py')
| -rw-r--r-- | ui/windows/channels_window.py | 114 |
1 files changed, 102 insertions, 12 deletions
diff --git a/ui/windows/channels_window.py b/ui/windows/channels_window.py index 31d1458..cc83b3b 100644 --- a/ui/windows/channels_window.py +++ b/ui/windows/channels_window.py @@ -431,8 +431,18 @@ class PipelineTab(QWidget): phys_bar = QWidget(); phys_bar.setObjectName("cfgGlobalBar") pb_lay = QHBoxLayout(phys_bar); pb_lay.setContentsMargins(10, 7, 10, 7); pb_lay.setSpacing(6) pb_lbl = QLabel("Signals"); pb_lbl.setObjectName("devWindowTitle") - pb_lay.addWidget(pb_lbl, 1) + pb_lay.addWidget(pb_lbl) + self._src_cb = _channel_combo(self.registry, self.processor, include_derived=True) + self._src_cb.setObjectName("channelPickerCb") + self._src_cb.insertItem(0, "Signal: none (empty channel)", userData=None) + self._src_cb.setCurrentIndex(0) + pb_lay.addWidget(self._src_cb, 1) + add_virt = QPushButton("+ New Channel"); add_virt.setObjectName("addTraceBtn") + add_virt.setToolTip("Create a Channel from the selected Signal") + add_virt.clicked.connect(self._add_virtual) + pb_lay.addWidget(add_virt) add_btn = QPushButton("+ Add"); add_btn.setObjectName("addTraceBtn") + add_btn.setToolTip("Add an existing Signal to this list") add_btn.clicked.connect(self._on_add_channel) pb_lay.addWidget(add_btn) phys_l.addWidget(phys_bar) @@ -458,15 +468,7 @@ 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) - self._src_cb = _channel_combo(self.registry, self.processor, include_derived=True) - self._src_cb.setObjectName("channelPickerCb") - self._src_cb.insertItem(0, "Signal: 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) + vb_lay.addWidget(vb_lbl, 1) virt_l.addWidget(virt_bar) div2 = QFrame(); div2.setFrameShape(QFrame.Shape.HLine) @@ -861,15 +863,45 @@ class ChannelBlock(QFrame): outer.addWidget(hdr) hdr.mousePressEvent = lambda e: self._toggle() + # Subtitle — always visible (collapsed or expanded): what this + # Channel is currently wired to, plus a one-click rebind for the + # common single-source case. Lets a rewire be fixed (or a channel + # proactively repointed at a different Signal) without expanding + # the block or hunting for the per-source combo in the editor body. + sub = QWidget(); sub.setObjectName("plotBlockSubtitle") + sub_lay = QHBoxLayout(sub) + sub_lay.setContentsMargins(30, 0, 6, 4); sub_lay.setSpacing(6) + self._src_lbl = QLabel(""); self._src_lbl.setObjectName("traceSource") + sub_lay.addWidget(self._src_lbl, 1) + self._reconnect_cb = _channel_combo(self.registry, include_derived=False) + self._reconnect_cb.setFixedWidth(220) + sub_lay.addWidget(self._reconnect_cb) + self._reconnect_btn = QPushButton("↺ Reconnect") + self._reconnect_btn.setObjectName("addTraceBtn") + self._reconnect_btn.setToolTip("Point this channel at a different physical Signal") + self._reconnect_btn.clicked.connect(self._do_reconnect) + sub_lay.addWidget(self._reconnect_btn) + outer.addWidget(sub) + # Body body = QWidget(); body.setObjectName("plotBlockSettings") bl = QVBoxLayout(body); bl.setContentsMargins(10, 8, 10, 10); bl.setSpacing(6) - # ID / unit row + # ID / unit row — the id is auto-derived from the name at creation + # (_slug_id()) and normally never needs to be seen, let alone typed; + # tucked behind a toggle so Name is the only identity field most + # users ever interact with. meta = QHBoxLayout() - meta.addWidget(QLabel("ID:")) + self._id_toggle = QToolButton(); self._id_toggle.setText("ID ▸") + self._id_toggle.setObjectName("traceRemoveBtn"); self._id_toggle.setCheckable(True) + self._id_toggle.setToolTip(f"Technical id: {self.dc.channel_id} — only needed to " + f"reference this channel from an expression/script by id, " + f"or to rename it. Click to edit.") + self._id_toggle.toggled.connect(self._toggle_id_box) + meta.addWidget(self._id_toggle) self._id = QLineEdit(self.dc.channel_id) self._id.setObjectName("traceLabel"); self._id.setFixedWidth(100) + self._id.setVisible(False) self._id.textChanged.connect(lambda t: setattr(self.dc, "channel_id", t)) meta.addWidget(self._id) meta.addWidget(QLabel("Unit:")) @@ -1023,6 +1055,58 @@ class ChannelBlock(QFrame): self._update_vars_hint() self._update_visibility() self.refresh_missing_sources() + self._refresh_source_subtitle() + + def _refresh_source_subtitle(self): + """Update the always-visible '← wired-to' line and show/hide the + inline quick-reconnect control — only offered for the unambiguous + single-source case; multi-source channels use the per-source combos + in the expanded editor instead.""" + names = [f"{_channel_display_name(self.registry, self.processor, d, c)} ({d}/{c})" + for d, c in self.dc.sources] + self._src_lbl.setText("← " + ", ".join(names) if names else "no source") + single = len(self.dc.sources) == 1 + self._reconnect_cb.setVisible(single) + self._reconnect_btn.setVisible(single) + # Pre-select the actual current source — otherwise the combo always + # shows whatever landed first in registry iteration order, which + # looks like that's what's connected even when it isn't. + if single: + cur = self.dc.sources[0] + self._reconnect_cb.blockSignals(True) + for i in range(self._reconnect_cb.count()): + if self._reconnect_cb.itemData(i) == cur: + self._reconnect_cb.setCurrentIndex(i) + break + else: + self._reconnect_cb.setCurrentIndex(-1) + self._reconnect_cb.blockSignals(False) + + def _do_reconnect(self): + """Inline one-click rebind: point this channel's sole source at + whatever Signal is currently selected in the quick-reconnect combo — + no need to expand the block or use the per-source editor combos.""" + new_src = self._reconnect_cb.currentData() + if not new_src or len(self.dc.sources) != 1: + return + self.dc.sources = [new_src] + self.dc.source_names = [_channel_display_name(self.registry, self.processor, *new_src)] + if self._src_combos: + cb = self._src_combos[0] + for i in range(cb.count()): + if cb.itemData(i) == new_src: + cb.setCurrentIndex(i) + break + err = self.processor.add_derived(self.dc) + self._refresh_source_subtitle() + self.refresh_missing_sources() + if err: + self._st.setText(f"⚠ {err[:100]}") + self._st.setStyleSheet("color:#ef4444;") + else: + self._st.setText("✓ Reconnected") + self._st.setStyleSheet("color:#22c55e;") + self.applied.emit(self.dc) def refresh_missing_sources(self): """Update the header warning icon for a source signal that no longer @@ -1046,6 +1130,10 @@ class ChannelBlock(QFrame): self._body.setVisible(self._expanded) self._toggle_btn.setText("▼" if self._expanded else "▶") + def _toggle_id_box(self, checked: bool): + self._id.setVisible(checked) + self._id_toggle.setText("ID ▾" if checked else "ID ▸") + def _add_src(self, src=None): row = QHBoxLayout() cb = _channel_combo(self.registry, self.processor, @@ -1247,6 +1335,8 @@ class ChannelBlock(QFrame): elif self.dc.kind in ("function", "custom_script"): self.dc.script = self._code.toPlainText() err = self.processor.add_derived(self.dc) + self._refresh_source_subtitle() + self.refresh_missing_sources() # Emitted regardless of whether this channel's own expression/script # compiles — propagate_rename() (via id_renamed) still needs to run # so other channels' sources / plot traces / controls don't dangle. |
