summaryrefslogtreecommitdiff
path: root/ui/namespace_panel.py
diff options
context:
space:
mode:
Diffstat (limited to 'ui/namespace_panel.py')
-rw-r--r--ui/namespace_panel.py192
1 files changed, 192 insertions, 0 deletions
diff --git a/ui/namespace_panel.py b/ui/namespace_panel.py
new file mode 100644
index 0000000..2fcc7a1
--- /dev/null
+++ b/ui/namespace_panel.py
@@ -0,0 +1,192 @@
+"""
+ui/namespace_panel.py
+
+NAMESPACE panel — dockable, live browser of everything referenceable in a
+script: Signals (raw device endpoints), Channels (processed/derived
+values), and vars (the shared dict bridging Control Panel On Action
+Scripts and Channel scripts). Read-only reference, not an editor — editing
+still happens in Configuration > Channels.
+
+Docked opposite the Control Panel (ui/main_window.py), built the same way:
+a QDockWidget you can drag, float, redock, or close.
+"""
+
+from __future__ import annotations
+from typing import Dict, Tuple
+
+from PyQt6.QtWidgets import (
+ QWidget, QVBoxLayout, QHBoxLayout, QLabel,
+ QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView,
+ QApplication,
+)
+from PyQt6.QtCore import QTimer, pyqtSlot
+from PyQt6.QtGui import QColor
+
+from devices.device_registry import DeviceRegistry
+from core.signal_processor import SignalProcessor
+
+_VARS_POLL_MS = 400
+
+_C_KIND = 0
+_C_NAME = 1
+_C_DIR = 2
+_C_VALUE = 3
+_C_SOURCE = 4
+
+
+def _fmt_value(v) -> str:
+ if v is None:
+ return "—"
+ if isinstance(v, float):
+ return f"{v:.4g}"
+ return str(v)
+
+
+class NamespacePanel(QWidget):
+ def __init__(self, registry: DeviceRegistry, processor: SignalProcessor):
+ super().__init__()
+ self.registry = registry
+ self.processor = processor
+ # row key -> row index. Keys:
+ # ("signal", dev_id, ch_id) | ("channel", channel_id) | ("var", name)
+ self._row_index: Dict[Tuple, int] = {}
+ self._build()
+ self._rebuild_rows()
+
+ self._poll = QTimer(self)
+ self._poll.setInterval(_VARS_POLL_MS)
+ self._poll.timeout.connect(self._on_poll)
+ self._poll.start()
+
+ def _build(self):
+ lay = QVBoxLayout(self)
+ lay.setContentsMargins(0, 0, 0, 0)
+ lay.setSpacing(0)
+
+ hint = QLabel(
+ "Double-click a row to copy a ready-to-paste reference "
+ "(vars.get(...) or channels.get(...)) to the clipboard."
+ )
+ hint.setObjectName("traceSource")
+ hint.setWordWrap(True)
+ hint.setContentsMargins(8, 6, 8, 6)
+ lay.addWidget(hint)
+
+ self._table = QTableWidget()
+ self._table.setObjectName("channelTable")
+ self._table.setColumnCount(5)
+ self._table.setHorizontalHeaderLabels(
+ ["Kind", "Name", "Dir", "Value", "Source"]
+ )
+ hdr = self._table.horizontalHeader()
+ hdr.setSectionResizeMode(_C_SOURCE, QHeaderView.ResizeMode.Stretch)
+ hdr.setSectionResizeMode(_C_KIND, QHeaderView.ResizeMode.ResizeToContents)
+ hdr.setSectionResizeMode(_C_NAME, QHeaderView.ResizeMode.ResizeToContents)
+ hdr.setSectionResizeMode(_C_DIR, QHeaderView.ResizeMode.ResizeToContents)
+ hdr.setSectionResizeMode(_C_VALUE, QHeaderView.ResizeMode.ResizeToContents)
+ self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
+ self._table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
+ self._table.setAlternatingRowColors(True)
+ self._table.verticalHeader().setVisible(False)
+ self._table.cellDoubleClicked.connect(self._on_double_clicked)
+ lay.addWidget(self._table)
+
+ # ── Row-set (structural) rebuild — devices/channels/vars added or removed ──
+
+ def _desired_keys(self):
+ keys = []
+ for dev in self.registry.all_instances():
+ for ch in dev.info.channels:
+ if ch.enabled:
+ keys.append(("signal", dev.info.device_id, ch.channel_id))
+ for dc in self.processor.get_derived():
+ keys.append(("channel", dc.channel_id))
+ for name in self.processor.list_vars():
+ keys.append(("var", name))
+ return keys
+
+ def _rebuild_rows(self):
+ keys = self._desired_keys()
+ if list(self._row_index.keys()) == keys:
+ return # no structural change — values refresh independently
+ self._row_index.clear()
+ self._table.setRowCount(len(keys))
+ for row, key in enumerate(keys):
+ self._row_index[key] = row
+ self._paint_row(row, key)
+
+ def _paint_row(self, row: int, key: Tuple):
+ kind = key[0]
+ if kind == "signal":
+ _, dev_id, ch_id = key
+ dev = self.registry.get_instance(dev_id)
+ ch = dev.get_channel(ch_id) if dev else None
+ name = ch.name if ch else ch_id
+ is_wr = bool(ch and ch.writable)
+ self._set_row(row, "Signal", name, "WRITE" if is_wr else "READ",
+ None, f"{dev_id}/{ch_id}")
+ elif kind == "channel":
+ _, channel_id = key
+ dc = next((d for d in self.processor.get_derived()
+ if d.channel_id == channel_id), None)
+ name = dc.name if dc else channel_id
+ src = dc.kind if dc else ""
+ self._set_row(row, "Channel", name, "READ", None, src)
+ else: # var
+ _, name = key
+ val = self.processor.get_var(name, None)
+ self._set_row(row, "var", name, "R/W", val, "shared (vars)")
+
+ def _set_row(self, row: int, kind: str, name: str, direction: str,
+ value, source: str):
+ self._table.setItem(row, _C_KIND, QTableWidgetItem(kind))
+ self._table.setItem(row, _C_NAME, QTableWidgetItem(name))
+ dir_item = QTableWidgetItem(direction)
+ dir_item.setForeground(_dir_color(direction))
+ self._table.setItem(row, _C_DIR, dir_item)
+ self._table.setItem(row, _C_VALUE, QTableWidgetItem(_fmt_value(value)))
+ self._table.setItem(row, _C_SOURCE, QTableWidgetItem(source))
+
+ # ── Live value updates ──────────────────────────────────────────────────
+
+ @pyqtSlot(str, str, float, float)
+ def on_processed_data(self, device_id: str, channel_id: str, ts: float, value: float):
+ key = ("channel", channel_id) if device_id == "derived" else ("signal", device_id, channel_id)
+ row = self._row_index.get(key)
+ if row is None:
+ return
+ item = self._table.item(row, _C_VALUE)
+ if item is not None:
+ item.setText(_fmt_value(value))
+
+ def _on_poll(self):
+ self._rebuild_rows()
+ for name in self.processor.list_vars():
+ row = self._row_index.get(("var", name))
+ if row is None:
+ continue
+ item = self._table.item(row, _C_VALUE)
+ if item is not None:
+ item.setText(_fmt_value(self.processor.get_var(name, None)))
+
+ # ── Copy-to-clipboard reference ─────────────────────────────────────────
+
+ def _on_double_clicked(self, row: int, _col: int):
+ key = next((k for k, r in self._row_index.items() if r == row), None)
+ if key is None:
+ return
+ if key[0] == "var":
+ snippet = f'vars.get("{key[1]}", 0)'
+ elif key[0] == "signal":
+ snippet = f'channels.get("{key[2]}", 0)'
+ else: # channel
+ snippet = f'channels.get("{key[1]}", 0)'
+ QApplication.clipboard().setText(snippet)
+
+
+def _dir_color(direction: str) -> QColor:
+ if direction == "WRITE":
+ return QColor("#f59e0b")
+ if direction == "R/W":
+ return QColor("#818cf8")
+ return QColor("#22c55e")