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/namespace_panel.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/namespace_panel.py')
| -rw-r--r-- | ui/namespace_panel.py | 55 |
1 files changed, 47 insertions, 8 deletions
diff --git a/ui/namespace_panel.py b/ui/namespace_panel.py index 9616db7..e2b8809 100644 --- a/ui/namespace_panel.py +++ b/ui/namespace_panel.py @@ -5,19 +5,24 @@ NAMESPACE panel — dockable, live browser of everything referenceable in a script: Signals (raw device endpoints), Channels (processed/derived values), and vars (the shared dict bridging Control Panel On Action Scripts and Channel scripts). Read-only reference, not an editor — editing -still happens in Configuration > Channels. +still happens in Configuration > Channels — with one narrow exception: +when constructed with `on_create_channel`, each Signal row gets a ⊕ +button to create a passthrough Channel from it directly. Only +ConfigWindow's Channels-tab instance opts into this (config_window.py); +the dockable instance on the main window (ui/main_window.py) does not +pass a callback, so it stays exactly the plain read-only reference. Docked opposite the Control Panel (ui/main_window.py), built the same way: a QDockWidget you can drag, float, redock, or close. """ from __future__ import annotations -from typing import Dict, Tuple +from typing import Callable, Dict, Optional, Tuple from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView, - QApplication, + QToolButton, QApplication, ) from PyQt6.QtCore import QTimer, pyqtSlot from PyQt6.QtGui import QColor @@ -33,6 +38,7 @@ _C_NAME = 2 _C_DIR = 3 _C_VALUE = 4 _C_SOURCE = 5 +_C_CREATE = 6 # only present when on_create_channel is set def _fmt_value(v) -> str: @@ -54,10 +60,14 @@ def _fmt_ref(dev_id: str, ch_id: str) -> str: class NamespacePanel(QWidget): - def __init__(self, registry: DeviceRegistry, processor: SignalProcessor): + def __init__(self, registry: DeviceRegistry, processor: SignalProcessor, + on_create_channel: Optional[Callable[[str, str, str, str, str], None]] = None): super().__init__() self.registry = registry self.processor = processor + # (dev_id, ch_id, name, unit, color) -> None, called when a Signal + # row's ⊕ button is clicked. None hides the column entirely. + self.on_create_channel = on_create_channel # row key -> row index. Keys: # ("signal", dev_id, ch_id) | ("channel", channel_id) | ("var", name) self._row_index: Dict[Tuple, int] = {} @@ -84,10 +94,11 @@ class NamespacePanel(QWidget): self._table = QTableWidget() self._table.setObjectName("channelTable") - self._table.setColumnCount(6) - self._table.setHorizontalHeaderLabels( - ["Kind", "ID", "Name", "Dir", "Value", "Source"] - ) + labels = ["Kind", "ID", "Name", "Dir", "Value", "Source"] + if self.on_create_channel: + labels.append("") + self._table.setColumnCount(len(labels)) + self._table.setHorizontalHeaderLabels(labels) hdr = self._table.horizontalHeader() hdr.setSectionResizeMode(_C_SOURCE, QHeaderView.ResizeMode.Stretch) hdr.setSectionResizeMode(_C_KIND, QHeaderView.ResizeMode.ResizeToContents) @@ -95,6 +106,8 @@ class NamespacePanel(QWidget): hdr.setSectionResizeMode(_C_NAME, QHeaderView.ResizeMode.ResizeToContents) hdr.setSectionResizeMode(_C_DIR, QHeaderView.ResizeMode.ResizeToContents) hdr.setSectionResizeMode(_C_VALUE, QHeaderView.ResizeMode.ResizeToContents) + if self.on_create_channel: + hdr.setSectionResizeMode(_C_CREATE, QHeaderView.ResizeMode.ResizeToContents) self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self._table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) self._table.setAlternatingRowColors(True) @@ -102,6 +115,17 @@ class NamespacePanel(QWidget): self._table.cellDoubleClicked.connect(self._on_double_clicked) lay.addWidget(self._table) + def _apply_min_width(self): + """Floor the panel's width at the sum of its columns' natural sizes + (Source gets a fixed placeholder rather than its live stretched + width, which would be circular) — so dragging the Channels-tab + splitter divider narrower can never push the ⊕ create-channel + column (or any other column) behind the horizontal scrollbar.""" + total = 0 + for i in range(self._table.columnCount()): + total += 80 if i == _C_SOURCE else self._table.columnWidth(i) + self.setMinimumWidth(total + 24) + # ── Row-set (structural) rebuild — devices/channels/vars added or removed ── def _desired_keys(self): @@ -125,9 +149,13 @@ class NamespacePanel(QWidget): for row, key in enumerate(keys): self._row_index[key] = row self._paint_row(row, key) + self._apply_min_width() def _paint_row(self, row: int, key: Tuple): kind = key[0] + if self.on_create_channel: + self._table.removeCellWidget(row, _C_CREATE) # row's kind may have + # changed since last paint if kind == "signal": _, dev_id, ch_id = key dev = self.registry.get_instance(dev_id) @@ -136,6 +164,17 @@ class NamespacePanel(QWidget): is_wr = bool(ch and ch.writable) self._set_row(row, "Signal", ch_id, name, "WRITE" if is_wr else "READ", None, _fmt_ref(dev_id, ch_id)) + if self.on_create_channel and ch is not None: + btn = QToolButton() + btn.setText("⊕") + btn.setObjectName("addTraceBtn") + btn.setFixedSize(24, 22) + btn.setToolTip("Create a Channel from this Signal") + btn.clicked.connect( + lambda _=False, d=dev_id, c=ch_id, n=ch.name, u=ch.unit, col=ch.color: + self.on_create_channel(d, c, n, u, col) + ) + self._table.setCellWidget(row, _C_CREATE, btn) elif kind == "channel": _, channel_id = key dc = next((d for d in self.processor.get_derived() |
