Skip to content

Commit 05cff1e

Browse files
committed
Cache camera scans and add settings setter
Introduce caching for camera discovery to avoid unnecessary rescans by adding _last_scan_backend and _has_scan_results and a _maybe_refresh_available_cameras(force=False) helper that only calls _refresh_available_cameras when backend or cache state requires it. Refactor population logic: add _populate_active_list_from_working to fill the active cameras list (preserving selection optionally) and simplify _populate_from_settings to use it. Add public set_settings(...) to update the dialog with new MultiCameraSettings and optional dlc_camera_id without destroying unsaved UI state. Improve scan worker cleanup logic and add a debug message to clarify deferred cleanup. Update backend-change handling to invalidate cache, and update scan result handlers to set cache flags appropriately. Minor UI tweaks: remove a stray currentRow() call, ensure button states are updated, and switch main window to call set_settings(...) when reusing the dialog so the dialog manages its own refresh behavior.
1 parent 851efb7 commit 05cff1e

File tree

2 files changed

+75
-18
lines changed

2 files changed

+75
-18
lines changed

dlclivegui/gui/camera_config/camera_config_dialog.py

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ def __init__(
7070
self._settings_scroll: QScrollArea | None = None
7171
self._settings_scroll_contents: QWidget | None = None
7272

73+
# Scan cache
74+
self._last_scan_backend: str | None = None
75+
self._has_scan_results: bool = False
76+
7377
self._setup_ui()
7478
self._connect_signals()
7579
self._populate_from_settings()
@@ -108,9 +112,11 @@ def showEvent(self, event):
108112
self._working_settings = self._multi_camera_settings.model_copy(deep=True)
109113
self._current_edit_index = None
110114

111-
self._populate_from_settings()
115+
self._populate_active_list_from_working(keep_selection=True)
112116
self._post_init_sync_selection()
113117

118+
self._maybe_refresh_available_cameras(force=False)
119+
114120
# Maintain overlay geometry when resizing
115121
def resizeEvent(self, event):
116122
super().resizeEvent(event)
@@ -207,8 +213,11 @@ def _on_close_cleanup(self) -> None:
207213
self._set_scan_state(CameraScanState.CANCELING, message="Canceling discovery…")
208214
sw.wait(300)
209215
if sw.isRunning():
210-
return # Let finished() handle cleanup
211-
if self._scan_worker and not self._scan_worker.isRunning():
216+
LOGGER.debug("Cleanup: scan worker still running; deferring worker cleanup to finished()")
217+
elif self._scan_worker is sw:
218+
# Worker has stopped; safe to perform scan-worker-specific cleanup now
219+
self._cleanup_scan_worker()
220+
elif self._scan_worker and not self._scan_worker.isRunning():
212221
self._cleanup_scan_worker()
213222

214223
# Cancel probe worker
@@ -369,7 +378,6 @@ def _update_button_states(self) -> None:
369378

370379
self.preview_btn.setEnabled(has_active_selection or self._preview.state == PreviewState.LOADING)
371380

372-
self.available_cameras_list.currentRow()
373381
self.add_camera_btn.setEnabled((self._selected_detected_camera() is not None) and not scan_running)
374382

375383
def _sync_preview_ui(self) -> None:
@@ -522,6 +530,8 @@ def _append_status(self, text: str) -> None:
522530
# Camera discovery and probing
523531
# -------------------------------
524532
def _on_backend_changed(self, _index: int) -> None:
533+
self._has_scan_results = False
534+
self._last_scan_backend = None
525535
self._refresh_available_cameras()
526536

527537
def _is_scan_running(self) -> bool:
@@ -628,6 +638,8 @@ def _on_scan_result(self, cams: list) -> None:
628638
# Apply results to UI first (stability guarantee)
629639
self._detected_cameras = cams or []
630640
self.available_cameras_list.clear()
641+
self._has_scan_results = True
642+
self._last_scan_backend = self._current_backend_key()
631643

632644
if not self._detected_cameras:
633645
placeholder = QListWidgetItem("No cameras detected.")
@@ -657,6 +669,8 @@ def _on_scan_error(self, msg: str) -> None:
657669
placeholder = QListWidgetItem("Scan failed.")
658670
placeholder.setFlags(Qt.ItemIsEnabled)
659671
self.available_cameras_list.addItem(placeholder)
672+
self._has_scan_results = False
673+
self._last_scan_backend = self._current_backend_key()
660674

661675
self._finish_scan("error")
662676

@@ -678,6 +692,8 @@ def request_scan_cancel(self) -> None:
678692
placeholder = QListWidgetItem("Scan canceled.")
679693
placeholder.setFlags(Qt.ItemIsEnabled)
680694
self.available_cameras_list.addItem(placeholder)
695+
self._has_scan_results = True # keep any results that did arrive, even if cancel requested
696+
self._last_scan_backend = self._current_backend_key()
681697

682698
if w is None or not w.isRunning():
683699
self._finish_scan("cancel")
@@ -1096,21 +1112,65 @@ def _clear_settings_form(self) -> None:
10961112
self.apply_settings_btn.setEnabled(False)
10971113
self.reset_settings_btn.setEnabled(False)
10981114

1115+
def set_settings(self, settings: MultiCameraSettings, *, dlc_camera_id: str | None = None) -> None:
1116+
self._multi_camera_settings = settings or MultiCameraSettings(cameras=[])
1117+
self._working_settings = self._multi_camera_settings.model_copy(deep=True)
1118+
self._current_edit_index = None
1119+
1120+
if dlc_camera_id is not None:
1121+
self.dlc_camera_id = dlc_camera_id
1122+
1123+
self._populate_active_list_from_working(keep_selection=True)
1124+
self._post_init_sync_selection()
1125+
self._maybe_refresh_available_cameras()
1126+
10991127
def _populate_from_settings(self) -> None:
11001128
"""Populate the dialog from existing settings."""
1101-
self.active_cameras_list.clear()
1102-
for i, cam in enumerate(self._working_settings.cameras):
1103-
item = QListWidgetItem(self._format_camera_label(cam, i))
1104-
item.setData(Qt.ItemDataRole.UserRole, cam)
1105-
if not cam.enabled:
1106-
item.setForeground(Qt.GlobalColor.gray)
1107-
self.active_cameras_list.addItem(item)
1129+
self._populate_active_list_from_working(keep_selection=True)
1130+
self._update_button_states()
1131+
1132+
def _populate_active_list_from_working(self, *, keep_selection: bool = True) -> None:
1133+
"""Populate only the active cameras list from _working_settings (no scanning)."""
1134+
prev_row = self.active_cameras_list.currentRow() if keep_selection else -1
11081135

1136+
self.active_cameras_list.blockSignals(True)
1137+
try:
1138+
self.active_cameras_list.clear()
1139+
for i, cam in enumerate(self._working_settings.cameras):
1140+
item = QListWidgetItem(self._format_camera_label(cam, i))
1141+
item.setData(Qt.ItemDataRole.UserRole, cam)
1142+
if not cam.enabled:
1143+
item.setForeground(Qt.GlobalColor.gray)
1144+
self.active_cameras_list.addItem(item)
1145+
finally:
1146+
self.active_cameras_list.blockSignals(False)
1147+
1148+
# restore selection if possible
11091149
if self.active_cameras_list.count() > 0:
1110-
self.active_cameras_list.setCurrentRow(0)
1150+
if keep_selection and 0 <= prev_row < self.active_cameras_list.count():
1151+
self.active_cameras_list.setCurrentRow(prev_row)
1152+
else:
1153+
self.active_cameras_list.setCurrentRow(0)
11111154

1112-
self._refresh_available_cameras()
1113-
self._update_button_states()
1155+
def _current_backend_key(self) -> str:
1156+
return (self.backend_combo.currentData() or self.backend_combo.currentText().split()[0] or "opencv").lower()
1157+
1158+
def _maybe_refresh_available_cameras(self, *, force: bool = False) -> None:
1159+
"""Refresh available list only when needed (backend changed, no cache, or forced)."""
1160+
backend = self._current_backend_key()
1161+
1162+
needs_scan = (
1163+
force
1164+
or not self._has_scan_results
1165+
or (self._last_scan_backend is None)
1166+
or (backend != self._last_scan_backend)
1167+
or (self.available_cameras_list.count() == 0) # defensive: list got cleared
1168+
)
1169+
if needs_scan:
1170+
self._refresh_available_cameras()
1171+
else:
1172+
# No scan; just ensure Add button state is consistent
1173+
self._update_button_states()
11141174

11151175
def _reset_selected_camera(self, *, clear_backend_cache: bool = False) -> None:
11161176
"""Reset the selected camera by probing device defaults and applying them to requested values."""

dlclivegui/gui/main_window.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,10 +1192,7 @@ def _open_camera_config_dialog(self) -> None:
11921192
self._cam_dialog = CameraConfigDialog(self, self._config.multi_camera)
11931193
self._cam_dialog.settings_changed.connect(self._on_multi_camera_settings_changed)
11941194
else:
1195-
# Refresh its UI from current settings when reopened
1196-
# self._cam_dialog._populate_from_settings()
1197-
# ^ do not call here -let the dialog handle it via showEvent to avoid overwriting unsaved changes
1198-
self._cam_dialog.dlc_camera_id = self._inference_camera_id
1195+
self._cam_dialog.set_settings(self._config.multi_camera, dlc_camera_id=self._inference_camera_id)
11991196

12001197
self._cam_dialog.show()
12011198
self._cam_dialog.raise_()

0 commit comments

Comments
 (0)