summaryrefslogtreecommitdiff
path: root/ui/windows/channels_window.py
diff options
context:
space:
mode:
authorChristian Kolset <ckolset@colostate.edu>2026-08-13 13:39:25 -0600
committerChristian Kolset <ckolset@colostate.edu>2026-08-13 13:39:25 -0600
commit34c12b2b5b280206890cd03153b1c95116acc85e (patch)
tree9b3dee7e0818d94c0bae87a35e7bf930a612ca2c /ui/windows/channels_window.py
parenta6e812dcbf3f2fc27565e52b6c0c6f9b7e4363ee (diff)
Channels tab drag-to-reorder, Namespace absolute references, fix stale Channel ID renames
Generalized the Plot pane drag-to-reorder (_PaneDragHandle/_PlotDropContainer -> _DragHandle/_DragReorderContainer) and reused it for the Channels tab's derived-channel list; reordering also updates SignalProcessor's evaluation order (list order matters for chained Channels) via new reorder_derived(). Namespace panel: added an ID column (bare channel_id, matching the actual channels.get(...) script convention) and fixed the Source column for Channels to show resolved absolute references (dev_id/ch_id for a Signal, bare id for a chained Channel) instead of the operation kind, so a chain is traceable by eye. Fixed a real bug this surfaced: renaming a Channel's ID never propagated to other Channels' sources, leaving dangling ("derived", old_id) references that silently stopped updating. Added SignalProcessor.rename_derived() to fix up sources plus carry the ring buffer/latest-value cache to the new key, wired to DerivedBlock's Apply. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'ui/windows/channels_window.py')
-rw-r--r--ui/windows/channels_window.py77
1 files changed, 74 insertions, 3 deletions
diff --git a/ui/windows/channels_window.py b/ui/windows/channels_window.py
index 2388b83..87d7840 100644
--- a/ui/windows/channels_window.py
+++ b/ui/windows/channels_window.py
@@ -38,6 +38,7 @@ from core.signal_processor import (
SignalProcessor, ChannelPipeline, DerivedChannel,
FilterBase, FILTER_CLASSES,
)
+from ui.windows.plot_window import _DragHandle, _DragReorderContainer
_DERIVED_COLORS = [
"#f72585","#7209b7","#4361ee","#4cc9f0",
@@ -473,7 +474,7 @@ class PipelineTab(QWidget):
virt_scroll = QScrollArea(); virt_scroll.setWidgetResizable(True)
virt_scroll.setObjectName("deviceScroll")
virt_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
- self._virt_cont = QWidget()
+ self._virt_cont = _DragReorderContainer(self)
self._virt_inner = QVBoxLayout(self._virt_cont)
self._virt_inner.setContentsMargins(10, 10, 10, 10); self._virt_inner.setSpacing(6)
self._virt_inner.addStretch()
@@ -645,6 +646,8 @@ class PipelineTab(QWidget):
blk = DerivedBlock(dc, self.registry, self.processor)
blk.removed.connect(self._rm_virtual)
blk.applied.connect(lambda _: self.derived_changed.emit())
+ blk.id_renamed.connect(self.refresh_derived)
+ blk._drag_handle.set_drag_source(self, blk)
self._derived_blocks.append(blk)
return blk
@@ -654,6 +657,56 @@ class PipelineTab(QWidget):
self._virt_inner.removeWidget(blk); blk.deleteLater()
self.derived_changed.emit()
+ # ── Drag-to-reorder — see _DragReorderContainer in ui/windows/plot_window.py
+
+ def _index_of(self, block: DerivedBlock) -> int:
+ return self._derived_blocks.index(block)
+
+ def _drop_index(self, y: float) -> int:
+ """Insertion index for a drop at container-relative y."""
+ n = self._virt_inner.count() - 1 # exclude trailing stretch
+ for i in range(n):
+ w = self._virt_inner.itemAt(i).widget()
+ if y < w.y() + w.height() / 2:
+ return i
+ return n
+
+ def _indicator_y_for(self, idx: int) -> int:
+ """Container-relative y for the drop-line at insertion index idx."""
+ n = self._virt_inner.count() - 1 # exclude trailing stretch
+ spacing = self._virt_inner.spacing()
+ if n == 0:
+ return self._virt_inner.contentsMargins().top()
+ if idx <= 0:
+ w = self._virt_inner.itemAt(0).widget()
+ return w.y() - spacing // 2
+ if idx >= n:
+ w = self._virt_inner.itemAt(n - 1).widget()
+ return w.y() + w.height() + spacing // 2
+ prev = self._virt_inner.itemAt(idx - 1).widget()
+ nxt = self._virt_inner.itemAt(idx).widget()
+ return (prev.y() + prev.height() + nxt.y()) // 2
+
+ def _reorder(self, src_idx: int, dst_idx: int):
+ """Move the Channel at src_idx to dst_idx (drop-target index,
+ pre-removal) — same semantics as ControlPanel._reorder(). Also
+ reorders processor._derived so evaluation order stays matched to
+ the visual order: a Channel sourced from another Channel needs that
+ source evaluated first in the same pass (SignalProcessor.
+ _evaluate_derived), or it lags by one sample."""
+ if dst_idx > src_idx:
+ dst_idx -= 1
+ if dst_idx == src_idx:
+ return
+
+ blk = self._derived_blocks.pop(src_idx)
+ self._derived_blocks.insert(dst_idx, blk)
+ self._virt_inner.removeWidget(blk)
+ self._virt_inner.insertWidget(dst_idx, blk)
+
+ self.processor.reorder_derived(src_idx, dst_idx)
+ self.derived_changed.emit()
+
def refresh_derived(self):
"""Rebuild DerivedBlock widgets from processor state (called after profile load)."""
for blk in self._derived_blocks:
@@ -754,8 +807,10 @@ def _sub_xrefs(code: str, names: list) -> str:
class DerivedBlock(QFrame):
- removed = pyqtSignal(object)
- applied = pyqtSignal(object) # DerivedChannel
+ removed = pyqtSignal(object)
+ applied = pyqtSignal(object) # DerivedChannel
+ id_renamed = pyqtSignal() # this Channel's ID changed — other blocks'
+ # source pickers may now be stale
def __init__(self, dc: DerivedChannel, registry: DeviceRegistry,
processor: SignalProcessor):
@@ -764,6 +819,7 @@ class DerivedBlock(QFrame):
self.setObjectName("plotBlock")
self._src_combos: List[QComboBox] = []
self._expanded = False
+ self._orig_id = dc.channel_id
self._build()
def _build(self):
@@ -772,6 +828,8 @@ class DerivedBlock(QFrame):
# Header
hdr = QWidget(); hdr.setObjectName("plotBlockHeader"); hdr.setFixedHeight(32)
hl = QHBoxLayout(hdr); hl.setContentsMargins(8, 0, 6, 0)
+ self._drag_handle = _DragHandle()
+ hl.addWidget(self._drag_handle)
self._name = QLineEdit(self.dc.name); self._name.setObjectName("plotBlockTitle")
self._name.textChanged.connect(lambda t: setattr(self.dc, "name", t))
hl.addWidget(self._name, 1)
@@ -1133,6 +1191,14 @@ class DerivedBlock(QFrame):
self._st.setText(f"⚠ ID '{channel_id}' is already used by another channel")
self._st.setStyleSheet("color:#ef4444;")
return
+ renamed = channel_id != self._orig_id
+ if renamed:
+ # Other channels sourced from this one under the old ID would
+ # otherwise keep pointing at a reference that no longer exists —
+ # fix their sources up front, carry the ring buffer + latest
+ # value over so the rename doesn't reset their plotted history.
+ self.processor.rename_derived(self._orig_id, channel_id)
+ self._orig_id = channel_id
sources = [cb.currentData() for cb in self._src_combos if cb.currentData()]
if self.dc.kind in ("derivative", "second_derivative"):
wrt = "channel" if self._wrt_cb.currentIndex() == 1 else "time"
@@ -1149,6 +1215,11 @@ class DerivedBlock(QFrame):
elif self.dc.kind in ("function", "custom_script"):
self.dc.script = self._code.toPlainText()
err = self.processor.add_derived(self.dc)
+ # The rename above already committed regardless of whether this
+ # channel's own expression/script compiles — other blocks' source
+ # pickers need to refresh either way.
+ if renamed:
+ self.id_renamed.emit()
if err:
self._st.setText(f"⚠ {err[:100]}")
self._st.setStyleSheet("color:#ef4444;")