summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ui/windows/channels_window.py351
1 files changed, 220 insertions, 131 deletions
diff --git a/ui/windows/channels_window.py b/ui/windows/channels_window.py
index a90cff6..9a90670 100644
--- a/ui/windows/channels_window.py
+++ b/ui/windows/channels_window.py
@@ -3,16 +3,18 @@ ui/windows/channels_window.py
CHANNELS window — toolbar section 3.
-Layout (QSplitter, vertical):
+Layout (QSplitter, horizontal):
Physical Channels — per-channel filter pipeline; add/remove channels,
- configure moving average, low-pass, derivative, etc.
- Virtual Channels — create derived channels (derivative dy/dt or dy/dx,
- RMS, power, difference, sum, Python expression/script)
+ configure filters (moving average, low-pass, etc.),
+ math ops (derivative, integral), calibrate.
+ Virtual Channels — create derived channels (RMS, power, difference,
+ sum, Python expression/script, etc.)
Classes:
ChannelsWindow — top-level floating window
PipelineTab — splitter content (physical + virtual sections)
- ChannelPipelineBlock — collapsible filter stack for one physical channel
+ ChannelPipelineBlock — collapsible filter+math stack for one physical channel
+ ChannelPickerDialog — checkbox dialog for selecting channels to add
DerivedBlock — editor for one virtual/derived channel
FilterRow — single filter stage row inside a pipeline block
SignalsListTab — unused; kept for profile-load compatibility
@@ -23,9 +25,9 @@ from typing import List, Optional, Set, Tuple
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
- QScrollArea, QFrame, QTabWidget, QComboBox, QLineEdit,
+ QScrollArea, QFrame, QComboBox, QLineEdit,
QDoubleSpinBox, QSpinBox, QCheckBox, QTextEdit, QGroupBox,
- QToolButton, QSplitter,
+ QToolButton, QSplitter, QDialog, QDialogButtonBox,
)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QCloseEvent, QFont, QFontMetrics
@@ -41,6 +43,9 @@ _DERIVED_COLORS = [
"#f77f00","#d62828","#588157","#e9c46a",
]
+_FILTER_KEYS = [k for k in FILTER_CLASSES if k not in ("derivative", "integral")]
+_MATH_KEYS = ["derivative", "integral"]
+
def _channel_combo(registry: DeviceRegistry,
processor: Optional[SignalProcessor] = None,
@@ -65,6 +70,59 @@ def _channel_combo(registry: DeviceRegistry,
# ══════════════════════════════════════════════════════════════════════════════
+# Channel Picker Dialog
+# ══════════════════════════════════════════════════════════════════════════════
+
+class ChannelPickerDialog(QDialog):
+ def __init__(self, registry: DeviceRegistry,
+ already_shown: Set[Tuple[str, str]], parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("Add Channels")
+ self.setMinimumWidth(380)
+ self.resize(420, 320)
+ self._checks: List[Tuple[QCheckBox, str, str]] = []
+
+ lay = QVBoxLayout(self)
+
+ scroll = QScrollArea(); scroll.setWidgetResizable(True)
+ scroll.setObjectName("deviceScroll")
+ cont = QWidget()
+ cl = QVBoxLayout(cont); cl.setContentsMargins(8, 8, 8, 8); cl.setSpacing(4)
+
+ any_available = False
+ for dev in registry.all_instances():
+ for ch in dev.info.channels:
+ if not ch.enabled:
+ continue
+ if (dev.info.device_id, ch.channel_id) in already_shown:
+ continue
+ any_available = True
+ label = f"{dev.info.device_id} / {ch.channel_id} ({ch.name})"
+ if ch.unit:
+ label += f" [{ch.unit}]"
+ chk = QCheckBox(label)
+ self._checks.append((chk, dev.info.device_id, ch.channel_id))
+ cl.addWidget(chk)
+
+ if not any_available:
+ cl.addWidget(QLabel("No additional channels available."))
+
+ cl.addStretch()
+ scroll.setWidget(cont)
+ lay.addWidget(scroll, 1)
+
+ btns = QDialogButtonBox(
+ QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
+ )
+ btns.accepted.connect(self.accept)
+ btns.rejected.connect(self.reject)
+ lay.addWidget(btns)
+
+ def selected(self) -> List[Tuple[str, str]]:
+ return [(d, c) for chk, d, c in self._checks if chk.isChecked()]
+
+
+# ══════════════════════════════════════════════════════════════════════════════
# Filter row
# ══════════════════════════════════════════════════════════════════════════════
@@ -90,9 +148,9 @@ class FilterRow(QFrame):
self._build()
def _build(self):
- lay = QHBoxLayout(self); lay.setContentsMargins(6,4,6,4); lay.setSpacing(8)
+ lay = QHBoxLayout(self); lay.setContentsMargins(6, 4, 6, 4); lay.setSpacing(8)
- lbl = QLabel(self.filt.name.replace("_"," ").title())
+ lbl = QLabel(self.filt.name.replace("_", " ").title())
lbl.setObjectName("traceSource"); lbl.setMinimumWidth(120)
lay.addWidget(lbl)
@@ -131,12 +189,12 @@ _QLabel.also = _also
# ══════════════════════════════════════════════════════════════════════════════
-# Channel Pipeline Block — collapsible filter stack for one channel
+# Channel Pipeline Block — collapsible filter+math stack for one channel
# ══════════════════════════════════════════════════════════════════════════════
class ChannelPipelineBlock(QFrame):
changed = pyqtSignal()
- removed = pyqtSignal(object) # emits self
+ removed = pyqtSignal(object)
def __init__(self, dev_id: str, ch_id: str, ch_name: str, unit: str,
processor: SignalProcessor):
@@ -144,7 +202,8 @@ class ChannelPipelineBlock(QFrame):
self.dev_id = dev_id
self.ch_id = ch_id
self.processor = processor
- self._rows: List[FilterRow] = []
+ self._filter_rows: List[FilterRow] = []
+ self._math_rows: List[FilterRow] = []
self._cur_pipeline: Optional[ChannelPipeline] = None
self._expanded = False
self.setObjectName("plotBlock")
@@ -154,17 +213,22 @@ class ChannelPipelineBlock(QFrame):
def _build(self, ch_name: str, unit: str):
self._ch_name = ch_name
self._unit = unit
- outer = QVBoxLayout(self); outer.setContentsMargins(0,0,0,0); outer.setSpacing(0)
+ outer = QVBoxLayout(self); outer.setContentsMargins(0, 0, 0, 0); outer.setSpacing(0)
# Header
hdr = QWidget(); hdr.setObjectName("plotBlockHeader"); hdr.setFixedHeight(32)
- hl = QHBoxLayout(hdr); hl.setContentsMargins(8,0,6,0)
+ hl = QHBoxLayout(hdr); hl.setContentsMargins(8, 0, 6, 0)
title = f"{self.dev_id} / {self.ch_id} ({ch_name})"
if unit:
title += f" [{unit}]"
self._title_lbl = QLabel(title); self._title_lbl.setObjectName("traceSource")
hl.addWidget(self._title_lbl, 1)
+ cal_btn = QPushButton("⚖ Calibrate"); cal_btn.setObjectName("toolbarSectionBtn")
+ cal_btn.setFixedHeight(22)
+ cal_btn.clicked.connect(self._add_calibrate)
+ hl.addWidget(cal_btn)
+
rm_btn = QToolButton(); rm_btn.setText("✕")
rm_btn.setObjectName("traceRemoveBtn"); rm_btn.setFixedSize(22, 22)
rm_btn.clicked.connect(lambda: self.removed.emit(self))
@@ -180,25 +244,48 @@ class ChannelPipelineBlock(QFrame):
# Body
self._body = QWidget(); self._body.setObjectName("plotBlockSettings")
bl = QVBoxLayout(self._body)
- bl.setContentsMargins(10,8,10,10); bl.setSpacing(6)
+ bl.setContentsMargins(10, 8, 10, 10); bl.setSpacing(6)
- self._en_chk = QCheckBox("Pipeline enabled"); self._en_chk.setChecked(True)
- self._en_chk.toggled.connect(self._on_enable)
- bl.addWidget(self._en_chk)
+ # ── Filters section ───────────────────────────────────────────────────
+ filt_lbl = QLabel("Filters"); filt_lbl.setObjectName("pwmLabel")
+ bl.addWidget(filt_lbl)
self._filter_area = QWidget()
self._filter_lay = QVBoxLayout(self._filter_area)
- self._filter_lay.setContentsMargins(0,0,0,0); self._filter_lay.setSpacing(3)
+ self._filter_lay.setContentsMargins(0, 0, 0, 0); self._filter_lay.setSpacing(3)
bl.addWidget(self._filter_area)
- add_bar = QHBoxLayout()
+ filt_add_bar = QHBoxLayout()
self._filt_cb = QComboBox(); self._filt_cb.setObjectName("channelPickerCb")
- self._filt_cb.addItems([k.replace("_"," ").title() for k in FILTER_CLASSES])
- add_bar.addWidget(self._filt_cb, 1)
- add_btn = QPushButton("+ Add Filter"); add_btn.setObjectName("addTraceBtn")
- add_btn.clicked.connect(self._add_filter)
- add_bar.addWidget(add_btn)
- bl.addLayout(add_bar)
+ self._filt_cb.addItems([k.replace("_", " ").title() for k in _FILTER_KEYS])
+ filt_add_bar.addWidget(self._filt_cb, 1)
+ filt_add_btn = QPushButton("+ Add Filter"); filt_add_btn.setObjectName("addTraceBtn")
+ filt_add_btn.clicked.connect(self._add_filter)
+ filt_add_bar.addWidget(filt_add_btn)
+ bl.addLayout(filt_add_bar)
+
+ div = QFrame(); div.setFrameShape(QFrame.Shape.HLine)
+ div.setObjectName("devWindowDivider")
+ bl.addWidget(div)
+
+ # ── Math section ──────────────────────────────────────────────────────
+ math_lbl = QLabel("Math"); math_lbl.setObjectName("pwmLabel")
+ bl.addWidget(math_lbl)
+
+ self._math_area = QWidget()
+ self._math_lay = QVBoxLayout(self._math_area)
+ self._math_lay.setContentsMargins(0, 0, 0, 0); self._math_lay.setSpacing(3)
+ bl.addWidget(self._math_area)
+
+ math_add_bar = QHBoxLayout()
+ deriv_btn = QPushButton("+ Derivative"); deriv_btn.setObjectName("addTraceBtn")
+ deriv_btn.clicked.connect(lambda: self._add_math("derivative"))
+ integ_btn = QPushButton("+ Integral"); integ_btn.setObjectName("addTraceBtn")
+ integ_btn.clicked.connect(lambda: self._add_math("integral"))
+ math_add_bar.addWidget(deriv_btn)
+ math_add_bar.addWidget(integ_btn)
+ math_add_bar.addStretch()
+ bl.addLayout(math_add_bar)
ap_row = QHBoxLayout(); ap_row.addStretch()
ap_btn = QPushButton("✓ Apply"); ap_btn.setObjectName("applyButton")
@@ -234,39 +321,61 @@ class ChannelPipelineBlock(QFrame):
pipeline = ChannelPipeline(device_id=self.dev_id, channel_id=self.ch_id)
self.processor.set_pipeline(pipeline)
self._cur_pipeline = pipeline
- self._en_chk.setChecked(pipeline.enabled)
+ pipeline.enabled = True
for f in pipeline.filters:
- self._add_filter_row(f)
+ if f.name in _MATH_KEYS:
+ self._add_math_row(f)
+ else:
+ self._add_filter_row(f)
+
+ def _add_calibrate(self):
+ filt = FILTER_CLASSES["scale_offset"]()
+ self._add_filter_row(filt)
+ self._push()
+ if not self._expanded:
+ self._toggle()
def _add_filter(self):
- key = list(FILTER_CLASSES.keys())[self._filt_cb.currentIndex()]
+ key = _FILTER_KEYS[self._filt_cb.currentIndex()]
filt = FILTER_CLASSES[key]()
- if self._cur_pipeline:
- self._cur_pipeline.filters.append(filt)
self._add_filter_row(filt)
self._push()
+ def _add_math(self, key: str):
+ filt = FILTER_CLASSES[key]()
+ self._add_math_row(filt)
+ self._push()
+
def _add_filter_row(self, filt: FilterBase):
row = FilterRow(filt)
- row.removed.connect(self._remove_filter)
+ row.removed.connect(self._remove_row)
row.changed.connect(self._push)
- self._rows.append(row)
+ self._filter_rows.append(row)
self._filter_lay.addWidget(row)
- def _remove_filter(self, row: FilterRow):
- if self._cur_pipeline and row.filt in self._cur_pipeline.filters:
- self._cur_pipeline.filters.remove(row.filt)
- self._rows.remove(row)
- self._filter_lay.removeWidget(row); row.deleteLater()
- self._push()
+ def _add_math_row(self, filt: FilterBase):
+ row = FilterRow(filt)
+ row.removed.connect(self._remove_row)
+ row.changed.connect(self._push)
+ self._math_rows.append(row)
+ self._math_lay.addWidget(row)
- def _on_enable(self, v: bool):
- if self._cur_pipeline:
- self._cur_pipeline.enabled = v
- self._push()
+ def _remove_row(self, row: FilterRow):
+ if row in self._filter_rows:
+ self._filter_rows.remove(row)
+ self._filter_lay.removeWidget(row)
+ else:
+ self._math_rows.remove(row)
+ self._math_lay.removeWidget(row)
+ row.deleteLater()
+ self._push()
def _push(self):
if self._cur_pipeline:
+ self._cur_pipeline.filters = (
+ [r.filt for r in self._filter_rows] +
+ [r.filt for r in self._math_rows]
+ )
self.processor.set_pipeline(self._cur_pipeline)
self.changed.emit()
@@ -283,29 +392,27 @@ class PipelineTab(QWidget):
super().__init__()
self.registry = registry
self.processor = processor
- self._shown: Set[Tuple[str,str]] = set()
- self._blocks: dict[Tuple[str,str], ChannelPipelineBlock] = {}
+ self._shown: Set[Tuple[str, str]] = set()
+ self._blocks: dict[Tuple[str, str], ChannelPipelineBlock] = {}
self._derived_blocks: List[DerivedBlock] = []
self._color_idx = 0
self._build()
def _build(self):
- root = QVBoxLayout(self); root.setContentsMargins(0,0,0,0); root.setSpacing(0)
+ root = QVBoxLayout(self); root.setContentsMargins(0, 0, 0, 0); root.setSpacing(0)
- splitter = QSplitter(Qt.Orientation.Vertical)
+ splitter = QSplitter(Qt.Orientation.Horizontal)
splitter.setObjectName("channelSplitter")
root.addWidget(splitter)
- # ── Physical Channels pane ──────────────────────────────────────────────
+ # ── Physical Channels pane ─────────────────────────────────────────────
phys_w = QWidget()
- phys_l = QVBoxLayout(phys_w); phys_l.setContentsMargins(0,0,0,0); phys_l.setSpacing(0)
+ phys_l = QVBoxLayout(phys_w); phys_l.setContentsMargins(0, 0, 0, 0); phys_l.setSpacing(0)
phys_bar = QWidget(); phys_bar.setObjectName("cfgGlobalBar")
- pb_lay = QHBoxLayout(phys_bar); pb_lay.setContentsMargins(10,7,10,7); pb_lay.setSpacing(6)
+ pb_lay = QHBoxLayout(phys_bar); pb_lay.setContentsMargins(10, 7, 10, 7); pb_lay.setSpacing(6)
pb_lbl = QLabel("Physical Channels"); pb_lbl.setObjectName("devWindowTitle")
pb_lay.addWidget(pb_lbl, 1)
- self._avail_cb = QComboBox(); self._avail_cb.setObjectName("channelPickerCb")
- pb_lay.addWidget(self._avail_cb)
add_btn = QPushButton("+ Add"); add_btn.setObjectName("addTraceBtn")
add_btn.clicked.connect(self._on_add_channel)
pb_lay.addWidget(add_btn)
@@ -319,23 +426,20 @@ class PipelineTab(QWidget):
phys_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self._phys_cont = QWidget()
self._phys_inner = QVBoxLayout(self._phys_cont)
- self._phys_inner.setContentsMargins(10,10,10,10); self._phys_inner.setSpacing(6)
+ self._phys_inner.setContentsMargins(10, 10, 10, 10); self._phys_inner.setSpacing(6)
self._phys_inner.addStretch()
phys_scroll.setWidget(self._phys_cont)
phys_l.addWidget(phys_scroll, 1)
splitter.addWidget(phys_w)
- # ── Virtual Channels pane ───────────────────────────────────────────────
+ # ── Virtual Channels pane ──────────────────────────────────────────────
virt_w = QWidget()
- virt_l = QVBoxLayout(virt_w); virt_l.setContentsMargins(0,0,0,0); virt_l.setSpacing(0)
+ virt_l = QVBoxLayout(virt_w); virt_l.setContentsMargins(0, 0, 0, 0); virt_l.setSpacing(0)
virt_bar = QWidget(); virt_bar.setObjectName("cfgGlobalBar")
- vb_lay = QHBoxLayout(virt_bar); vb_lay.setContentsMargins(10,7,10,7); vb_lay.setSpacing(6)
+ vb_lay = QHBoxLayout(virt_bar); vb_lay.setContentsMargins(10, 7, 10, 7); vb_lay.setSpacing(6)
vb_lbl = QLabel("Virtual Channels"); vb_lbl.setObjectName("devWindowTitle")
vb_lay.addWidget(vb_lbl, 1)
- self._new_kind = QComboBox(); self._new_kind.setObjectName("channelPickerCb")
- self._new_kind.addItems([_KIND_DISPLAY[k] for k in _ALL_KINDS])
- vb_lay.addWidget(self._new_kind)
add_virt = QPushButton("+ Add Virtual"); add_virt.setObjectName("addTraceBtn")
add_virt.clicked.connect(self._add_virtual)
vb_lay.addWidget(add_virt)
@@ -349,22 +453,20 @@ class PipelineTab(QWidget):
virt_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self._virt_cont = QWidget()
self._virt_inner = QVBoxLayout(self._virt_cont)
- self._virt_inner.setContentsMargins(10,10,10,10); self._virt_inner.setSpacing(6)
+ self._virt_inner.setContentsMargins(10, 10, 10, 10); self._virt_inner.setSpacing(6)
self._virt_inner.addStretch()
virt_scroll.setWidget(self._virt_cont)
virt_l.addWidget(virt_scroll, 1)
splitter.addWidget(virt_w)
- splitter.setSizes([300, 300])
+ splitter.setSizes([500, 500])
for dev in self.registry.all_instances():
for ch in dev.info.channels:
if ch.enabled:
self._add_block(dev.info.device_id, ch.channel_id, ch.name, ch.unit)
- self._refresh_avail_cb()
-
- # ── Physical channel block management ─────────────────────────────────────
+ # ── Physical channel block management ──────────────────────────────────────
def _add_block(self, dev_id: str, ch_id: str, ch_name: str, unit: str):
if (dev_id, ch_id) in self._shown:
@@ -393,7 +495,6 @@ class PipelineTab(QWidget):
block = self._blocks.get(key)
if block:
block.setVisible(False)
- self._refresh_avail_cb()
def on_device_removed(self, dev_id: str):
keys = [(d, c) for (d, c) in list(self._shown) if d == dev_id]
@@ -401,7 +502,6 @@ class PipelineTab(QWidget):
block = self._blocks.get(key)
if block:
self._on_remove_block(block)
- self._refresh_avail_cb()
def on_device_reconfigured(self, dev_id: str):
dev = self.registry.get_instance(dev_id)
@@ -415,10 +515,9 @@ class PipelineTab(QWidget):
block = self._blocks.get(key)
if block:
self._on_remove_block(block)
- self._refresh_avail_cb()
def on_device_added(self):
- self._refresh_avail_cb()
+ pass
def on_channel_name_changed(self, dev_id: str, ch_id: str, ch_name: str):
key = (dev_id, ch_id)
@@ -430,7 +529,6 @@ class PipelineTab(QWidget):
ch = dev.get_channel(ch_id)
if ch and ch.enabled:
self._add_block(dev_id, ch_id, ch_name, ch.unit)
- self._refresh_avail_cb()
def on_channel_unit_changed(self, dev_id: str, ch_id: str, unit: str):
key = (dev_id, ch_id)
@@ -438,18 +536,17 @@ class PipelineTab(QWidget):
self._blocks[key].update_unit(unit)
def _on_add_channel(self):
- data = self._avail_cb.currentData()
- if not data:
+ dlg = ChannelPickerDialog(self.registry, self._shown, self)
+ if dlg.exec() != QDialog.DialogCode.Accepted:
return
- dev_id, ch_id = data
- dev = self.registry.get_instance(dev_id)
- if not dev:
- return
- ch = dev.get_channel(ch_id)
- if not ch:
- return
- self._add_block(dev_id, ch_id, ch.name, ch.unit)
- self._refresh_avail_cb()
+ for dev_id, ch_id in dlg.selected():
+ dev = self.registry.get_instance(dev_id)
+ if not dev:
+ continue
+ ch = dev.get_channel(ch_id)
+ if not ch:
+ continue
+ self._add_block(dev_id, ch_id, ch.name, ch.unit)
def _on_remove_block(self, block: ChannelPipelineBlock):
key = (block.dev_id, block.ch_id)
@@ -457,30 +554,15 @@ class PipelineTab(QWidget):
self._blocks.pop(key, None)
self._phys_inner.removeWidget(block)
block.deleteLater()
- self._refresh_avail_cb()
- def _refresh_avail_cb(self):
- self._avail_cb.clear()
- for dev in self.registry.all_instances():
- for ch in dev.info.channels:
- if not ch.enabled:
- continue
- if (dev.info.device_id, ch.channel_id) not in self._shown:
- label = f"{dev.info.device_id} / {ch.channel_id} ({ch.name})"
- if ch.unit:
- label += f" [{ch.unit}]"
- self._avail_cb.addItem(label,
- userData=(dev.info.device_id, ch.channel_id))
-
- # ── Virtual channel management ────────────────────────────────────────────
+ # ── Virtual channel management ─────────────────────────────────────────────
def _add_virtual(self):
- kind = _ALL_KINDS[self._new_kind.currentIndex()]
color = _DERIVED_COLORS[self._color_idx % len(_DERIVED_COLORS)]
self._color_idx += 1
n = len(self._derived_blocks) + 1
dc = DerivedChannel(channel_id=f"CH_V{n}", name=f"Virtual Channel {n}",
- kind=kind, color=color)
+ kind="expression", color=color)
blk = self._make_derived_block(dc)
self._virt_inner.insertWidget(self._virt_inner.count() - 1, blk)
@@ -544,41 +626,43 @@ class DerivedBlock(QFrame):
self.dc = dc; self.registry = registry; self.processor = processor
self.setObjectName("plotBlock")
self._src_combos: List[QComboBox] = []
+ self._expanded = False
self._build()
def _build(self):
- outer = QVBoxLayout(self); outer.setContentsMargins(0,0,0,0); outer.setSpacing(0)
+ outer = QVBoxLayout(self); outer.setContentsMargins(0, 0, 0, 0); outer.setSpacing(0)
# Header
hdr = QWidget(); hdr.setObjectName("plotBlockHeader"); hdr.setFixedHeight(32)
- hl = QHBoxLayout(hdr); hl.setContentsMargins(8,0,6,0)
+ hl = QHBoxLayout(hdr); hl.setContentsMargins(8, 0, 6, 0)
self._name = QLineEdit(self.dc.name); self._name.setObjectName("plotBlockTitle")
- self._name.textChanged.connect(lambda t: setattr(self.dc,"name",t))
+ self._name.textChanged.connect(lambda t: setattr(self.dc, "name", t))
hl.addWidget(self._name, 1)
- self._en = QCheckBox(); self._en.setChecked(self.dc.enabled)
- self._en.setToolTip("Enable")
- self._en.toggled.connect(lambda v: setattr(self.dc,"enabled",v))
- hl.addWidget(self._en)
rm = QToolButton(); rm.setText("✕"); rm.setObjectName("plotRemoveBtn")
- rm.setFixedSize(22,22); rm.clicked.connect(lambda: self.removed.emit(self))
+ rm.setFixedSize(22, 22); rm.clicked.connect(lambda: self.removed.emit(self))
hl.addWidget(rm)
+ self._toggle_btn = QToolButton(); self._toggle_btn.setText("▶")
+ self._toggle_btn.setObjectName("traceRemoveBtn"); self._toggle_btn.setFixedSize(22, 22)
+ self._toggle_btn.clicked.connect(self._toggle)
+ hl.addWidget(self._toggle_btn)
outer.addWidget(hdr)
+ hdr.mousePressEvent = lambda e: self._toggle()
# Body
body = QWidget(); body.setObjectName("plotBlockSettings")
- bl = QVBoxLayout(body); bl.setContentsMargins(10,8,10,10); bl.setSpacing(6)
+ bl = QVBoxLayout(body); bl.setContentsMargins(10, 8, 10, 10); bl.setSpacing(6)
# ID / unit row
meta = QHBoxLayout()
meta.addWidget(QLabel("ID:"))
self._id = QLineEdit(self.dc.channel_id)
self._id.setObjectName("traceLabel"); self._id.setFixedWidth(100)
- self._id.textChanged.connect(lambda t: setattr(self.dc,"channel_id",t))
+ self._id.textChanged.connect(lambda t: setattr(self.dc, "channel_id", t))
meta.addWidget(self._id)
meta.addWidget(QLabel("Unit:"))
self._unit = QLineEdit(self.dc.unit)
self._unit.setObjectName("traceLabel"); self._unit.setFixedWidth(60)
- self._unit.textChanged.connect(lambda t: setattr(self.dc,"unit",t))
+ self._unit.textChanged.connect(lambda t: setattr(self.dc, "unit", t))
meta.addWidget(self._unit); meta.addStretch()
bl.addLayout(meta)
@@ -591,29 +675,28 @@ class DerivedBlock(QFrame):
self._kind_cb.currentIndexChanged.connect(self._on_kind)
k_row.addWidget(self._kind_cb); k_row.addStretch()
bl.addLayout(k_row)
- self._help = QLabel(_KIND_HELP.get(self.dc.kind,""))
+ self._help = QLabel(_KIND_HELP.get(self.dc.kind, ""))
self._help.setObjectName("traceSource"); self._help.setWordWrap(True)
bl.addWidget(self._help)
# RMS window
self._win_row = QHBoxLayout()
self._win_row.addWidget(QLabel("Window samples:"))
- self._win_sp = QSpinBox(); self._win_sp.setRange(2,10000)
- self._win_sp.setValue(self.dc.params.get("window",20))
+ self._win_sp = QSpinBox(); self._win_sp.setRange(2, 10000)
+ self._win_sp.setValue(self.dc.params.get("window", 20))
self._win_sp.setObjectName("traceWidthSpin"); self._win_sp.setFixedWidth(80)
- self._win_sp.valueChanged.connect(lambda v: self.dc.params.update({"window":v}))
+ self._win_sp.valueChanged.connect(lambda v: self.dc.params.update({"window": v}))
self._win_row.addWidget(self._win_sp); self._win_row.addStretch()
self._win_widget = QWidget(); self._win_widget.setLayout(self._win_row)
bl.addWidget(self._win_widget)
# Sources — include derived channels as valid sources
src_grp = QGroupBox("Sources"); src_grp.setObjectName("cfgGlobalBar")
- src_lay = QVBoxLayout(src_grp); src_lay.setContentsMargins(6,4,6,4); src_lay.setSpacing(3)
+ src_lay = QVBoxLayout(src_grp); src_lay.setContentsMargins(6, 4, 6, 4); src_lay.setSpacing(3)
self._src_cont = QWidget(); self._src_vlay = QVBoxLayout(self._src_cont)
- self._src_vlay.setContentsMargins(0,0,0,0); self._src_vlay.setSpacing(3)
+ self._src_vlay.setContentsMargins(0, 0, 0, 0); self._src_vlay.setSpacing(3)
_is_deriv = self.dc.kind in ("derivative", "second_derivative")
if _is_deriv:
- # Always show exactly one y-source row
self._add_src(self.dc.sources[0] if self.dc.sources else None)
else:
for s in self.dc.sources:
@@ -625,7 +708,7 @@ class DerivedBlock(QFrame):
# W.r.t. — only for derivative/second_derivative
self._wrt_grp = QGroupBox("With Respect To"); self._wrt_grp.setObjectName("cfgGlobalBar")
- wrt_lay = QVBoxLayout(self._wrt_grp); wrt_lay.setContentsMargins(6,6,6,6); wrt_lay.setSpacing(4)
+ wrt_lay = QVBoxLayout(self._wrt_grp); wrt_lay.setContentsMargins(6, 6, 6, 6); wrt_lay.setSpacing(4)
wrt_top = QHBoxLayout(); wrt_top.addWidget(QLabel("Variable:"))
self._wrt_cb = QComboBox(); self._wrt_cb.setObjectName("channelPickerCb")
self._wrt_cb.addItems(["Time", "Channel"])
@@ -635,7 +718,7 @@ class DerivedBlock(QFrame):
wrt_top.addWidget(self._wrt_cb); wrt_top.addStretch()
wrt_lay.addLayout(wrt_top)
self._wrt_x_row = QWidget()
- wx_lay = QHBoxLayout(self._wrt_x_row); wx_lay.setContentsMargins(0,0,0,0); wx_lay.setSpacing(6)
+ wx_lay = QHBoxLayout(self._wrt_x_row); wx_lay.setContentsMargins(0, 0, 0, 0); wx_lay.setSpacing(6)
wx_lay.addWidget(QLabel("Channel (x):"))
self._wrt_x_cb = _channel_combo(self.registry, self.processor,
include_derived=True, show_unit=True)
@@ -651,13 +734,13 @@ class DerivedBlock(QFrame):
# Code editor
self._code_grp = QGroupBox("Code")
- code_lay = QVBoxLayout(self._code_grp); code_lay.setContentsMargins(6,4,6,4)
+ code_lay = QVBoxLayout(self._code_grp); code_lay.setContentsMargins(6, 4, 6, 4)
self._code = QTextEdit(); self._code.setObjectName("codeEditor")
self._code.setMinimumHeight(90); self._code.setMaximumHeight(180)
_mono = QFont("IBM Plex Mono, Consolas, Monospace"); _mono.setStyleHint(QFont.StyleHint.Monospace)
self._code.setFont(_mono)
self._code.setTabStopDistance(QFontMetrics(_mono).horizontalAdvance(" ") * 4)
- txt = self.dc.expression if self.dc.kind=="expression" else self.dc.script
+ txt = self.dc.expression if self.dc.kind == "expression" else self.dc.script
self._code.setPlainText(txt)
code_lay.addWidget(self._code)
bl.addWidget(self._code_grp)
@@ -671,12 +754,18 @@ class DerivedBlock(QFrame):
ap_row.addWidget(ap)
bl.addLayout(ap_row)
+ body.setVisible(False)
outer.addWidget(body)
+ self._body = body
self._update_visibility()
+ def _toggle(self):
+ self._expanded = not self._expanded
+ self._body.setVisible(self._expanded)
+ self._toggle_btn.setText("▼" if self._expanded else "▶")
+
def _add_src(self, src=None):
row = QHBoxLayout()
- # Include derived channels as valid sources (enables chaining)
cb = _channel_combo(self.registry, self.processor,
include_derived=True, show_unit=True)
cb.setObjectName("channelPickerCb")
@@ -685,8 +774,8 @@ class DerivedBlock(QFrame):
if cb.itemData(i) == src:
cb.setCurrentIndex(i); break
rm = QToolButton(); rm.setText("✕"); rm.setObjectName("traceRemoveBtn")
- rm.setFixedSize(22,22); rm.clicked.connect(lambda: self._rm_src(row, cb))
- row.addWidget(cb,1); row.addWidget(rm)
+ rm.setFixedSize(22, 22); rm.clicked.connect(lambda: self._rm_src(row, cb))
+ row.addWidget(cb, 1); row.addWidget(rm)
self._src_vlay.addLayout(row)
self._src_combos.append(cb)
@@ -700,7 +789,7 @@ class DerivedBlock(QFrame):
def _on_kind(self, idx: int):
k = _ALL_KINDS[idx]
self.dc.kind = k
- self._help.setText(_KIND_HELP.get(k,""))
+ self._help.setText(_KIND_HELP.get(k, ""))
if k in ("derivative", "second_derivative") and not self._src_combos:
self._add_src()
self._update_visibility()
@@ -729,7 +818,7 @@ class DerivedBlock(QFrame):
self.dc.sources = sources
if self.dc.kind == "expression":
self.dc.expression = self._code.toPlainText().strip()
- elif self.dc.kind in ("function","custom_script"):
+ elif self.dc.kind in ("function", "custom_script"):
self.dc.script = self._code.toPlainText()
err = self.processor.add_derived(self.dc)
if err:
@@ -761,9 +850,9 @@ class SignalsListTab(QWidget):
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self._cont = QWidget()
self._lay = QVBoxLayout(self._cont)
- self._lay.setContentsMargins(8,8,8,8); self._lay.setSpacing(2)
+ self._lay.setContentsMargins(8, 8, 8, 8); self._lay.setSpacing(2)
scroll.setWidget(self._cont)
- root = QVBoxLayout(self); root.setContentsMargins(0,0,0,0)
+ root = QVBoxLayout(self); root.setContentsMargins(0, 0, 0, 0)
root.addWidget(scroll)
self.refresh()
@@ -810,7 +899,7 @@ class SignalsListTab(QWidget):
def _make_row(self, color: str, name: str, unit: str, source: str,
enabled: bool, on_toggle) -> QFrame:
row = QFrame(); row.setObjectName("traceRow")
- lay = QHBoxLayout(row); lay.setContentsMargins(8,4,8,4); lay.setSpacing(8)
+ lay = QHBoxLayout(row); lay.setContentsMargins(8, 4, 8, 4); lay.setSpacing(8)
dot = QLabel("●"); dot.setStyleSheet(f"color:{color};"); dot.setFixedWidth(14)
lay.addWidget(dot)
@@ -849,15 +938,15 @@ class ChannelsWindow(QWidget):
self.processor = processor
self.setWindowTitle("Channels")
- self.setMinimumSize(640, 520)
- self.resize(740, 640)
+ self.setMinimumSize(800, 520)
+ self.resize(1400, 820)
self._build()
def _build(self):
- root = QVBoxLayout(self); root.setContentsMargins(0,0,0,0); root.setSpacing(0)
+ root = QVBoxLayout(self); root.setContentsMargins(0, 0, 0, 0); root.setSpacing(0)
hdr = QWidget(); hdr.setObjectName("devWindowTitleBar"); hdr.setFixedHeight(44)
- hl = QHBoxLayout(hdr); hl.setContentsMargins(14,0,14,0)
+ hl = QHBoxLayout(hdr); hl.setContentsMargins(14, 0, 14, 0)
hl.addWidget(QLabel("CHANNELS").also(lambda w: w.setObjectName("devWindowTitle")), 1)
root.addWidget(hdr)
div = QFrame(); div.setFrameShape(QFrame.Shape.HLine)