summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/windows/signals_window.py156
1 files changed, 113 insertions, 43 deletions
diff --git a/ui/windows/signals_window.py b/ui/windows/signals_window.py
index eb146f9..fbe0ec4 100644
--- a/ui/windows/signals_window.py
+++ b/ui/windows/signals_window.py
@@ -16,7 +16,7 @@ from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QScrollArea, QFrame, QTabWidget, QComboBox, QLineEdit,
QDoubleSpinBox, QSpinBox, QCheckBox, QTextEdit, QGroupBox,
- QToolButton,
+ QToolButton, QSplitter,
)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QCloseEvent
@@ -281,43 +281,72 @@ class PipelineTab(QWidget):
self._build()
def _build(self):
- lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.setSpacing(0)
+ root = QVBoxLayout(self); root.setContentsMargins(0,0,0,0); root.setSpacing(0)
- # Top bar: add physical channel + add virtual channel
- add_bar = QWidget(); add_bar.setObjectName("cfgGlobalBar")
- ab_lay = QHBoxLayout(add_bar); ab_lay.setContentsMargins(10,7,10,7); ab_lay.setSpacing(6)
- ab_lay.addWidget(QLabel("Add:"))
- self._avail_cb = QComboBox(); self._avail_cb.setObjectName("channelPickerCb")
- ab_lay.addWidget(self._avail_cb, 1)
- add_btn = QPushButton("+ Add Channel"); add_btn.setObjectName("addTraceBtn")
- add_btn.clicked.connect(self._on_add_channel)
- ab_lay.addWidget(add_btn)
+ splitter = QSplitter(Qt.Orientation.Vertical)
+ splitter.setObjectName("channelSplitter")
+ root.addWidget(splitter)
- sep = QFrame(); sep.setFrameShape(QFrame.Shape.VLine)
- sep.setObjectName("toolbarSep"); sep.setFixedWidth(6)
- ab_lay.addWidget(sep)
+ # ── Physical Channels pane ──────────────────────────────────────────────
+ phys_w = QWidget()
+ phys_l = QVBoxLayout(phys_w); phys_l.setContentsMargins(0,0,0,0); phys_l.setSpacing(0)
- ab_lay.addWidget(QLabel("Virtual:"))
+ phys_bar = QWidget(); phys_bar.setObjectName("cfgGlobalBar")
+ 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)
+ phys_l.addWidget(phys_bar)
+
+ div1 = QFrame(); div1.setFrameShape(QFrame.Shape.HLine)
+ div1.setObjectName("devWindowDivider"); phys_l.addWidget(div1)
+
+ phys_scroll = QScrollArea(); phys_scroll.setWidgetResizable(True)
+ phys_scroll.setObjectName("deviceScroll")
+ 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.addStretch()
+ phys_scroll.setWidget(self._phys_cont)
+ phys_l.addWidget(phys_scroll, 1)
+ splitter.addWidget(phys_w)
+
+ # ── Virtual Channels pane ───────────────────────────────────────────────
+ virt_w = QWidget()
+ 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_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])
- ab_lay.addWidget(self._new_kind)
+ vb_lay.addWidget(self._new_kind)
add_virt = QPushButton("+ Add Virtual"); add_virt.setObjectName("addTraceBtn")
add_virt.clicked.connect(self._add_virtual)
- ab_lay.addWidget(add_virt)
- lay.addWidget(add_bar)
+ vb_lay.addWidget(add_virt)
+ virt_l.addWidget(virt_bar)
- div = QFrame(); div.setFrameShape(QFrame.Shape.HLine)
- div.setObjectName("devWindowDivider"); lay.addWidget(div)
+ div2 = QFrame(); div2.setFrameShape(QFrame.Shape.HLine)
+ div2.setObjectName("devWindowDivider"); virt_l.addWidget(div2)
- scroll = QScrollArea(); scroll.setWidgetResizable(True)
- scroll.setObjectName("deviceScroll")
- scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
- self._cont = QWidget()
- self._inner = QVBoxLayout(self._cont)
- self._inner.setContentsMargins(10,10,10,10); self._inner.setSpacing(6)
- self._inner.addStretch()
- scroll.setWidget(self._cont)
- lay.addWidget(scroll, 1)
+ virt_scroll = QScrollArea(); virt_scroll.setWidgetResizable(True)
+ virt_scroll.setObjectName("deviceScroll")
+ 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.addStretch()
+ virt_scroll.setWidget(self._virt_cont)
+ virt_l.addWidget(virt_scroll, 1)
+ splitter.addWidget(virt_w)
+
+ splitter.setSizes([300, 300])
for dev in self.registry.all_instances():
for ch in dev.info.channels:
@@ -336,9 +365,7 @@ class PipelineTab(QWidget):
block.changed.connect(self.changed)
block.removed.connect(self._on_remove_block)
self._blocks[(dev_id, ch_id)] = block
- # Insert before derived blocks and trailing stretch
- insert_at = self._inner.count() - 1 - len(self._derived_blocks)
- self._inner.insertWidget(insert_at, block)
+ self._phys_inner.insertWidget(self._phys_inner.count() - 1, block)
def on_channel_enabled_changed(self, dev_id: str, ch_id: str, enabled: bool):
key = (dev_id, ch_id)
@@ -419,7 +446,7 @@ class PipelineTab(QWidget):
key = (block.dev_id, block.ch_id)
self._shown.discard(key)
self._blocks.pop(key, None)
- self._inner.removeWidget(block)
+ self._phys_inner.removeWidget(block)
block.deleteLater()
self._refresh_avail_cb()
@@ -446,7 +473,7 @@ class PipelineTab(QWidget):
dc = DerivedChannel(channel_id=f"sig_{n}", name=f"Signal {n}",
kind=kind, color=color)
blk = self._make_derived_block(dc)
- self._inner.insertWidget(self._inner.count() - 1, blk)
+ self._virt_inner.insertWidget(self._virt_inner.count() - 1, blk)
def _make_derived_block(self, dc: DerivedChannel) -> DerivedBlock:
blk = DerivedBlock(dc, self.registry, self.processor)
@@ -458,19 +485,19 @@ class PipelineTab(QWidget):
def _rm_virtual(self, blk: DerivedBlock):
self.processor.remove_derived(blk.dc.channel_id)
self._derived_blocks.remove(blk)
- self._inner.removeWidget(blk); blk.deleteLater()
+ self._virt_inner.removeWidget(blk); blk.deleteLater()
self.derived_changed.emit()
def refresh_derived(self):
"""Rebuild DerivedBlock widgets from processor state (called after profile load)."""
for blk in self._derived_blocks:
- self._inner.removeWidget(blk); blk.deleteLater()
+ self._virt_inner.removeWidget(blk); blk.deleteLater()
self._derived_blocks.clear()
self._color_idx = 0
for dc in self.processor.get_derived():
blk = self._make_derived_block(dc)
- self._inner.insertWidget(self._inner.count() - 1, blk)
+ self._virt_inner.insertWidget(self._virt_inner.count() - 1, blk)
self._color_idx = len(self._derived_blocks)
@@ -575,13 +602,41 @@ class DerivedBlock(QFrame):
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)
- for s in self.dc.sources:
+ _is_deriv = self.dc.kind in ("derivative", "second_derivative")
+ src_init = [self.dc.sources[0]] if (_is_deriv and self.dc.sources) else self.dc.sources
+ for s in src_init:
self._add_src(s)
- add_s = QPushButton("+ Add Source"); add_s.setObjectName("addTraceBtn")
- add_s.clicked.connect(lambda: self._add_src())
- src_lay.addWidget(self._src_cont); src_lay.addWidget(add_s)
+ self._add_src_btn = QPushButton("+ Add Source"); self._add_src_btn.setObjectName("addTraceBtn")
+ self._add_src_btn.clicked.connect(lambda: self._add_src())
+ src_lay.addWidget(self._src_cont); src_lay.addWidget(self._add_src_btn)
bl.addWidget(src_grp)
+ # 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_top = QHBoxLayout(); wrt_top.addWidget(QLabel("Variable:"))
+ self._wrt_cb = QComboBox(); self._wrt_cb.setObjectName("channelPickerCb")
+ self._wrt_cb.addItems(["Time", "Channel"])
+ _cur_wrt = self.dc.params.get("deriv_wrt", "time")
+ self._wrt_cb.setCurrentIndex(1 if _cur_wrt == "channel" else 0)
+ self._wrt_cb.currentIndexChanged.connect(self._on_wrt_changed)
+ 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.addWidget(QLabel("Channel (x):"))
+ self._wrt_x_cb = _channel_combo(self.registry, self.processor,
+ include_derived=True, show_unit=True)
+ self._wrt_x_cb.setObjectName("channelPickerCb")
+ if _cur_wrt == "channel" and len(self.dc.sources) > 1:
+ x_src = self.dc.sources[1]
+ for i in range(self._wrt_x_cb.count()):
+ if self._wrt_x_cb.itemData(i) == x_src:
+ self._wrt_x_cb.setCurrentIndex(i); break
+ wx_lay.addWidget(self._wrt_x_cb, 1)
+ wrt_lay.addWidget(self._wrt_x_row)
+ bl.addWidget(self._wrt_grp)
+
# Code editor
self._code_grp = QGroupBox("Code")
code_lay = QVBoxLayout(self._code_grp); code_lay.setContentsMargins(6,4,6,4)
@@ -633,13 +688,28 @@ class DerivedBlock(QFrame):
self._help.setText(_KIND_HELP.get(k,""))
self._update_visibility()
+ def _on_wrt_changed(self, idx: int):
+ self._wrt_x_row.setVisible(idx == 1)
+
def _update_visibility(self):
+ is_deriv = self.dc.kind in ("derivative", "second_derivative")
self._code_grp.setVisible(self.dc.kind in _SCRIPT)
self._win_widget.setVisible(self.dc.kind == "rms")
+ self._wrt_grp.setVisible(is_deriv)
+ self._add_src_btn.setVisible(not is_deriv)
+ if is_deriv:
+ self._wrt_x_row.setVisible(self._wrt_cb.currentIndex() == 1)
def _apply(self):
- self.dc.sources = [cb.currentData() for cb in self._src_combos
- if cb.currentData()]
+ 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"
+ self.dc.params["deriv_wrt"] = wrt
+ if wrt == "channel":
+ x_src = self._wrt_x_cb.currentData()
+ if x_src:
+ sources.append(x_src)
+ self.dc.sources = sources
if self.dc.kind == "expression":
self.dc.expression = self._code.toPlainText().strip()
elif self.dc.kind in ("function","custom_script"):