@@ -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."""
0 commit comments