summaryrefslogtreecommitdiff
path: root/ui/devices_window.py
diff options
context:
space:
mode:
Diffstat (limited to 'ui/devices_window.py')
-rw-r--r--ui/devices_window.py224
1 files changed, 224 insertions, 0 deletions
diff --git a/ui/devices_window.py b/ui/devices_window.py
new file mode 100644
index 0000000..0548330
--- /dev/null
+++ b/ui/devices_window.py
@@ -0,0 +1,224 @@
+"""
+ui/devices_window.py
+
+Floating "Devices" settings window.
+Opens from the toolbar ⚙ DEVICES button; can stay open alongside the main window.
+Contains: device status cards, Add Device, and per-device Configure.
+"""
+
+from PyQt6.QtWidgets import (
+ QWidget, QVBoxLayout, QHBoxLayout, QLabel,
+ QPushButton, QScrollArea, QFrame, QSizePolicy,
+ QDialog,
+)
+from PyQt6.QtCore import Qt, pyqtSignal, QTimer
+from PyQt6.QtGui import QCloseEvent
+
+from devices.base_device import DeviceStatus
+from devices.device_registry import DeviceRegistry
+from core.acquisition import AcquisitionEngine
+
+
+_STATUS_STYLE = {
+ DeviceStatus.CONNECTED: "color:#22c55e;",
+ DeviceStatus.SIMULATED: "color:#3b82f6;",
+ DeviceStatus.DISCONNECTED: "color:#475569;",
+ DeviceStatus.CONNECTING: "color:#f59e0b;",
+ DeviceStatus.ERROR: "color:#ef4444;",
+}
+_STATUS_LABEL = {
+ DeviceStatus.CONNECTED: "CONNECTED",
+ DeviceStatus.SIMULATED: "SIMULATED",
+ DeviceStatus.DISCONNECTED: "OFFLINE",
+ DeviceStatus.CONNECTING: "CONNECTING",
+ DeviceStatus.ERROR: "ERROR",
+}
+
+
+class DeviceCard(QFrame):
+ config_requested = pyqtSignal(str)
+
+ def __init__(self, device):
+ super().__init__()
+ self.device = device
+ self.setObjectName("deviceCard")
+ self._build()
+
+ def _build(self):
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(10, 10, 10, 10)
+ layout.setSpacing(5)
+
+ # Header: icon + name + status dot
+ hdr = QHBoxLayout()
+ icon_lbl = QLabel(self.device.info.icon)
+ icon_lbl.setObjectName("deviceIcon")
+ hdr.addWidget(icon_lbl)
+
+ name_lbl = QLabel(self.device.info.name)
+ name_lbl.setObjectName("deviceName")
+ hdr.addWidget(name_lbl, 1)
+
+ self._dot = QLabel("●")
+ self._dot.setStyleSheet(
+ _STATUS_STYLE.get(self.device.status, "color:#475569;") + " font-size:10px;"
+ )
+ hdr.addWidget(self._dot)
+ layout.addLayout(hdr)
+
+ # ID
+ id_lbl = QLabel(self.device.info.device_id)
+ id_lbl.setObjectName("deviceSub")
+ layout.addWidget(id_lbl)
+
+ # Status text
+ self._status_lbl = QLabel(_STATUS_LABEL.get(self.device.status, "UNKNOWN"))
+ self._apply_status_style()
+ layout.addWidget(self._status_lbl)
+
+ # Channel count · type
+ n = sum(1 for c in self.device.info.channels if c.enabled)
+ ch_lbl = QLabel(f"{n} ch · {self.device.info.device_type}")
+ ch_lbl.setObjectName("deviceChannelCount")
+ layout.addWidget(ch_lbl)
+
+ # Configure button
+ cfg = QPushButton("Configure")
+ cfg.setObjectName("configButton")
+ cfg.clicked.connect(lambda: self.config_requested.emit(self.device.info.device_id))
+ layout.addWidget(cfg)
+
+ def refresh(self):
+ dot_style = _STATUS_STYLE.get(self.device.status, "color:#475569;") + " font-size:10px;"
+ self._dot.setStyleSheet(dot_style)
+ self._status_lbl.setText(_STATUS_LABEL.get(self.device.status, "UNKNOWN"))
+ self._apply_status_style()
+
+ def _apply_status_style(self):
+ s = _STATUS_STYLE.get(self.device.status, "color:#475569;")
+ self._status_lbl.setStyleSheet(
+ s + " font-family:'IBM Plex Mono',monospace;"
+ " font-size:10px; font-weight:700; letter-spacing:1px;"
+ )
+
+
+class DevicesWindow(QWidget):
+ """
+ Standalone floating window for device management.
+
+ Signals
+ -------
+ config_requested(device_id) — user clicked Configure on a card
+ device_added() — a new device was added via the dialog
+ closed() — window was closed (so toolbar btn can untoggle)
+ """
+
+ config_requested = pyqtSignal(str)
+ device_added = pyqtSignal()
+ closed = pyqtSignal()
+
+ def __init__(self, registry: DeviceRegistry, engine: AcquisitionEngine, parent=None):
+ super().__init__(
+ parent,
+ # Acts like a real standalone window but stays on top of parent
+ Qt.WindowType.Window |
+ Qt.WindowType.Tool,
+ )
+ self.registry = registry
+ self.engine = engine
+ self._cards: dict = {}
+
+ self.setWindowTitle("Devices")
+ self.setMinimumSize(300, 480)
+ self.resize(320, 600)
+ self._build()
+ self.refresh()
+
+ self._timer = QTimer(self)
+ self._timer.setInterval(2000)
+ self._timer.timeout.connect(self._refresh_status)
+ self._timer.start()
+
+ # ── Build ─────────────────────────────────────────────────────────────
+
+ def _build(self):
+ root = QVBoxLayout(self)
+ root.setContentsMargins(0, 0, 0, 0)
+ root.setSpacing(0)
+
+ # ── Title bar row ──────────────────────────────────────────────
+ title_bar = QWidget()
+ title_bar.setObjectName("devWindowTitleBar")
+ title_bar.setFixedHeight(40)
+ tb_lay = QHBoxLayout(title_bar)
+ tb_lay.setContentsMargins(12, 0, 8, 0)
+
+ title_lbl = QLabel("⚙ DEVICES")
+ title_lbl.setObjectName("devWindowTitle")
+ tb_lay.addWidget(title_lbl, 1)
+
+ self._add_btn = QPushButton("+ Add")
+ self._add_btn.setObjectName("addDeviceButton")
+ self._add_btn.clicked.connect(self._on_add)
+ tb_lay.addWidget(self._add_btn)
+
+ root.addWidget(title_bar)
+
+ # ── Divider ────────────────────────────────────────────────────
+ div = QFrame()
+ div.setFrameShape(QFrame.Shape.HLine)
+ div.setObjectName("devWindowDivider")
+ root.addWidget(div)
+
+ # ── Scrollable device cards ────────────────────────────────────
+ scroll = QScrollArea()
+ scroll.setObjectName("deviceScroll")
+ scroll.setWidgetResizable(True)
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
+
+ self._container = QWidget()
+ self._inner = QVBoxLayout(self._container)
+ self._inner.setContentsMargins(10, 10, 10, 10)
+ self._inner.setSpacing(10)
+ self._inner.addStretch()
+
+ scroll.setWidget(self._container)
+ root.addWidget(scroll)
+
+ # ── Public API ────────────────────────────────────────────────────────
+
+ def refresh(self):
+ """Rebuild all device cards from current registry state."""
+ for card in self._cards.values():
+ self._inner.removeWidget(card)
+ card.deleteLater()
+ self._cards.clear()
+
+ for dev in self.registry.all_instances():
+ card = DeviceCard(dev)
+ card.config_requested.connect(self.config_requested.emit)
+ self._inner.insertWidget(self._inner.count() - 1, card)
+ self._cards[dev.info.device_id] = card
+
+ # ── Slots ────────────────────────────────────────────────────────────
+
+ def _on_add(self):
+ from ui.add_device_dialog import AddDeviceDialog
+ from ui.config_dialog import DeviceConfigDialog
+ dlg = AddDeviceDialog(self.registry, self)
+ if dlg.exec():
+ dev = dlg.created_device
+ if dev:
+ dev.connect()
+ self.registry.add_instance(dev)
+ self.engine.add_device(dev)
+ self.refresh()
+ self.device_added.emit()
+
+ def _refresh_status(self):
+ for card in self._cards.values():
+ card.refresh()
+
+ def closeEvent(self, event: QCloseEvent):
+ self.closed.emit()
+ event.accept()