@@ -225,6 +225,7 @@ def __init__(
225225 self ._probe_apply_to_requested : bool = False
226226 self ._probe_target_row : int | None = None
227227 self ._current_edit_index : int | None = None
228+ self ._suppress_selection_actions : bool = False
228229
229230 # Preview state
230231 self ._preview_backend : CameraBackend | None = None
@@ -729,6 +730,8 @@ def eventFilter(self, obj, event):
729730 self .cam_fps ,
730731 self .cam_width ,
731732 self .cam_height ,
733+ self .cam_exposure ,
734+ self .cam_gain ,
732735 self .cam_crop_x0 ,
733736 self .cam_crop_y0 ,
734737 self .cam_crop_x1 ,
@@ -824,6 +827,8 @@ def _mark_dirty(*_args):
824827 self .cam_crop_y0 ,
825828 self .cam_crop_x1 ,
826829 self .cam_crop_y1 ,
830+ self .cam_exposure ,
831+ self .cam_gain ,
827832 self .cam_width ,
828833 self .cam_height ,
829834 ):
@@ -993,14 +998,24 @@ def _on_available_camera_double_clicked(self, item: QListWidgetItem) -> None:
993998 self ._add_selected_camera ()
994999
9951000 def _on_active_camera_selected (self , row : int ) -> None :
1001+ if getattr (self , "_suppress_selection_change" , False ):
1002+ LOGGER .debug ("[Selection] Suppressed currentRowChanged event at index %d." , row )
1003+ return
1004+ prev_row = self ._current_edit_index
9961005 LOGGER .info (
9971006 "[Select] row=%s prev=%s preview_active=%s loading_active=%s" ,
9981007 row ,
999- self . _current_edit_index ,
1008+ prev_row ,
10001009 self ._preview_active ,
10011010 self ._loading_active ,
10021011 )
1003- prev_row = self ._current_edit_index
1012+ if row is None or row < 0 :
1013+ LOGGER .debug (
1014+ "[Selection] row<0 (selection cleared) ignored to avoid"
1015+ " stopping preview/loading when clicking away. row=%s" ,
1016+ row ,
1017+ )
1018+ return
10041019
10051020 # If row is the same, ignore
10061021 if prev_row is not None and prev_row == row :
@@ -1111,36 +1126,62 @@ def _update_active_list_item(self, row: int, cam: CameraSettings) -> None:
11111126 item = self .active_cameras_list .item (row )
11121127 if not item :
11131128 return
1114- item .setText (self ._format_camera_label (cam , row ))
1115- item .setData (Qt .ItemDataRole .UserRole , cam )
1116- item .setForeground (Qt .GlobalColor .gray if not cam .enabled else Qt .GlobalColor .black )
1117- self ._refresh_camera_labels ()
1118- self ._update_button_states ()
1129+ self ._suppress_selection_change = True # prevent unwanted selection change events during update
1130+ try :
1131+ item .setText (self ._format_camera_label (cam , row ))
1132+ item .setData (Qt .ItemDataRole .UserRole , cam )
1133+ item .setForeground (Qt .GlobalColor .gray if not cam .enabled else Qt .GlobalColor .black )
1134+ self ._refresh_camera_labels ()
1135+ self ._update_button_states ()
1136+ finally :
1137+ self ._suppress_selection_change = False
11191138
11201139 def _load_camera_to_form (self , cam : CameraSettings ) -> None :
1121- backend = (cam .backend or "" ).lower ()
1122- props = cam .properties if isinstance (cam .properties , dict ) else {}
1123- ns = props .get (backend , {}) if isinstance (props , dict ) else {}
1124- self .cam_enabled_checkbox .setChecked (cam .enabled )
1125- self .cam_name_label .setText (cam .name )
1126- self .cam_device_name_label .setText (str (ns .get ("device_id" , "" )))
1127- self .cam_index_label .setText (str (cam .index ))
1128- self .cam_backend_label .setText (cam .backend )
1129- self ._update_controls_for_backend (cam .backend )
1130- self .cam_width .setValue (cam .width )
1131- self .cam_height .setValue (cam .height )
1132- self .cam_fps .setValue (cam .fps )
1133- self .cam_exposure .setValue (cam .exposure )
1134- self .cam_gain .setValue (cam .gain )
1135- rot_index = self .cam_rotation .findData (cam .rotation )
1136- if rot_index >= 0 :
1137- self .cam_rotation .setCurrentIndex (rot_index )
1138- self .cam_crop_x0 .setValue (cam .crop_x0 )
1139- self .cam_crop_y0 .setValue (cam .crop_y0 )
1140- self .cam_crop_x1 .setValue (cam .crop_x1 )
1141- self .cam_crop_y1 .setValue (cam .crop_y1 )
1142- self .apply_settings_btn .setEnabled (True )
1143- self ._set_detected_labels (cam )
1140+ block = [
1141+ self .cam_enabled_checkbox ,
1142+ self .cam_width ,
1143+ self .cam_height ,
1144+ self .cam_fps ,
1145+ self .cam_exposure ,
1146+ self .cam_gain ,
1147+ self .cam_rotation ,
1148+ self .cam_crop_x0 ,
1149+ self .cam_crop_y0 ,
1150+ self .cam_crop_x1 ,
1151+ self .cam_crop_y1 ,
1152+ ]
1153+ for widget in block :
1154+ if hasattr (widget , "blockSignals" ):
1155+ widget .blockSignals (True )
1156+ try :
1157+ backend = (cam .backend or "" ).lower ()
1158+ props = cam .properties if isinstance (cam .properties , dict ) else {}
1159+ ns = props .get (backend , {}) if isinstance (props , dict ) else {}
1160+ self .cam_enabled_checkbox .setChecked (cam .enabled )
1161+ self .cam_name_label .setText (cam .name )
1162+ self .cam_device_name_label .setText (str (ns .get ("device_id" , "" )))
1163+ self .cam_index_label .setText (str (cam .index ))
1164+ self .cam_backend_label .setText (cam .backend )
1165+ self ._update_controls_for_backend (cam .backend )
1166+ self .cam_width .setValue (cam .width )
1167+ self .cam_height .setValue (cam .height )
1168+ self .cam_fps .setValue (cam .fps )
1169+ self .cam_exposure .setValue (cam .exposure )
1170+ self .cam_gain .setValue (cam .gain )
1171+ rot_index = self .cam_rotation .findData (cam .rotation )
1172+ if rot_index >= 0 :
1173+ self .cam_rotation .setCurrentIndex (rot_index )
1174+ self .cam_crop_x0 .setValue (cam .crop_x0 )
1175+ self .cam_crop_y0 .setValue (cam .crop_y0 )
1176+ self .cam_crop_x1 .setValue (cam .crop_x1 )
1177+ self .cam_crop_y1 .setValue (cam .crop_y1 )
1178+ self .apply_settings_btn .setEnabled (True )
1179+ self ._set_detected_labels (cam )
1180+ finally :
1181+ for widget in block :
1182+ if hasattr (widget , "blockSignals" ):
1183+ widget .blockSignals (False )
1184+
11441185 self .apply_settings_btn .setEnabled (False )
11451186 self ._set_apply_dirty (False )
11461187
@@ -1185,7 +1226,7 @@ def _start_probe_for_camera(self, cam: CameraSettings, *, apply_to_requested: bo
11851226 requested width/height/fps with detected device values.
11861227 """
11871228 # Don’t probe if preview is active/loading
1188- if self ._loading_active :
1229+ if self ._loading_active or self . _preview_active :
11891230 return
11901231
11911232 # Track probe intent
@@ -1483,6 +1524,8 @@ def _apply_camera_settings(self) -> bool:
14831524 self .cam_crop_x0 ,
14841525 self .cam_width ,
14851526 self .cam_height ,
1527+ self .cam_exposure ,
1528+ self .cam_gain ,
14861529 self .cam_crop_y0 ,
14871530 self .cam_crop_x1 ,
14881531 self .cam_crop_y1 ,
@@ -1648,18 +1691,21 @@ def _restart_preview_for_camera(self, cam: CameraSettings) -> None:
16481691 cam .backend ,
16491692 cam .index ,
16501693 )
1694+ self ._suppress_selection_actions = True
1695+ try :
1696+ # Stop any running preview cleanly
1697+ self ._stop_preview ()
16511698
1652- # Stop any running preview cleanly
1653- self ._stop_preview ()
1654-
1655- # Force preview-safe backend flags
1656- if isinstance (cam .properties , dict ):
1657- ns = cam .properties .setdefault ((cam .backend or "" ).lower (), {})
1658- if isinstance (ns , dict ):
1659- ns ["fast_start" ] = False
1699+ # Force preview-safe backend flags
1700+ if isinstance (cam .properties , dict ):
1701+ ns = cam .properties .setdefault ((cam .backend or "" ).lower (), {})
1702+ if isinstance (ns , dict ):
1703+ ns ["fast_start" ] = False
16601704
1661- # Start preview without relying on selection state
1662- self ._start_preview_with_camera (cam )
1705+ # Start preview without relying on selection state
1706+ self ._start_preview_with_camera (cam )
1707+ finally :
1708+ self ._suppress_selection_actions = False
16631709
16641710 def _start_preview_with_camera (self , cam : CameraSettings ) -> None :
16651711 """Start preview for a given CameraSettings object."""
@@ -1759,6 +1805,9 @@ def _stop_preview(self) -> None:
17591805 self ._preview_active ,
17601806 getattr (getattr (self ._preview_backend , "settings" , None ), "backend" , None ),
17611807 )
1808+ # Also show traceback to see who called stop_preview,
1809+ # since this should only be called from a few places.
1810+ # LOGGER.debug("[Preview] stop_preview called from: %s", "".join(traceback.format_stack(limit=6)))
17621811 # Cancel loader if running
17631812 if self ._loader and self ._loader .isRunning ():
17641813 self ._loader .request_cancel ()
0 commit comments