@@ -182,7 +182,8 @@ def _on_close_cleanup(self) -> None:
182182 # Keep this short to reduce UI freeze
183183 sw .wait (300 )
184184 self ._set_scan_state (CameraScanState .IDLE )
185- self ._cleanup_scan_worker ()
185+ if self ._scan_worker and not self ._scan_worker .isRunning ():
186+ self ._cleanup_scan_worker ()
186187
187188 # Cancel probe worker
188189 pw = getattr (self , "_probe_worker" , None )
@@ -643,7 +644,9 @@ def _on_available_camera_selected(self, row: int) -> None:
643644 if self ._scan_worker and self ._scan_worker .isRunning ():
644645 self .add_camera_btn .setEnabled (False )
645646 return
646- self .add_camera_btn .setEnabled (row >= 0 and not self ._is_scan_running ())
647+ item = self .available_cameras_list .item (row ) if row >= 0 else None
648+ detected = item .data (Qt .ItemDataRole .UserRole ) if item else None
649+ self .add_camera_btn .setEnabled (isinstance (detected , DetectedCamera ))
647650
648651 def _on_available_camera_double_clicked (self , item : QListWidgetItem ) -> None :
649652 if self ._is_scan_running ():
@@ -927,6 +930,14 @@ def _commit_pending_edits(self, *, reason: str = "") -> bool:
927930 )
928931 return False
929932
933+ def _enabled_count_with (self , row : int , new_enabled : bool ) -> int :
934+ count = 0
935+ for i , cam in enumerate (self ._working_settings .cameras ):
936+ enabled = new_enabled if i == row else bool (cam .enabled )
937+ if enabled :
938+ count += 1
939+ return count
940+
930941 def _apply_camera_settings (self ) -> bool :
931942 try :
932943 for sb in (
@@ -954,6 +965,14 @@ def _apply_camera_settings(self) -> bool:
954965 current_model = self ._working_settings .cameras [row ]
955966 new_model = self ._build_model_from_form (current_model )
956967
968+ if bool (new_model .enabled ):
969+ if self ._enabled_count_with (row , True ) > self .MAX_CAMERAS :
970+ QMessageBox .warning (
971+ self , "Maximum Cameras" , f"Maximum of { self .MAX_CAMERAS } active cameras allowed."
972+ )
973+ self .cam_enabled_checkbox .setChecked (bool (current_model .enabled ))
974+ return False
975+
957976 diff = CameraSettings .check_diff (current_model , new_model )
958977
959978 LOGGER .debug (
@@ -1087,6 +1106,9 @@ def _on_ok_clicked(self) -> None:
10871106 # Auto-apply pending edits before saving
10881107 if not self ._commit_pending_edits (reason = "before going back to the main window" ):
10891108 return
1109+ if len (self ._working_settings .get_active_cameras ()) > self .MAX_CAMERAS :
1110+ QMessageBox .warning (self , "Maximum Cameras" , f"Maximum of { self .MAX_CAMERAS } active cameras allowed." )
1111+ return
10901112 try :
10911113 if self .apply_settings_btn .isEnabled ():
10921114 self ._append_status ("[OK button] Auto-applying pending settings before closing dialog." )
@@ -1099,13 +1121,13 @@ def _on_ok_clicked(self) -> None:
10991121 QMessageBox .warning (self , "No Active Cameras" , "Please enable at least one camera or remove all cameras." )
11001122 return
11011123 self ._multi_camera_settings = self ._working_settings .model_copy (deep = True )
1102- self .settings_changed .emit (copy . deepcopy ( self ._working_settings ) )
1124+ self .settings_changed .emit (self ._multi_camera_settings )
11031125
11041126 self ._on_close_cleanup ()
11051127 self .accept ()
11061128
11071129 # -------------------------------
1108- # Probe (device telemetry) management
1130+ # Probe management
11091131 # -------------------------------
11101132
11111133 def _start_probe_for_camera (self , cam : CameraSettings , * , apply_to_requested : bool = False ) -> None :
@@ -1118,6 +1140,15 @@ def _start_probe_for_camera(self, cam: CameraSettings, *, apply_to_requested: bo
11181140 if self ._is_preview_live ():
11191141 return
11201142
1143+ pw = getattr (self , "_probe_worker" , None )
1144+ if pw and pw .isRunning ():
1145+ try :
1146+ pw .request_cancel ()
1147+ except Exception :
1148+ pass
1149+ pw .wait (200 )
1150+ self ._probe_worker = None
1151+
11211152 # Track probe intent
11221153 self ._probe_apply_to_requested = bool (apply_to_requested )
11231154 self ._probe_target_row = int (self ._current_edit_index ) if self ._current_edit_index is not None else None
@@ -1610,13 +1641,21 @@ def _update_preview(self) -> None:
16101641 rotation = self .cam_rotation .currentData ()
16111642 frame = apply_rotation (frame , rotation )
16121643
1613- # Apply crop if set in the form (real-time from UI)
1644+ # Compute crop with clamping
16141645 h , w = frame .shape [:2 ]
1615- x0 = self .cam_crop_x0 .value ()
1616- y0 = self .cam_crop_y0 .value ()
1617- x1 = self .cam_crop_x1 .value () or w
1618- y1 = self .cam_crop_y1 .value () or h
1619- frame = apply_crop (frame , x0 , y0 , x1 , y1 )
1646+ x0 = max (0 , min (self .cam_crop_x0 .value (), w ))
1647+ y0 = max (0 , min (self .cam_crop_y0 .value (), h ))
1648+ x1_val = self .cam_crop_x1 .value ()
1649+ y1_val = self .cam_crop_y1 .value ()
1650+ x1 = max (0 , min (x1_val if x1_val > 0 else w , w ))
1651+ y1 = max (0 , min (y1_val if y1_val > 0 else h , h ))
1652+
1653+ # Only apply if valid rectangle; otherwise skip crop
1654+ if x1 > x0 and y1 > y0 :
1655+ frame = apply_crop (frame , x0 , y0 , x1 , y1 )
1656+ else :
1657+ # Optional: show a status once, not every frame
1658+ pass
16201659
16211660 # Resize to fit preview label
16221661 frame = resize_to_fit (frame , max_w = 400 , max_h = 300 )
0 commit comments