added camera search and select, auto preference saving and version tag to 0.4.2
This commit is contained in:
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user