"""Settings side panel for the finger dashboard.""" from __future__ import annotations from PyQt5.QtCore import QSize, Qt, pyqtSignal from PyQt5.QtWidgets import ( QCheckBox, QComboBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QSizePolicy, QVBoxLayout, QWidget, ) from orisys.finger import DEFAULT_FINGER_EXPORT_DIR from viz.render_2d import ArrowBackground from ..layout import DashboardLayout, LAYOUT from ..mesh_view_state import MeshViewState from ..transport_icons import reset_icon, transport_icon _PLAY_PAUSE_BUTTON_NAME = "btnPlayPause" _PLAY_PAUSE_ICON_PX = 18 _PLAY_PAUSE_ICON_COLORS = { "run": "#4caf84", "starting": "#555555", "pause": "#ff9800", } class SettingsPanelWidget(QWidget): """Right-side settings: log, sources, and 3D view controls.""" log_visibility_changed = pyqtSignal(bool) free_rotation_changed = pyqtSignal(bool) arrow_background_changed = pyqtSignal(str) view_reset_requested = pyqtSignal() browse_video_requested = pyqtSignal() browse_config_requested = pyqtSignal() browse_finger_assets_requested = pyqtSignal() def __init__( self, parent=None, *, layout: DashboardLayout = LAYOUT, mesh_state: MeshViewState | None = None, show_log: bool = True, ): super().__init__(parent) self._layout = layout self._mesh_state = mesh_state or MeshViewState.from_layout(layout) self.setObjectName("settingsPanel") self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) root = QVBoxLayout(self) root.setContentsMargins(10, 10, 10, 10) root.setSpacing(10) title = QLabel(layout.settings_panel_title) title.setObjectName("settingsTitle") root.addWidget(title) control_row = QHBoxLayout() control_row.setSpacing(8) self.btn_play_pause = QPushButton() self.btn_play_pause.setObjectName(_PLAY_PAUSE_BUTTON_NAME) self.btn_play_pause.setFixedSize(36, 32) self.btn_play_pause.setIconSize(QSize(_PLAY_PAUSE_ICON_PX, _PLAY_PAUSE_ICON_PX)) control_row.addWidget(self.btn_play_pause) self.btn_reset_tracker = QPushButton("Reset Sensor") self.btn_reset_tracker.setToolTip("Reset tracker") self.btn_reset_tracker.setEnabled(False) control_row.addWidget(self.btn_reset_tracker, 1) root.addLayout(control_row) self.show_log_check = QCheckBox("Show log panel") self.show_log_check.setChecked(show_log) self.show_log_check.toggled.connect(self.log_visibility_changed.emit) root.addWidget(self.show_log_check) self.show_flow_arrows_check = QCheckBox("Show displacement arrows") self.show_flow_arrows_check.setChecked(self._mesh_state.flow_arrows_enabled) self.show_flow_arrows_check.toggled.connect(self._mesh_state.set_flow_arrows_enabled) root.addWidget(self.show_flow_arrows_check) root.addWidget(self._caption("UV arrow background")) self.arrow_background_combo = QComboBox() for label, value in ( ("Unrolled", "unrolled"), ("Pure color", "pure_color"), ): self.arrow_background_combo.addItem(label, value) self.set_arrow_background(layout.arrow_background) self.arrow_background_combo.currentIndexChanged.connect(self._emit_arrow_background_changed) root.addWidget(self.arrow_background_combo) root.addWidget(self._caption("Video source")) video_row = QHBoxLayout() video_row.setSpacing(6) self.video_edit = QLineEdit("0") self.video_edit.setPlaceholderText("Camera ID or video path") video_row.addWidget(self.video_edit, 1) self.btn_browse_video = QPushButton("…") self.btn_browse_video.setFixedWidth(32) self.btn_browse_video.setToolTip("Browse video file") self.btn_browse_video.clicked.connect(self.browse_video_requested.emit) video_row.addWidget(self.btn_browse_video) root.addLayout(video_row) root.addWidget(self._caption("Config")) config_row = QHBoxLayout() config_row.setSpacing(6) self.config_edit = QLineEdit("./config/dd02-ov.json") config_row.addWidget(self.config_edit, 1) self.btn_browse_config = QPushButton("…") self.btn_browse_config.setFixedWidth(32) self.btn_browse_config.setToolTip("Browse config JSON") self.btn_browse_config.clicked.connect(self.browse_config_requested.emit) config_row.addWidget(self.btn_browse_config) root.addLayout(config_row) self.cal_edit = QLineEdit("") self.cal_edit.setPlaceholderText("Optional calibration path") root.addWidget(self._caption("Calibration")) root.addWidget(self.cal_edit) root.addWidget(self._caption("Finger assets")) finger_assets_row = QHBoxLayout() finger_assets_row.setSpacing(6) self.finger_assets_edit = QLineEdit(DEFAULT_FINGER_EXPORT_DIR) self.finger_assets_edit.setPlaceholderText("Blender export bundle directory") finger_assets_row.addWidget(self.finger_assets_edit, 1) self.btn_browse_finger_assets = QPushButton("…") self.btn_browse_finger_assets.setFixedWidth(32) self.btn_browse_finger_assets.setToolTip("Browse finger asset directory") self.btn_browse_finger_assets.clicked.connect(self.browse_finger_assets_requested.emit) finger_assets_row.addWidget(self.btn_browse_finger_assets) root.addLayout(finger_assets_row) root.addSpacing(8) rotate_row = QHBoxLayout() rotate_row.setSpacing(6) self.free_rotate_check = QCheckBox("Free rotate 3D view") self.free_rotate_check.toggled.connect(self.free_rotation_changed.emit) rotate_row.addWidget(self.free_rotate_check, 1) self.btn_reset_view = QPushButton() self.btn_reset_view.setObjectName("btnResetView") self.btn_reset_view.setToolTip("Reset 3D mesh camera") self.btn_reset_view.setIcon(reset_icon("#999999")) self.btn_reset_view.setIconSize(QSize(14, 14)) self.btn_reset_view.setFixedSize(28, 28) self.btn_reset_view.clicked.connect(self.view_reset_requested.emit) rotate_row.addWidget(self.btn_reset_view) root.addLayout(rotate_row) root.addStretch() @staticmethod def _caption(text: str) -> QLabel: label = QLabel(text) label.setObjectName("settingsCaption") return label def set_panel_size(self, width: int, height: int) -> None: self.setFixedSize(max(int(width), 64), max(int(height), 64)) def set_play_pause_state(self, state: str) -> None: """``run`` = green play, ``starting`` = grey disabled play, ``pause`` = orange pause.""" btn = self.btn_play_pause if state == "run": btn.setToolTip("Run") btn.setEnabled(True) elif state == "starting": btn.setToolTip("Starting…") btn.setEnabled(False) elif state == "pause": btn.setToolTip("Pause") btn.setEnabled(True) else: raise ValueError(f"unknown play/pause state: {state!r}") kind = "pause" if state == "pause" else "play" btn.setText("") btn.setIcon(transport_icon(kind, _PLAY_PAUSE_ICON_COLORS[state])) btn.setProperty("playState", state) style = btn.style() style.unpolish(btn) style.polish(btn) btn.update() def set_video_source(self, value: str) -> None: self.video_edit.setText(value) def set_config_path(self, value: str) -> None: self.config_edit.setText(value) def set_calibration_path(self, value: str) -> None: self.cal_edit.setText(value or "") def set_finger_export_dir(self, value: str) -> None: self.finger_assets_edit.setText(value) def set_finger_export_dir_enabled(self, enabled: bool) -> None: self.finger_assets_edit.setEnabled(enabled) self.btn_browse_finger_assets.setEnabled(enabled) def _emit_arrow_background_changed(self, _index: int) -> None: value = self.arrow_background_combo.currentData() if value: self.arrow_background_changed.emit(value) def set_arrow_background(self, value: ArrowBackground) -> None: idx = self.arrow_background_combo.findData(value) if idx < 0: return self.arrow_background_combo.blockSignals(True) self.arrow_background_combo.setCurrentIndex(idx) self.arrow_background_combo.blockSignals(False) def arrow_background(self) -> ArrowBackground: value = self.arrow_background_combo.currentData() return value if value else "unrolled" def video_source(self) -> str: return self.video_edit.text().strip() def config_path(self) -> str: return self.config_edit.text().strip() def calibration_path(self) -> str: return self.cal_edit.text().strip() def finger_export_dir(self) -> str: return self.finger_assets_edit.text().strip()