summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ui/main_window.py55
-rw-r--r--ui/windows/devices_window.py15
-rw-r--r--ui/windows/signals_window.py289
3 files changed, 208 insertions, 151 deletions
diff --git a/ui/main_window.py b/ui/main_window.py
index abfd585..45e6c95 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -39,7 +39,7 @@ from ui.control_panel import ControlPanel
from ui.strip_chart import StripChartWidget
from ui.profile_manager_ui import ProfileButton
from ui.windows.devices_window import DevicesWindow
-from ui.windows.signals_window import SignalsWindow
+from ui.windows.signals_window import ChannelsWindow
from ui.windows.plot_window import PlotWindow, build_default_layout
from ui.windows.settings_window import SettingsWindow
@@ -64,7 +64,7 @@ class MainWindow(QMainWindow):
self._elapsed = 0
self._win_devices = None
- self._win_signals = None
+ self._win_channels = None
self._win_plot = None
self._win_settings = None
@@ -138,10 +138,10 @@ class MainWindow(QMainWindow):
dev_btn.clicked.connect(lambda c: self._toggle_win("devices", c, dev_btn))
tb.addWidget(dev_btn); self._btn_devices = dev_btn
- sig_btn = QPushButton("⚗ Signals")
+ sig_btn = QPushButton("⚗ Channels")
sig_btn.setObjectName("toolbarSectionBtn"); sig_btn.setCheckable(True)
- sig_btn.clicked.connect(lambda c: self._toggle_win("signals", c, sig_btn))
- tb.addWidget(sig_btn); self._btn_signals = sig_btn
+ sig_btn.clicked.connect(lambda c: self._toggle_win("channels", c, sig_btn))
+ tb.addWidget(sig_btn); self._btn_channels = sig_btn
plot_btn = QPushButton("📐 Plot")
plot_btn.setObjectName("toolbarSectionBtn"); plot_btn.setCheckable(True)
@@ -309,13 +309,13 @@ class MainWindow(QMainWindow):
def _toggle_win(self, name: str, checked: bool, btn: QPushButton):
creators = {
"devices": self._open_devices,
- "signals": self._open_signals,
+ "channels": self._open_channels,
"plot": self._open_plot,
"settings": self._open_settings,
}
wins = {
"devices": "_win_devices",
- "signals": "_win_signals",
+ "channels": "_win_channels",
"plot": "_win_plot",
"settings": "_win_settings",
}
@@ -332,17 +332,17 @@ class MainWindow(QMainWindow):
self._win_devices.device_removed.connect(self._on_device_removed)
self._win_devices.device_reconfigured.connect(self._on_device_reconfigured)
self._win_devices.channel_visibility_changed.connect(self._on_channel_visibility_changed)
+ self._win_devices.channel_name_changed.connect(self._on_channel_name_changed)
+ self._win_devices.channel_unit_changed.connect(self._on_channel_unit_changed)
self._win_devices.closed.connect(lambda: self._btn_devices.setChecked(False))
self._show_win(self._win_devices, "right")
- def _open_signals(self):
- if self._win_signals is None:
- self._win_signals = SignalsWindow(self.registry, self.processor, self)
- self._win_signals.derived_changed.connect(self._on_derived_changed)
- self._win_signals.visibility_changed.connect(self._chart.refresh)
- self._win_signals.channel_toggled.connect(self._on_channel_visibility_changed)
- self._win_signals.closed.connect(lambda: self._btn_signals.setChecked(False))
- self._show_win(self._win_signals, "right")
+ def _open_channels(self):
+ if self._win_channels is None:
+ self._win_channels = ChannelsWindow(self.registry, self.processor, self)
+ self._win_channels.derived_changed.connect(self._on_derived_changed)
+ self._win_channels.closed.connect(lambda: self._btn_channels.setChecked(False))
+ self._show_win(self._win_channels, "right")
def _open_plot(self):
if self._win_plot is None:
@@ -415,8 +415,8 @@ class MainWindow(QMainWindow):
# Refresh open windows
if self._win_devices:
self._win_devices.refresh()
- if self._win_signals:
- self._win_signals.refresh_derived()
+ if self._win_channels:
+ self._win_channels.refresh_derived()
if self._win_plot:
self._win_plot.refresh_channels()
@@ -463,8 +463,8 @@ class MainWindow(QMainWindow):
# Refresh open windows
if self._win_devices:
self._win_devices.refresh()
- if self._win_signals:
- self._win_signals.refresh_derived()
+ if self._win_channels:
+ self._win_channels.refresh_derived()
if self._win_plot:
self._win_plot.refresh_channels()
self._status.setText(f"Profile loaded: {profile.name}")
@@ -474,24 +474,35 @@ class MainWindow(QMainWindow):
def _on_device_added(self):
self._chart.refresh()
if self._win_plot: self._win_plot.refresh_channels()
+ if self._win_channels: self._win_channels.on_device_added()
self._status.setText("Device added.")
def _on_device_removed(self, dev_id: str):
self._chart.refresh()
+ if self._win_channels: self._win_channels.on_device_removed(dev_id)
self._status.setText(f"Device '{dev_id}' removed.")
def _on_device_reconfigured(self, dev_id: str):
self._chart.refresh()
if self._win_plot: self._win_plot.refresh_channels()
+ if self._win_channels: self._win_channels.on_device_reconfigured(dev_id)
self._status.setText(f"Device '{dev_id}' reconfigured.")
def _on_channel_visibility_changed(self, dev_id: str, ch_id: str, enabled: bool):
self._chart.on_channel_enabled_changed(dev_id, ch_id, enabled)
- if self._win_signals:
- self._win_signals.on_channel_enabled_changed(dev_id, ch_id, enabled)
+ if self._win_channels:
+ self._win_channels.on_channel_enabled_changed(dev_id, ch_id, enabled)
if self._win_plot:
self._win_plot.refresh_channels()
+ def _on_channel_name_changed(self, dev_id: str, ch_id: str, name: str):
+ if self._win_channels:
+ self._win_channels.on_channel_name_changed(dev_id, ch_id, name)
+
+ def _on_channel_unit_changed(self, dev_id: str, ch_id: str, unit: str):
+ if self._win_channels:
+ self._win_channels.on_channel_unit_changed(dev_id, ch_id, unit)
+
def _on_derived_changed(self):
self._chart.refresh()
if self._win_plot: self._win_plot.refresh_channels()
@@ -544,7 +555,7 @@ class MainWindow(QMainWindow):
self._time_lbl.setText(f"{h:02d}:{m:02d}:{s:02d}")
def closeEvent(self, event):
- for w in (self._win_devices, self._win_signals,
+ for w in (self._win_devices, self._win_channels,
self._win_plot, self._win_settings):
if w: w.close()
for plugin in list(self._plugin_mgr.get_loaded()):
diff --git a/ui/windows/devices_window.py b/ui/windows/devices_window.py
index 8f6a48b..f942552 100644
--- a/ui/windows/devices_window.py
+++ b/ui/windows/devices_window.py
@@ -118,7 +118,9 @@ class DeviceRow(QFrame):
class ChannelsTab(QWidget):
"""Shows all channels across all devices in an editable table."""
- channel_visibility_changed = pyqtSignal(str, str, bool) # dev_id, ch_id, enabled
+ channel_visibility_changed = pyqtSignal(str, str, bool) # dev_id, ch_id, enabled
+ channel_name_changed = pyqtSignal(str, str, str) # dev_id, ch_id, name
+ channel_unit_changed = pyqtSignal(str, str, str) # dev_id, ch_id, unit
# Col indices
_C_DEVICE = 0
@@ -228,8 +230,10 @@ class ChannelsTab(QWidget):
return
if col == self._C_NAME:
ch.name = self._table.item(row, self._C_NAME).text()
+ self.channel_name_changed.emit(dev_id, ch_id, ch.name)
elif col == self._C_UNIT:
ch.unit = self._table.item(row, self._C_UNIT).text()
+ self.channel_unit_changed.emit(dev_id, ch_id, ch.unit)
elif col == self._C_MIN:
try:
ch.min_value = float(self._table.item(row, self._C_MIN).text())
@@ -247,6 +251,8 @@ class DevicesWindow(QWidget):
device_removed = pyqtSignal(str)
device_reconfigured = pyqtSignal(str)
channel_visibility_changed = pyqtSignal(str, str, bool) # dev_id, ch_id, enabled
+ channel_name_changed = pyqtSignal(str, str, str) # dev_id, ch_id, name
+ channel_unit_changed = pyqtSignal(str, str, str) # dev_id, ch_id, unit
closed = pyqtSignal()
def __init__(self, registry: DeviceRegistry, engine: AcquisitionEngine, parent=None):
@@ -305,7 +311,9 @@ class DevicesWindow(QWidget):
# Tab 2: Channels
self._ch_tab = ChannelsTab(self.registry)
self._ch_tab.channel_visibility_changed.connect(self.channel_visibility_changed)
- tabs.addTab(self._ch_tab, " Channels ")
+ self._ch_tab.channel_name_changed.connect(self.channel_name_changed)
+ self._ch_tab.channel_unit_changed.connect(self.channel_unit_changed)
+ tabs.addTab(self._ch_tab, " Signals ")
tabs.currentChanged.connect(lambda i: self._ch_tab.refresh() if i == 1 else None)
def refresh(self):
@@ -318,6 +326,7 @@ class DevicesWindow(QWidget):
row.remove_clicked.connect(self._on_remove)
self._inner.insertWidget(self._inner.count() - 1, row)
self._rows[dev.info.device_id] = row
+ self._ch_tab.refresh()
def _refresh_status(self):
for row in self._rows.values():
@@ -340,7 +349,7 @@ class DevicesWindow(QWidget):
dev = self.registry.get_instance(device_id)
if dev:
DeviceConfigDialog(dev, self).exec()
- # Signal that device config may have changed (backend/simulate/port)
+ self._ch_tab.refresh()
self.device_reconfigured.emit(device_id)
def _on_remove(self, device_id: str):
diff --git a/ui/windows/signals_window.py b/ui/windows/signals_window.py
index 0bf9dba..eb146f9 100644
--- a/ui/windows/signals_window.py
+++ b/ui/windows/signals_window.py
@@ -143,6 +143,8 @@ class ChannelPipelineBlock(QFrame):
self._load()
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)
# Header
@@ -151,8 +153,8 @@ class ChannelPipelineBlock(QFrame):
title = f"{self.dev_id} / {self.ch_id} ({ch_name})"
if unit:
title += f" [{unit}]"
- lbl = QLabel(title); lbl.setObjectName("traceSource")
- hl.addWidget(lbl, 1)
+ self._title_lbl = QLabel(title); self._title_lbl.setObjectName("traceSource")
+ hl.addWidget(self._title_lbl, 1)
rm_btn = QToolButton(); rm_btn.setText("✕")
rm_btn.setObjectName("traceRemoveBtn"); rm_btn.setFixedSize(22, 22)
@@ -198,6 +200,20 @@ class ChannelPipelineBlock(QFrame):
outer.addWidget(self._body)
self._body.setVisible(False)
+ def _refresh_title(self):
+ title = f"{self.dev_id} / {self.ch_id} ({self._ch_name})"
+ if self._unit:
+ title += f" [{self._unit}]"
+ self._title_lbl.setText(title)
+
+ def update_name(self, ch_name: str):
+ self._ch_name = ch_name
+ self._refresh_title()
+
+ def update_unit(self, unit: str):
+ self._unit = unit
+ self._refresh_title()
+
def _toggle(self):
self._expanded = not self._expanded
self._body.setVisible(self._expanded)
@@ -247,11 +263,12 @@ class ChannelPipelineBlock(QFrame):
# ══════════════════════════════════════════════════════════════════════════════
-# Channels Tab (formerly Pipeline Tab) — user-curated channel list
+# Pipeline Tab — filter pipelines per channel + virtual channel creation
# ══════════════════════════════════════════════════════════════════════════════
class PipelineTab(QWidget):
- changed = pyqtSignal()
+ changed = pyqtSignal()
+ derived_changed = pyqtSignal()
def __init__(self, registry: DeviceRegistry, processor: SignalProcessor):
super().__init__()
@@ -259,12 +276,14 @@ class PipelineTab(QWidget):
self.processor = processor
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):
lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.setSpacing(0)
- # Add-channel bar at top
+ # 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:"))
@@ -273,6 +292,18 @@ class PipelineTab(QWidget):
add_btn = QPushButton("+ Add Channel"); add_btn.setObjectName("addTraceBtn")
add_btn.clicked.connect(self._on_add_channel)
ab_lay.addWidget(add_btn)
+
+ sep = QFrame(); sep.setFrameShape(QFrame.Shape.VLine)
+ sep.setObjectName("toolbarSep"); sep.setFixedWidth(6)
+ ab_lay.addWidget(sep)
+
+ ab_lay.addWidget(QLabel("Virtual:"))
+ 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)
+ 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)
div = QFrame(); div.setFrameShape(QFrame.Shape.HLine)
@@ -288,7 +319,6 @@ class PipelineTab(QWidget):
scroll.setWidget(self._cont)
lay.addWidget(scroll, 1)
- # Populate with enabled device channels on startup
for dev in self.registry.all_instances():
for ch in dev.info.channels:
if ch.enabled:
@@ -296,7 +326,7 @@ class PipelineTab(QWidget):
self._refresh_avail_cb()
- # ── 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:
@@ -306,8 +336,9 @@ class PipelineTab(QWidget):
block.changed.connect(self.changed)
block.removed.connect(self._on_remove_block)
self._blocks[(dev_id, ch_id)] = block
- # Insert before the trailing stretch
- self._inner.insertWidget(self._inner.count() - 1, block)
+ # Insert before derived blocks and trailing stretch
+ insert_at = self._inner.count() - 1 - len(self._derived_blocks)
+ self._inner.insertWidget(insert_at, block)
def on_channel_enabled_changed(self, dev_id: str, ch_id: str, enabled: bool):
key = (dev_id, ch_id)
@@ -328,6 +359,48 @@ class PipelineTab(QWidget):
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]
+ for key in keys:
+ 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)
+ if dev is None:
+ self.on_device_removed(dev_id)
+ return
+ valid = {ch.channel_id for ch in dev.info.channels}
+ keys = [(d, c) for (d, c) in list(self._shown)
+ if d == dev_id and c not in valid]
+ for key in keys:
+ 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()
+
+ def on_channel_name_changed(self, dev_id: str, ch_id: str, ch_name: str):
+ key = (dev_id, ch_id)
+ if key in self._blocks:
+ self._blocks[key].update_name(ch_name)
+ else:
+ dev = self.registry.get_instance(dev_id)
+ if dev:
+ 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)
+ if key in self._blocks:
+ self._blocks[key].update_unit(unit)
+
def _on_add_channel(self):
data = self._avail_cb.currentData()
if not data:
@@ -363,6 +436,44 @@ class PipelineTab(QWidget):
self._avail_cb.addItem(label,
userData=(dev.info.device_id, ch.channel_id))
+ # ── 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"sig_{n}", name=f"Signal {n}",
+ kind=kind, color=color)
+ blk = self._make_derived_block(dc)
+ self._inner.insertWidget(self._inner.count() - 1, blk)
+
+ def _make_derived_block(self, dc: DerivedChannel) -> DerivedBlock:
+ blk = DerivedBlock(dc, self.registry, self.processor)
+ blk.removed.connect(self._rm_virtual)
+ blk.applied.connect(lambda _: self.derived_changed.emit())
+ self._derived_blocks.append(blk)
+ return blk
+
+ 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.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._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._color_idx = len(self._derived_blocks)
+
# ══════════════════════════════════════════════════════════════════════════════
# Derived editor for one DerivedChannel
@@ -544,12 +655,12 @@ class DerivedBlock(QFrame):
# ══════════════════════════════════════════════════════════════════════════════
-# Signals Tab — flat unified list of all signals
+# Signals List Tab — flat list of all signals with enable/disable
# ══════════════════════════════════════════════════════════════════════════════
-class SignalsTab(QWidget):
- changed = pyqtSignal()
- channel_toggled = pyqtSignal(str, str, bool) # dev_id, ch_id, enabled
+class SignalsListTab(QWidget):
+ visibility_changed = pyqtSignal()
+ channel_toggled = pyqtSignal(str, str, bool) # dev_id, ch_id, enabled
def __init__(self, registry: DeviceRegistry, processor: SignalProcessor):
super().__init__()
@@ -560,6 +671,7 @@ class SignalsTab(QWidget):
def _build(self):
scroll = QScrollArea(); scroll.setWidgetResizable(True)
scroll.setObjectName("deviceScroll")
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self._cont = QWidget()
self._lay = QVBoxLayout(self._cont)
self._lay.setContentsMargins(8,8,8,8); self._lay.setSpacing(2)
@@ -573,39 +685,37 @@ class SignalsTab(QWidget):
it = self._lay.takeAt(0)
if it.widget(): it.widget().deleteLater()
- # Physical channels — flat list across all devices
for dev in self.registry.all_instances():
for ch in dev.info.channels:
dev_id_cap = dev.info.device_id
ch_id_cap = ch.channel_id
self._lay.addWidget(self._make_row(
- color = ch.color,
- name = f"{ch.channel_id} — {ch.name}",
- unit = ch.unit,
- source = dev_id_cap,
- enabled = ch.enabled,
+ color = ch.color,
+ name = f"{ch.channel_id} — {ch.name}",
+ unit = ch.unit,
+ source = dev_id_cap,
+ enabled = ch.enabled,
on_toggle = lambda v, c=ch, d=dev_id_cap, ci=ch_id_cap: (
setattr(c, "enabled", v),
- self.changed.emit(),
+ self.visibility_changed.emit(),
self.channel_toggled.emit(d, ci, v),
),
))
- # Thin divider before derived (only if both sections have entries)
- if self.registry.all_instances() and self.processor.get_derived():
+ derived = self.processor.get_derived()
+ if self.registry.all_instances() and derived:
div = QFrame(); div.setFrameShape(QFrame.Shape.HLine)
div.setObjectName("devWindowDivider")
self._lay.addWidget(div)
- # Derived signals
- for dc in self.processor.get_derived():
+ for dc in derived:
self._lay.addWidget(self._make_row(
- color = dc.color,
- name = f"{dc.channel_id} — {dc.name}",
- unit = dc.unit,
- source = "derived",
- enabled = dc.enabled,
- on_toggle = lambda v, d=dc: (setattr(d,"enabled",v), self.changed.emit()),
+ color = dc.color,
+ name = f"{dc.channel_id} — {dc.name}",
+ unit = dc.unit,
+ source = "virtual",
+ enabled = dc.enabled,
+ on_toggle = lambda v, d=dc: (setattr(d, "enabled", v), self.visibility_changed.emit()),
))
self._lay.addStretch()
@@ -637,25 +747,21 @@ class SignalsTab(QWidget):
# ══════════════════════════════════════════════════════════════════════════════
-# SignalsWindow — main
+# ChannelsWindow — main
# ══════════════════════════════════════════════════════════════════════════════
-class SignalsWindow(QWidget):
- pipeline_changed = pyqtSignal()
- derived_changed = pyqtSignal()
- visibility_changed = pyqtSignal()
- channel_toggled = pyqtSignal(str, str, bool) # dev_id, ch_id, enabled
- closed = pyqtSignal()
+class ChannelsWindow(QWidget):
+ pipeline_changed = pyqtSignal()
+ derived_changed = pyqtSignal()
+ closed = pyqtSignal()
def __init__(self, registry: DeviceRegistry,
processor: SignalProcessor, parent=None):
super().__init__(parent, Qt.WindowType.Window | Qt.WindowType.Tool)
self.registry = registry
self.processor = processor
- self._derived_blocks: List[DerivedBlock] = []
- self._color_idx = 0
- self.setWindowTitle("Signals")
+ self.setWindowTitle("Channels")
self.setMinimumSize(640, 520)
self.resize(740, 640)
self._build()
@@ -663,109 +769,40 @@ class SignalsWindow(QWidget):
def _build(self):
root = QVBoxLayout(self); root.setContentsMargins(0,0,0,0); root.setSpacing(0)
- # Header
hdr = QWidget(); hdr.setObjectName("devWindowTitleBar"); hdr.setFixedHeight(44)
hl = QHBoxLayout(hdr); hl.setContentsMargins(14,0,14,0)
- hl.addWidget(QLabel("SIGNALS").also(lambda w: w.setObjectName("devWindowTitle")), 1)
+ hl.addWidget(QLabel("CHANNELS").also(lambda w: w.setObjectName("devWindowTitle")), 1)
root.addWidget(hdr)
div = QFrame(); div.setFrameShape(QFrame.Shape.HLine)
div.setObjectName("devWindowDivider"); root.addWidget(div)
- tabs = QTabWidget(); tabs.setObjectName("signalBuilderTabs")
- root.addWidget(tabs, 1)
-
- # Tab 1: Channels (filter pipelines per channel)
self._pip_tab = PipelineTab(self.registry, self.processor)
self._pip_tab.changed.connect(self.pipeline_changed)
- tabs.addTab(self._pip_tab, " Channels ")
+ self._pip_tab.derived_changed.connect(self.derived_changed)
+ root.addWidget(self._pip_tab, 1)
- # Tab 2: Derived channels
- tabs.addTab(self._build_derived_tab(), " Derived ")
-
- # Tab 3: Flat signals list
- self._vis_tab = SignalsTab(self.registry, self.processor)
- self._vis_tab.changed.connect(self.visibility_changed)
- self._vis_tab.channel_toggled.connect(self._on_vis_tab_toggled)
- tabs.addTab(self._vis_tab, " Signals ")
-
- # Keep Signals tab in sync when derived channels are added/removed
- self.derived_changed.connect(self._vis_tab.refresh)
-
- # Populate from processor state (handles opening window after profile load)
self.refresh_derived()
- def _build_derived_tab(self):
- w = QWidget()
- lay = QVBoxLayout(w); lay.setContentsMargins(0,0,0,0); lay.setSpacing(0)
-
- top = QWidget(); top.setObjectName("cfgGlobalBar")
- tl = QHBoxLayout(top); tl.setContentsMargins(10,7,10,7)
- tl.addWidget(QLabel("New:"))
- self._new_kind = QComboBox(); self._new_kind.setObjectName("channelPickerCb")
- self._new_kind.addItems([_KIND_DISPLAY[k] for k in _ALL_KINDS])
- tl.addWidget(self._new_kind)
- cr = QPushButton("+ Create Derived Channel")
- cr.setObjectName("addDeviceButton"); cr.clicked.connect(self._add_derived)
- tl.addWidget(cr); tl.addStretch()
- lay.addWidget(top)
-
- div = QFrame(); div.setFrameShape(QFrame.Shape.HLine)
- div.setObjectName("devWindowDivider"); lay.addWidget(div)
-
- scroll = QScrollArea(); scroll.setWidgetResizable(True)
- scroll.setObjectName("deviceScroll")
- self._d_cont = QWidget()
- self._d_lay = QVBoxLayout(self._d_cont)
- self._d_lay.setContentsMargins(10,10,10,10); self._d_lay.setSpacing(10)
- self._d_lay.addStretch()
- scroll.setWidget(self._d_cont)
- lay.addWidget(scroll, 1)
- return w
-
- def _add_derived(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"sig_{n}", name=f"Signal {n}",
- kind=kind, color=color)
- blk = DerivedBlock(dc, self.registry, self.processor)
- blk.removed.connect(self._rm_derived)
- blk.applied.connect(lambda _: self.derived_changed.emit())
- self._derived_blocks.append(blk)
- self._d_lay.insertWidget(self._d_lay.count()-1, blk)
+ def refresh_derived(self):
+ self._pip_tab.refresh_derived()
- def _rm_derived(self, blk: DerivedBlock):
- self.processor.remove_derived(blk.dc.channel_id)
- self._derived_blocks.remove(blk)
- self._d_lay.removeWidget(blk); blk.deleteLater()
- self.derived_changed.emit()
+ def on_channel_enabled_changed(self, dev_id: str, ch_id: str, enabled: bool):
+ self._pip_tab.on_channel_enabled_changed(dev_id, ch_id, enabled)
- def refresh_derived(self):
- """Rebuild DerivedBlock widgets from processor state (called after profile load)."""
- for blk in self._derived_blocks:
- self._d_lay.removeWidget(blk); blk.deleteLater()
- self._derived_blocks.clear()
+ def on_device_removed(self, dev_id: str):
+ self._pip_tab.on_device_removed(dev_id)
- for dc in self.processor.get_derived():
- blk = DerivedBlock(dc, self.registry, self.processor)
- blk.removed.connect(self._rm_derived)
- blk.applied.connect(lambda _: self.derived_changed.emit())
- self._derived_blocks.append(blk)
- self._d_lay.insertWidget(self._d_lay.count() - 1, blk)
+ def on_device_reconfigured(self, dev_id: str):
+ self._pip_tab.on_device_reconfigured(dev_id)
- self._color_idx = len(self._derived_blocks)
- self._vis_tab.refresh()
+ def on_device_added(self):
+ self._pip_tab.on_device_added()
- def _on_vis_tab_toggled(self, dev_id: str, ch_id: str, enabled: bool):
- """Signals tab toggled a channel — propagate to pipeline + chart without re-refreshing vis_tab."""
- self._pip_tab.on_channel_enabled_changed(dev_id, ch_id, enabled)
- self.channel_toggled.emit(dev_id, ch_id, enabled)
+ def on_channel_name_changed(self, dev_id: str, ch_id: str, ch_name: str):
+ self._pip_tab.on_channel_name_changed(dev_id, ch_id, ch_name)
- def on_channel_enabled_changed(self, dev_id: str, ch_id: str, enabled: bool):
- """Called from outside (Devices window) — update pipeline block and sync vis_tab checkboxes."""
- self._pip_tab.on_channel_enabled_changed(dev_id, ch_id, enabled)
- self._vis_tab.refresh()
+ def on_channel_unit_changed(self, dev_id: str, ch_id: str, unit: str):
+ self._pip_tab.on_channel_unit_changed(dev_id, ch_id, unit)
def closeEvent(self, e: QCloseEvent):
self.closed.emit(); e.accept()