summaryrefslogtreecommitdiff
path: root/ui/namespace_panel.py
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-09-20 22:30:13 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-09-20 22:30:13 -0600
commit006366ef8099c8313ae596bf6267e9f98dfbeb5e (patch)
treecc3f76db630c21ec6422a6fd4f3053903a47d186 /ui/namespace_panel.py
parentbb8be051ffffa56a01093c9f3aaa80e84748568f (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.py55
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()