diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-06-03 11:38:49 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-06-03 11:38:49 -0600 |
| commit | 23c669b3c6fb3bc805887cef00d6c37756e5c41e (patch) | |
| tree | d563ab55c83dd59083fe748347a5e4010b7fa416 /ui | |
| parent | 3610309b1e5d83049554afe287129c99389d231d (diff) | |
Sync Channels window with device/channel changes
- Remove redundant Signals tab from Channels window; PipelineTab is now direct content
- PipelineTab removes stale blocks when devices are removed or reconfigured
- Devices>Signals tab refreshes automatically on device add/remove/configure
- Channel name and unit edits in Devices>Signals propagate live to Channels pipeline block headers; missing blocks auto-added if channel is enabled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'ui')
| -rw-r--r-- | ui/main_window.py | 55 | ||||
| -rw-r--r-- | ui/windows/devices_window.py | 15 | ||||
| -rw-r--r-- | ui/windows/signals_window.py | 289 |
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() |
