diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-06-03 14:33:36 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-06-03 14:33:36 -0600 |
| commit | 00be36b73e68aa851328b43391afd060818d7f4b (patch) | |
| tree | d6af17f73b711bc296084d696e47fa10643bb7c0 /ui/windows/channels_window.py | |
| parent | 4274ab7262219c13fdf76935f28951b39ee6c988 (diff) | |
Redesign Channels window: side-by-side layout, picker dialog, collapsible virtual blocks
Physical channels panel: horizontal splitter (left/right), channel picker
dialog replaces dropdown, Filters and Math sections per block, Calibrate
button, pipeline-enabled checkbox removed. Virtual channels: collapsible
blocks, enable checkbox removed, kind dropdown removed (defaults to
expression). Window resized to 1400×820.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'ui/windows/channels_window.py')
| -rw-r--r-- | ui/windows/channels_window.py | 351 |
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) |
