""" motion_capture/camera_panel.py CameraPanel for AddDeviceDialog. Camera discovery and simulation toggle are handled by AddDeviceDialog. CameraScanThread is kept here and used by the unified scanner. """ from __future__ import annotations from typing import Optional, Tuple from PyQt6.QtCore import QThread, pyqtSignal from PyQt6.QtWidgets import QComboBox, QFormLayout, QLabel, QVBoxLayout, QWidget _RESOLUTIONS = { "Default": None, "640 × 480": (640, 480), "1280 × 720": (1280, 720), "1920 × 1080": (1920, 1080), "2560 × 1440": (2560, 1440), } _FPS = { "Default": None, "15 fps": 15, "24 fps": 24, "30 fps": 30, "60 fps": 60, "120 fps": 120, } 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._selected_index: int = 0 self._simulate: bool = False lay = QVBoxLayout(self) lay.setContentsMargins(0, 4, 0, 4) lay.setSpacing(8) self._selected_lbl = QLabel("No camera selected — scan above and click a camera") self._selected_lbl.setObjectName("traceSource") self._selected_lbl.setWordWrap(True) lay.addWidget(self._selected_lbl) form = QFormLayout() form.setContentsMargins(0, 0, 0, 0) self.res_cb = QComboBox() self.res_cb.addItems(list(_RESOLUTIONS.keys())) self.res_cb.setCurrentText("1280 × 720") form.addRow("Resolution:", self.res_cb) self.fps_cb = QComboBox() self.fps_cb.addItems(list(_FPS.keys())) self.fps_cb.setCurrentText("30 fps") form.addRow("Frame Rate:", self.fps_cb) lay.addLayout(form) 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() def is_valid(self) -> bool: return True def set_simulate(self, simulate: bool): self._simulate = simulate def set_camera_index(self, index: int): self._selected_index = index self._selected_lbl.setText(f"Selected: Camera {index}") def build_device(self, device_id: str): from device import CameraDevice resolution: Optional[Tuple[int, int]] = _RESOLUTIONS.get( self.res_cb.currentText() ) fps: Optional[int] = _FPS.get(self.fps_cb.currentText()) return CameraDevice( device_id=device_id, camera_index=self._selected_index, simulate=self._simulate, resolution=resolution, fps=fps, )