summaryrefslogtreecommitdiff
path: root/ui/windows/config_window.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/windows/config_window.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/windows/config_window.py')
-rw-r--r--ui/windows/config_window.py93
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)