"""Camera Settings dialog — sliders for UVC controls + Qt WhiteBalance/Exposure.""" from __future__ import annotations import logging from PySide6.QtCore import Qt from PySide6.QtMultimedia import QCamera from PySide6.QtWidgets import ( QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QGroupBox, QHBoxLayout, QLabel, QScrollArea, QSlider, QSpinBox, QVBoxLayout, QWidget, ) from app.camera.uvc.base import UvcControllerBase, UvcParam logger = logging.getLogger(__name__) # Human-readable labels for each UVC parameter _PARAM_LABELS: dict[UvcParam, str] = { UvcParam.BRIGHTNESS: "Brightness", UvcParam.CONTRAST: "Contrast", UvcParam.SATURATION: "Saturation", UvcParam.HUE: "Hue", UvcParam.SHARPNESS: "Sharpness", UvcParam.GAMMA: "Gamma", UvcParam.WHITE_BALANCE: "White Balance (K)", UvcParam.BACKLIGHT_COMPENSATION: "Backlight Compensation", UvcParam.EXPOSURE: "Exposure Time", } # Qt WhiteBalance modes shown in the combo _WB_MODES: list[tuple[str, QCamera.WhiteBalanceMode]] = [ ("Auto", QCamera.WhiteBalanceMode.WhiteBalanceAuto), ("Sunlight", QCamera.WhiteBalanceMode.WhiteBalanceSunlight), ("Cloudy", QCamera.WhiteBalanceMode.WhiteBalanceCloudy), ("Shade", QCamera.WhiteBalanceMode.WhiteBalanceShade), ("Tungsten", QCamera.WhiteBalanceMode.WhiteBalanceTungsten), ("Fluorescent", QCamera.WhiteBalanceMode.WhiteBalanceFluorescent), ("Flash", QCamera.WhiteBalanceMode.WhiteBalanceFlash), ("Sunset", QCamera.WhiteBalanceMode.WhiteBalanceSunset), ("Manual (K)", QCamera.WhiteBalanceMode.WhiteBalanceManual), ] _EXPOSURE_MODES: list[tuple[str, QCamera.ExposureMode]] = [ ("Auto", QCamera.ExposureMode.ExposureAuto), ("Manual", QCamera.ExposureMode.ExposureManual), ] class CameraSettingsDialog(QDialog): """ Modal dialog for camera image controls. Sections: • Qt controls — WhiteBalance mode + colour temperature, Exposure mode + time • UVC controls — sliders for Brightness, Contrast, Saturation, Hue, Sharpness, Gamma, White Balance (manual K), Backlight, Exposure (if UVC controller is open) Changes are applied live to the camera. """ def __init__( self, camera: QCamera, uvc: UvcControllerBase, parent: QWidget | None = None, ) -> None: super().__init__(parent) self.setWindowTitle("Camera Settings") self.setMinimumWidth(440) self._camera = camera self._uvc = uvc outer = QVBoxLayout(self) scroll = QScrollArea() scroll.setWidgetResizable(True) outer.addWidget(scroll) content = QWidget() scroll.setWidget(content) layout = QVBoxLayout(content) layout.setAlignment(Qt.AlignmentFlag.AlignTop) layout.addWidget(self._build_qt_group()) uvc_group = self._build_uvc_group() if uvc_group is not None: layout.addWidget(uvc_group) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) buttons.rejected.connect(self.accept) outer.addWidget(buttons) # ------------------------------------------------------------------ # Qt controls section # ------------------------------------------------------------------ def _build_qt_group(self) -> QGroupBox: group = QGroupBox("Qt Camera Controls") form = QFormLayout(group) # White Balance mode self._wb_combo = QComboBox() for label, mode in _WB_MODES: self._wb_combo.addItem(label, mode) current_wb = self._camera.whiteBalanceMode() for i, (_, mode) in enumerate(_WB_MODES): if mode == current_wb: self._wb_combo.setCurrentIndex(i) break self._wb_combo.currentIndexChanged.connect(self._on_wb_mode_changed) form.addRow("White Balance:", self._wb_combo) # Colour temperature (manual only) self._temp_label = QLabel("Colour Temp (K):") self._temp_spin = QSpinBox() self._temp_spin.setRange(2000, 10000) self._temp_spin.setSingleStep(100) current_temp = self._camera.colorTemperature() self._temp_spin.setValue(current_temp if current_temp >= 2000 else 5500) self._temp_spin.valueChanged.connect(self._on_color_temp_changed) form.addRow(self._temp_label, self._temp_spin) self._update_temp_visibility() form.addRow(QLabel("")) # spacer # Exposure mode self._exp_combo = QComboBox() for label, mode in _EXPOSURE_MODES: self._exp_combo.addItem(label, mode) current_exp = self._camera.exposureMode() for i, (_, mode) in enumerate(_EXPOSURE_MODES): if mode == current_exp: self._exp_combo.setCurrentIndex(i) break self._exp_combo.currentIndexChanged.connect(self._on_exp_mode_changed) form.addRow("Exposure Mode:", self._exp_combo) # Manual exposure time self._exp_label = QLabel("Exposure Time (s):") self._exp_spin = QDoubleSpinBox() exp_min = self._camera.minimumExposureTime() exp_max = self._camera.maximumExposureTime() self._exp_spin.setRange( exp_min if exp_min > 0 else 1e-6, exp_max if exp_max > 0 else 1.0, ) self._exp_spin.setDecimals(6) self._exp_spin.setSingleStep(0.001) manual_t = self._camera.manualExposureTime() self._exp_spin.setValue(manual_t if manual_t > 0 else 0.033) self._exp_spin.valueChanged.connect(self._on_exp_time_changed) form.addRow(self._exp_label, self._exp_spin) self._update_exp_visibility() return group # ------------------------------------------------------------------ # UVC controls section # ------------------------------------------------------------------ def _build_uvc_group(self) -> QGroupBox | None: params = self._uvc.get_all_params() supported = [p for p in params if p.supported] group = QGroupBox("UVC Camera Controls") form = QFormLayout(group) if not supported: note = QLabel( "UVC controls not available.\n" "Install duvc-ctl (Windows) or pyuvc (macOS) to enable." ) note.setEnabled(False) form.addRow(note) return group self._uvc_sliders: dict[UvcParam, QSlider] = {} self._uvc_spins: dict[UvcParam, QSpinBox] = {} self._uvc_auto_boxes: dict[UvcParam, QCheckBox] = {} for info in params: label = _PARAM_LABELS.get(info.param, info.param.name.title()) if not info.supported: lbl = QLabel("Not supported") lbl.setEnabled(False) form.addRow(f"{label}:", lbl) continue row_widget = QWidget() row_layout = QHBoxLayout(row_widget) row_layout.setContentsMargins(0, 0, 0, 0) slider = QSlider(Qt.Orientation.Horizontal) slider.setRange(info.minimum, info.maximum) slider.setValue(info.current) slider.setSingleStep(max(1, info.step)) spin = QSpinBox() spin.setRange(info.minimum, info.maximum) spin.setValue(info.current) spin.setFixedWidth(75) # Keep slider ↔ spin in sync slider.valueChanged.connect(spin.setValue) spin.valueChanged.connect(slider.setValue) slider.valueChanged.connect( lambda v, p=info.param: self._on_uvc_value(p, v) ) row_layout.addWidget(slider) row_layout.addWidget(spin) if info.auto_supported: auto_box = QCheckBox("Auto") auto_box.setChecked(info.auto_enabled) auto_box.toggled.connect( lambda checked, p=info.param: self._on_uvc_auto(p, checked) ) row_layout.addWidget(auto_box) self._uvc_auto_boxes[info.param] = auto_box # Init enabled state slider.setEnabled(not info.auto_enabled) spin.setEnabled(not info.auto_enabled) self._uvc_sliders[info.param] = slider self._uvc_spins[info.param] = spin form.addRow(f"{label}:", row_widget) return group # ------------------------------------------------------------------ # Qt control slots # ------------------------------------------------------------------ def _on_wb_mode_changed(self, index: int) -> None: mode: QCamera.WhiteBalanceMode = self._wb_combo.itemData(index) if self._camera.isWhiteBalanceModeSupported(mode): self._camera.setWhiteBalanceMode(mode) logger.debug("WB mode: %s", mode) else: logger.debug("WB mode %s not supported by this camera", mode) self._update_temp_visibility() def _on_color_temp_changed(self, value: int) -> None: self._camera.setColorTemperature(value) logger.debug("Colour temp: %d K", value) def _on_exp_mode_changed(self, index: int) -> None: mode: QCamera.ExposureMode = self._exp_combo.itemData(index) if self._camera.isExposureModeSupported(mode): self._camera.setExposureMode(mode) logger.debug("Exposure mode: %s", mode) self._update_exp_visibility() def _on_exp_time_changed(self, value: float) -> None: self._camera.setManualExposureTime(value) logger.debug("Exposure time: %.6f s", value) def _update_temp_visibility(self) -> None: manual = ( self._wb_combo.currentData() == QCamera.WhiteBalanceMode.WhiteBalanceManual ) self._temp_label.setVisible(manual) self._temp_spin.setVisible(manual) def _update_exp_visibility(self) -> None: manual = ( self._exp_combo.currentData() == QCamera.ExposureMode.ExposureManual ) self._exp_label.setVisible(manual) self._exp_spin.setVisible(manual) # ------------------------------------------------------------------ # UVC control slots # ------------------------------------------------------------------ def _on_uvc_value(self, param: UvcParam, value: int) -> None: self._uvc.set_value(param, value) def _on_uvc_auto(self, param: UvcParam, enabled: bool) -> None: self._uvc.set_auto(param, enabled) slider = self._uvc_sliders.get(param) spin = self._uvc_spins.get(param) if slider: slider.setEnabled(not enabled) if spin: spin.setEnabled(not enabled)