diff options
Diffstat (limited to 'ui/devices_window.py')
| -rw-r--r-- | ui/devices_window.py | 224 |
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() |
