diff options
Diffstat (limited to 'ui/windows/config_window.py')
| -rw-r--r-- | ui/windows/config_window.py | 113 |
1 files changed, 56 insertions, 57 deletions
diff --git a/ui/windows/config_window.py b/ui/windows/config_window.py index 05cc1fb..64c669d 100644 --- a/ui/windows/config_window.py +++ b/ui/windows/config_window.py @@ -5,8 +5,8 @@ Unified Configuration window — Devices | Channels | Plot in one tabbed window. Channels tab layout (2 columns): Left — signal table (Device / Signal ID / Dir / On / Name / Unit / Min / Max) - ⚙ button per row opens that channel's filter/calibration pipeline dialog - Right — derived / virtual channels + Right — derived / virtual channels — operations (filters/calibration/math/script) + are configured per Channel here, not on the Signals side. """ from __future__ import annotations @@ -17,17 +17,17 @@ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QToolButton, QScrollArea, QFrame, QTabWidget, QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox, QMessageBox, QAbstractItemView, QSplitter, - QDoubleSpinBox, QDialog, QDialogButtonBox, + QDoubleSpinBox, ) from PyQt6.QtCore import Qt, pyqtSignal, QTimer from PyQt6.QtGui import QCloseEvent, QColor from devices.device_registry import DeviceRegistry from core.acquisition import AcquisitionEngine -from core.signal_processor import SignalProcessor +from core.signal_processor import SignalProcessor, DerivedChannel from ui.windows.devices_window import DeviceRow, ChannelsTab -from ui.windows.channels_window import PipelineTab, ChannelPipelineBlock +from ui.windows.channels_window import PipelineTab, _slug_id, _dedupe_name from ui.windows.plot_window import ( PlotWindow, LayoutConfig, LayoutCanvas, PaneBlock, build_default_layout, @@ -35,37 +35,12 @@ from ui.windows.plot_window import ( # ══════════════════════════════════════════════════════════════════════════════ -# Pipeline dialog — per-channel filter/calibration popup -# ══════════════════════════════════════════════════════════════════════════════ - -class _PipelineDialog(QDialog): - """Shows one ChannelPipelineBlock inside a dialog for a single signal.""" - - def __init__(self, dev_id: str, ch_id: str, ch_name: str, unit: str, - processor: SignalProcessor, parent=None): - super().__init__(parent) - self.setWindowTitle(f"Pipeline — {dev_id} / {ch_id}") - self.setMinimumWidth(460) - self.resize(500, 420) - - lay = QVBoxLayout(self) - lay.setContentsMargins(0, 0, 0, 12) - - self._block = ChannelPipelineBlock(dev_id, ch_id, ch_name, unit, processor) - self._block._toggle() # start expanded - lay.addWidget(self._block, 1) - - btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) - btns.rejected.connect(self.accept) - lay.addWidget(btns) - - -# ══════════════════════════════════════════════════════════════════════════════ # Enhanced signals table — adds Dir badge and pipeline ⚙ button # ══════════════════════════════════════════════════════════════════════════════ class _SignalsTab(ChannelsTab): - """ChannelsTab with Dir (IN/OUT) badge and per-row ⚙ pipeline button.""" + """ChannelsTab with a Dir (READ/WRITE) badge. Operations (filters/calibration/math) + are configured per Channel in the Channels pane, not here.""" _C_DEVICE = 0 _C_CH_ID = 1 @@ -75,7 +50,9 @@ class _SignalsTab(ChannelsTab): _C_UNIT = 5 _C_MIN = 6 _C_MAX = 7 - _C_PIPE = 8 # ⚙ pipeline button + _C_MKCH = 8 # "+ Channel" button + + channel_created = pyqtSignal() def __init__(self, registry: DeviceRegistry, processor: SignalProcessor): self._processor = processor @@ -97,7 +74,7 @@ class _SignalsTab(ChannelsTab): hdr.setSectionResizeMode(self._C_CH_ID, QHeaderView.ResizeMode.ResizeToContents) hdr.setSectionResizeMode(self._C_DIR, QHeaderView.ResizeMode.ResizeToContents) hdr.setSectionResizeMode(self._C_ENABLED, QHeaderView.ResizeMode.ResizeToContents) - hdr.setSectionResizeMode(self._C_PIPE, QHeaderView.ResizeMode.ResizeToContents) + hdr.setSectionResizeMode(self._C_MKCH, QHeaderView.ResizeMode.ResizeToContents) self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self._table.setAlternatingRowColors(True) self._table.verticalHeader().setVisible(False) @@ -122,8 +99,8 @@ class _SignalsTab(ChannelsTab): ch_item.setForeground(QColor(ch.color)) self._table.setItem(row, self._C_CH_ID, ch_item) - is_out = getattr(ch, "is_output", False) - dir_item = QTableWidgetItem("OUT" if is_out else "IN") + is_out = getattr(ch, "writable", False) + dir_item = QTableWidgetItem("WRITE" if is_out else "READ") dir_item.setFlags(dir_item.flags() & ~Qt.ItemFlag.ItemIsEditable) dir_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) dir_item.setForeground(QColor("#f59e0b") if is_out else QColor("#22c55e")) @@ -156,33 +133,47 @@ class _SignalsTab(ChannelsTab): self._table.setItem(row, self._C_MIN, QTableWidgetItem(str(ch.min_value))) self._table.setItem(row, self._C_MAX, QTableWidgetItem(str(ch.max_value))) - # Pipeline ⚙ button - pipe_w = QWidget() - pipe_l = QHBoxLayout(pipe_w) - pipe_l.setContentsMargins(2, 0, 2, 0) - pipe_l.setAlignment(Qt.AlignmentFlag.AlignCenter) - pipe_btn = QToolButton() - pipe_btn.setText("⚙") - pipe_btn.setObjectName("traceRemoveBtn") - pipe_btn.setFixedSize(22, 22) - pipe_btn.setToolTip("Configure filters / calibration") - d_cap, c_cap, n_cap, u_cap = ( - dev.info.device_id, ch.channel_id, ch.name, ch.unit + mkch_w = QWidget() + mkch_l = QHBoxLayout(mkch_w) + mkch_l.setContentsMargins(2, 0, 2, 0) + mkch_l.setAlignment(Qt.AlignmentFlag.AlignCenter) + mkch_btn = QToolButton() + mkch_btn.setText("⊕") + mkch_btn.setObjectName("addTraceBtn") + mkch_btn.setFixedSize(24, 22) + mkch_btn.setToolTip("Create a Channel from this Signal") + d_cap, c_cap, n_cap, u_cap, col_cap = ( + dev.info.device_id, ch.channel_id, ch.name, ch.unit, ch.color ) - pipe_btn.clicked.connect( - lambda _=False, d=d_cap, c=c_cap, n=n_cap, u=u_cap: - self._open_pipeline(d, c, n, u) + mkch_btn.clicked.connect( + lambda _=False, d=d_cap, c=c_cap, n=n_cap, u=u_cap, col=col_cap: + self._on_create_channel(d, c, n, u, col) ) - pipe_l.addWidget(pipe_btn) - self._table.setCellWidget(row, self._C_PIPE, pipe_w) + mkch_l.addWidget(mkch_btn) + self._table.setCellWidget(row, self._C_MKCH, mkch_w) row += 1 self._table.blockSignals(False) - def _open_pipeline(self, dev_id: str, ch_id: str, ch_name: str, unit: str): - dlg = _PipelineDialog(dev_id, ch_id, ch_name, unit, self._processor, self) - dlg.exec() + def _on_create_channel(self, dev_id: str, ch_id: str, ch_name: str, unit: str, color: str): + """Create a passthrough Channel (kind="none") from this Signal, + named after it — the "1 Signal -> 1 Channel" quick path. No-ops if + a passthrough Channel for this exact Signal already exists.""" + existing = self._processor.get_derived() + for dc in existing: + if dc.kind == "none" and dc.sources == [(dev_id, ch_id)]: + return + existing_names = {d.name for d in existing} + existing_ids = {d.channel_id for d in existing} + name = _dedupe_name(ch_name, existing_names) + channel_id = _slug_id(name, existing_ids) + dc = DerivedChannel(channel_id=channel_id, name=name, kind="none", + sources=[(dev_id, ch_id)], source_names=[ch_name], + unit=unit, color=color or "#00d4ff") + err = self._processor.add_derived(dc) + if err is None: + self.channel_created.emit() # ══════════════════════════════════════════════════════════════════════════════ @@ -353,6 +344,7 @@ class ConfigWindow(QWidget): row = DeviceRow(dev) row.configure_clicked.connect(self._on_configure_device) row.remove_clicked.connect(self._on_remove_device) + row.name_changed.connect(self._on_device_renamed) self._dev_inner.insertWidget(self._dev_inner.count() - 1, row) self._dev_rows[dev.info.device_id] = row @@ -371,14 +363,20 @@ class ConfigWindow(QWidget): self.engine.add_device(dev) self.refresh_devices() self._signals_tab.refresh() + self._pipeline_tab.on_device_added() self.device_added.emit() + def _on_device_renamed(self, device_id: str): + self._signals_tab.refresh() + self.device_reconfigured.emit(device_id) + def _on_configure_device(self, device_id: str): from ui.config_dialog import DeviceConfigDialog dev = self.registry.get_instance(device_id) if dev: DeviceConfigDialog(dev, self).exec() self._signals_tab.refresh() + self._pipeline_tab.on_device_reconfigured(device_id) self.device_reconfigured.emit(device_id) def _on_remove_device(self, device_id: str): @@ -412,7 +410,7 @@ class ConfigWindow(QWidget): sig_l = QVBoxLayout(sig_w); sig_l.setContentsMargins(0, 0, 0, 0); sig_l.setSpacing(0) sig_bar = QWidget(); sig_bar.setObjectName("cfgGlobalBar") - sb_lay = QHBoxLayout(sig_bar); sb_lay.setContentsMargins(10, 7, 10, 7) + sb_lay = QHBoxLayout(sig_bar); sb_lay.setContentsMargins(10, 7, 10, 7); sb_lay.setSpacing(6) sb_lbl = QLabel("Signals"); sb_lbl.setObjectName("devWindowTitle") sb_lay.addWidget(sb_lbl, 1) sig_l.addWidget(sig_bar) @@ -435,6 +433,7 @@ class ConfigWindow(QWidget): if inner_splitters: inner_splitters[0].widget(0).setVisible(False) splitter.addWidget(self._pipeline_tab) + self._signals_tab.channel_created.connect(self._pipeline_tab.refresh_derived) splitter.setStretchFactor(0, 2) splitter.setStretchFactor(1, 1) |
