@@ -231,6 +231,7 @@ def __init__(
231231 self ._preview_backend : CameraBackend | None = None
232232 self ._preview_timer : QTimer | None = None
233233 self ._preview_active : bool = False
234+ self ._preview_starting : bool = False
234235
235236 # Camera detection worker
236237 self ._scan_worker : DetectCamerasWorker | None = None
@@ -271,8 +272,8 @@ def _build_model_from_form(self, base: CameraSettings) -> CameraSettings:
271272 "width" : int (self .cam_width .value ()),
272273 "height" : int (self .cam_height .value ()),
273274 "fps" : float (self .cam_fps .value ()),
274- "exposure" : int (self .cam_exposure .value ()),
275- "gain" : float (self .cam_gain .value ()),
275+ "exposure" : int (self .cam_exposure .value ()) if self . cam_exposure . isEnabled () else 0 ,
276+ "gain" : float (self .cam_gain .value ()) if self . cam_gain . isEnabled () else 0.0 ,
276277 "rotation" : int (self .cam_rotation .currentData () or 0 ),
277278 "crop_x0" : int (self .cam_crop_x0 .value ()),
278279 "crop_y0" : int (self .cam_crop_y0 .value ()),
@@ -1190,8 +1191,8 @@ def _write_form_to_cam(self, cam: CameraSettings) -> None:
11901191 cam .width = int (self .cam_width .value ())
11911192 cam .height = int (self .cam_height .value ())
11921193 cam .fps = float (self .cam_fps .value ())
1193- cam .exposure = int (self .cam_exposure .value ())
1194- cam .gain = float (self .cam_gain .value ())
1194+ cam .exposure = int (self .cam_exposure .value () if self . cam_exposure . isEnabled () else 0 )
1195+ cam .gain = float (self .cam_gain .value () if self . cam_gain . isEnabled () else 0.0 )
11951196 cam .rotation = int (self .cam_rotation .currentData () or 0 )
11961197 cam .crop_x0 = int (self .cam_crop_x0 .value ())
11971198 cam .crop_y0 = int (self .cam_crop_y0 .value ())
@@ -1597,6 +1598,11 @@ def _cam_diff(old: CameraSettings, new: CameraSettings) -> dict:
15971598 restart = False
15981599 if self ._preview_active and isinstance (old_settings , CameraSettings ):
15991600 restart = self ._should_restart_preview (old_settings , new_model )
1601+ # If the preview is starting but not fully active yet,
1602+ # we can skip the restart since the new settings will be picked up on start anyway
1603+ if self ._preview_active and not getattr (self , "._preview_starting" , False ):
1604+ if restart :
1605+ QTimer .singleShot (0 , lambda cam = new_model : self ._restart_preview_for_camera (cam ))
16001606
16011607 LOGGER .info (
16021608 "[Apply] preview_active=%s restart=%s backend=%s idx=%s" ,
@@ -1745,64 +1751,72 @@ def _start_preview_with_camera(self, cam: CameraSettings) -> None:
17451751
17461752 def _start_preview (self ) -> None :
17471753 """Start camera preview asynchronously (no UI freeze)."""
1748- row = self ._current_edit_index
1749- if row is None or row < 0 :
1750- row = self .active_cameras_list .currentRow ()
1751-
1752- if row is None or row < 0 :
1753- LOGGER .warning ("[Preview] No camera selected to start preview." )
1754+ if not self ._commit_pending_edits (reason = "before starting preview" ):
17541755 return
1756+ if self ._preview_active or self ._loading_active :
1757+ return
1758+ self .starting_preview = True
1759+ try :
1760+ row = self ._current_edit_index
1761+ if row is None or row < 0 :
1762+ row = self .active_cameras_list .currentRow ()
17551763
1756- self ._current_edit_index = row
1757- LOGGER .info (
1758- "[Preview] resolved start row=%s active_row=%s" ,
1759- self ._current_edit_index ,
1760- self .active_cameras_list .currentRow (),
1761- )
1764+ if row is None or row < 0 :
1765+ LOGGER .warning ("[Preview] No camera selected to start preview." )
1766+ return
17621767
1763- item = self .active_cameras_list .item (self ._current_edit_index )
1764- if not item :
1765- return
1766- cam = item .data (Qt .ItemDataRole .UserRole )
1767- if not cam :
1768- return
1769- LOGGER .info (
1770- "[Preview] start requested row=%s backend=%s idx=%s name=%s loading=%s active=%s" ,
1771- self ._current_edit_index ,
1772- cam .backend ,
1773- cam .index ,
1774- cam .name ,
1775- self ._loading_active ,
1776- self ._preview_active ,
1777- )
1768+ self ._current_edit_index = row
1769+ LOGGER .info (
1770+ "[Preview] resolved start row=%s active_row=%s" ,
1771+ self ._current_edit_index ,
1772+ self .active_cameras_list .currentRow (),
1773+ )
17781774
1779- # Ensure any existing preview or loader is stopped/canceled
1780- self ._stop_preview ()
1781- # if self._loader and self._loader.isRunning():
1782- # self._loader.request_cancel()
1783- # Never use probe or fast_start mode
1784- if isinstance (cam .properties , dict ):
1785- ns = cam .properties .get ((cam .backend or "" ).lower (), {})
1786- if isinstance (ns , dict ):
1787- ns ["fast_start" ] = False
1788- # Create worker
1789- self ._loader = CameraLoadWorker (cam , self )
1790- self ._loader .progress .connect (self ._on_loader_progress )
1791- self ._loader .success .connect (self ._on_loader_success )
1792- self ._loader .error .connect (self ._on_loader_error )
1793- self ._loader .canceled .connect (self ._on_loader_canceled )
1794- self ._loader .finished .connect (self ._on_loader_finished )
1795- self ._loading_active = True
1796- self ._update_button_states ()
1775+ item = self .active_cameras_list .item (self ._current_edit_index )
1776+ if not item :
1777+ return
1778+ cam = item .data (Qt .ItemDataRole .UserRole )
1779+ if not cam :
1780+ return
1781+ LOGGER .info (
1782+ "[Preview] start requested row=%s backend=%s idx=%s name=%s loading=%s active=%s" ,
1783+ self ._current_edit_index ,
1784+ cam .backend ,
1785+ cam .index ,
1786+ cam .name ,
1787+ self ._loading_active ,
1788+ self ._preview_active ,
1789+ )
17971790
1798- # Prepare UI
1799- self .preview_group .setVisible (True )
1800- self .preview_label .setText ("No preview" )
1801- self .preview_status .clear ()
1802- self ._show_loading_overlay ("Loading camera…" )
1803- self ._set_preview_button_loading (True )
1791+ # Ensure any existing preview or loader is stopped/canceled
1792+ self ._stop_preview ()
1793+ # if self._loader and self._loader.isRunning():
1794+ # self._loader.request_cancel()
1795+ # Never use probe or fast_start mode
1796+ if isinstance (cam .properties , dict ):
1797+ ns = cam .properties .get ((cam .backend or "" ).lower (), {})
1798+ if isinstance (ns , dict ):
1799+ ns ["fast_start" ] = False
1800+ # Create worker
1801+ self ._loader = CameraLoadWorker (cam , self )
1802+ self ._loader .progress .connect (self ._on_loader_progress )
1803+ self ._loader .success .connect (self ._on_loader_success )
1804+ self ._loader .error .connect (self ._on_loader_error )
1805+ self ._loader .canceled .connect (self ._on_loader_canceled )
1806+ self ._loader .finished .connect (self ._on_loader_finished )
1807+ self ._loading_active = True
1808+ self ._update_button_states ()
18041809
1805- self ._loader .start ()
1810+ # Prepare UI
1811+ self .preview_group .setVisible (True )
1812+ self .preview_label .setText ("No preview" )
1813+ self .preview_status .clear ()
1814+ self ._show_loading_overlay ("Loading camera…" )
1815+ self ._set_preview_button_loading (True )
1816+
1817+ self ._loader .start ()
1818+ finally :
1819+ self .starting_preview = False
18061820
18071821 def _stop_preview (self ) -> None :
18081822 """Stop camera preview and cancel any ongoing loading."""
0 commit comments