""" motion_capture/camera_panel.py CameraPanel for AddDeviceDialog β€” scans for OpenCV cameras and builds a CameraDevice when the user clicks "Add Device". """ from __future__ import annotations from PyQt6.QtCore import Qt, QThread, pyqtSignal from PyQt6.QtWidgets import ( QCheckBox, QGroupBox, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton, QVBoxLayout, QWidget, ) class CameraScanThread(QThread): cameras_found = pyqtSignal(list) # list of (index: int, label: str) def run(self): results = [] try: import cv2 for i in range(6): cap = cv2.VideoCapture(i) if cap.isOpened(): ret, _ = cap.read() if ret: w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) results.append((i, f"Camera {i} ({w}Γ—{h})")) cap.release() except ImportError: pass self.cameras_found.emit(results) class CameraPanel(QWidget): """Config panel for Camera device in AddDeviceDialog.""" def __init__(self): super().__init__() self._cameras: list = [] self._selected_index: int = 0 self._scanner = None lay = QVBoxLayout(self) lay.setContentsMargins(0, 4, 0, 4) lay.setSpacing(8) self.sim_chk = QCheckBox("Simulation mode (Lissajous pattern, no camera needed)") self.sim_chk.setChecked(False) lay.addWidget(self.sim_chk) scan_grp = QGroupBox("Available Cameras") sg = QVBoxLayout(scan_grp) sg.setSpacing(4) top = QHBoxLayout() self._scan_btn = QPushButton("πŸ” Scan Cameras") self._scan_btn.setObjectName("addTraceBtn") self._scan_btn.clicked.connect(self._scan) self._status = QLabel("Click Scan to detect cameras") self._status.setObjectName("traceSource") top.addWidget(self._scan_btn) top.addWidget(self._status, 1) sg.addLayout(top) self._list = QListWidget() self._list.setMaximumHeight(120) self._list.setObjectName("portList") self._list.itemClicked.connect(self._on_select) self._list.setToolTip("Click a camera to select it") sg.addWidget(self._list) hint = QLabel("↑ Click a camera above to select it") hint.setObjectName("traceSource") sg.addWidget(hint) lay.addWidget(scan_grp) note = QLabel( "Requires OpenCV: pip install opencv-python\n" "Use Simulation mode if no camera is available." ) note.setObjectName("traceSource") note.setWordWrap(True) lay.addWidget(note) lay.addStretch() # ── Scan ───────────────────────────────────────────────────────────── def _scan(self): self._scan_btn.setEnabled(False) self._status.setText("Scanning…") self._list.clear() self._scanner = CameraScanThread() self._scanner.cameras_found.connect(self._on_found) self._scanner.start() def _on_found(self, cameras): self._scan_btn.setEnabled(True) self._cameras = cameras self._list.clear() if not cameras: self._status.setText("No cameras found") item = QListWidgetItem(" No cameras detected") item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsSelectable) self._list.addItem(item) else: self._status.setText(f"{len(cameras)} camera(s) found") for idx, label in cameras: item = QListWidgetItem(f" {label}") item.setData(Qt.ItemDataRole.UserRole, idx) self._list.addItem(item) self._list.setCurrentRow(0) self._selected_index = cameras[0][0] def _on_select(self, item: QListWidgetItem): idx = item.data(Qt.ItemDataRole.UserRole) if idx is not None: self._selected_index = idx # ── Build ───────────────────────────────────────────────────────────── def build_device(self, device_id: str): from device import CameraDevice return CameraDevice( device_id=device_id, camera_index=self._selected_index, simulate=self.sim_chk.isChecked(), )