added camera search and select, auto preference saving and version tag to 0.4.2

This commit is contained in:
ykcheungab
2026-07-15 15:08:59 +08:00
parent 7b12d17595
commit 11a747a771
9 changed files with 525 additions and 18 deletions
+175 -7
View File
@@ -43,6 +43,8 @@ class SettingsPanelWidget(QWidget):
browse_finger_assets_requested = pyqtSignal()
browse_csv_dir_requested = pyqtSignal()
csv_record_toggled = pyqtSignal(bool)
camera_refresh_requested = pyqtSignal()
camera_selection_changed = pyqtSignal(str)
def __init__(
self,
@@ -105,9 +107,16 @@ class SettingsPanelWidget(QWidget):
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.camera_combo = QComboBox()
self.camera_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.camera_combo.setToolTip("Select an available camera")
self.camera_combo.currentIndexChanged.connect(self._on_camera_combo_changed)
video_row.addWidget(self.camera_combo, 1)
self.btn_refresh_cameras = QPushButton("Refresh")
self.btn_refresh_cameras.setFixedWidth(64)
self.btn_refresh_cameras.setToolTip("Re-scan available cameras")
self.btn_refresh_cameras.clicked.connect(self.camera_refresh_requested.emit)
video_row.addWidget(self.btn_refresh_cameras)
self.btn_browse_video = QPushButton("…")
self.btn_browse_video.setFixedWidth(32)
self.btn_browse_video.setToolTip("Browse video file")
@@ -115,6 +124,16 @@ class SettingsPanelWidget(QWidget):
video_row.addWidget(self.btn_browse_video)
root.addLayout(video_row)
self.video_file_label = QLabel("")
self.video_file_label.setObjectName("settingsCaption")
self.video_file_label.setWordWrap(True)
self.video_file_label.hide()
root.addWidget(self.video_file_label)
self._source_mode = "camera" # "camera" | "file"
self._video_file_path = ""
self._updating_camera_options = False
root.addWidget(self._caption("Config"))
config_row = QHBoxLayout()
config_row.setSpacing(6)
@@ -218,8 +237,103 @@ class SettingsPanelWidget(QWidget):
style.polish(btn)
btn.update()
def set_camera_options(self, cameras, selected_value: str | None = None) -> None:
"""Populate camera combo from discovered cameras.
``cameras`` items may be objects with ``.source`` / ``.label()`` or
``(source, label)`` tuples.
"""
self._updating_camera_options = True
self.camera_combo.blockSignals(True)
self.camera_combo.clear()
sources: list[str] = []
for cam in cameras:
if hasattr(cam, "source") and callable(getattr(cam, "label", None)):
source = str(cam.source)
label = str(cam.label())
else:
source, label = cam
source = str(source)
label = str(label)
self.camera_combo.addItem(label, source)
sources.append(source)
preferred = (selected_value or "").strip()
idx = self.camera_combo.findData(preferred) if preferred else -1
if idx < 0 and self.camera_combo.count() > 0:
idx = 0
if idx >= 0:
self.camera_combo.setCurrentIndex(idx)
# File mode is cleared only when we intentionally select a camera.
if self._source_mode != "file":
self._source_mode = "camera"
self._video_file_path = ""
self.video_file_label.hide()
self.camera_combo.setEnabled(self.camera_combo.count() > 0)
self.camera_combo.blockSignals(False)
self._updating_camera_options = False
def set_video_source(self, value: str) -> None:
self.video_edit.setText(value)
"""Set current video source (camera index string or file path)."""
value = (value or "").strip()
if not value:
return
# Numeric camera index → select in combo if present; else keep as preferred text via adding.
if value.isdigit() or (value.startswith("-") and value[1:].isdigit()):
idx = self.camera_combo.findData(value)
if idx >= 0:
self._source_mode = "camera"
self._video_file_path = ""
self.video_file_label.hide()
self.camera_combo.blockSignals(True)
self.camera_combo.setCurrentIndex(idx)
self.camera_combo.blockSignals(False)
else:
# Not yet scanned / not present: remember as pending camera selection.
self._source_mode = "camera"
self._video_file_path = ""
self.video_file_label.hide()
return
# Treat as video file path.
self._source_mode = "file"
self._video_file_path = value
self.video_file_label.setText(f"File: {value}")
self.video_file_label.show()
def selected_camera_source(self) -> str:
data = self.camera_combo.currentData()
return str(data) if data is not None else ""
def video_source(self) -> str:
if self._source_mode == "file" and self._video_file_path:
return self._video_file_path
return self.selected_camera_source()
def has_usable_source(self) -> bool:
if self._source_mode == "file" and self._video_file_path:
return True
return bool(self.selected_camera_source())
def is_file_source(self) -> bool:
return self._source_mode == "file" and bool(self._video_file_path)
def set_camera_controls_enabled(self, enabled: bool) -> None:
self.btn_refresh_cameras.setEnabled(enabled)
self.btn_browse_video.setEnabled(enabled)
self.camera_combo.setEnabled(enabled and self.camera_combo.count() > 0)
def _on_camera_combo_changed(self, _index: int) -> None:
if self._updating_camera_options:
return
source = self.selected_camera_source()
if not source:
return
self._source_mode = "camera"
self._video_file_path = ""
self.video_file_label.hide()
self.camera_selection_changed.emit(source)
def set_config_path(self, value: str) -> None:
self.config_edit.setText(value)
@@ -290,9 +404,6 @@ class SettingsPanelWidget(QWidget):
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()
@@ -304,3 +415,60 @@ class SettingsPanelWidget(QWidget):
def csv_save_dir(self) -> str:
return self.csv_dir_edit.text().strip()
def preference_state(self) -> dict:
"""Serialize user-facing settings suitable for preference.json."""
return {
"video_source": self.video_source(),
"config_path": self.config_path(),
"finger_assets_dir": self.finger_export_dir(),
"csv_save_dir": self.csv_save_dir(),
"show_log": bool(self.show_log_check.isChecked()),
"show_flow_arrows": bool(self.show_flow_arrows_check.isChecked()),
"arrow_background": self.arrow_background(),
"free_rotate": bool(self.free_rotate_check.isChecked()),
}
def apply_preference_state(self, state: dict | None) -> None:
"""Restore user-facing settings from preference.json."""
if not isinstance(state, dict) or not state:
return
show_log = state.get("show_log")
if isinstance(show_log, bool):
self.show_log_check.blockSignals(True)
self.show_log_check.setChecked(show_log)
self.show_log_check.blockSignals(False)
show_flow = state.get("show_flow_arrows")
if isinstance(show_flow, bool):
self.show_flow_arrows_check.blockSignals(True)
self.show_flow_arrows_check.setChecked(show_flow)
self.show_flow_arrows_check.blockSignals(False)
self._mesh_state.set_flow_arrows_enabled(show_flow)
arrow_bg = state.get("arrow_background")
if isinstance(arrow_bg, str) and arrow_bg:
self.set_arrow_background(arrow_bg) # type: ignore[arg-type]
free_rotate = state.get("free_rotate")
if isinstance(free_rotate, bool):
self.free_rotate_check.blockSignals(True)
self.free_rotate_check.setChecked(free_rotate)
self.free_rotate_check.blockSignals(False)
config_path = state.get("config_path")
if isinstance(config_path, str) and config_path.strip():
self.set_config_path(config_path.strip())
assets = state.get("finger_assets_dir")
if isinstance(assets, str) and assets.strip():
self.set_finger_export_dir(assets.strip())
csv_dir = state.get("csv_save_dir")
if isinstance(csv_dir, str) and csv_dir.strip():
self.set_csv_save_dir(csv_dir.strip())
video = state.get("video_source")
if isinstance(video, str) and video.strip():
self.set_video_source(video.strip())