Skip to content

Commit f66553d

Browse files
committed
Improve camera dialog validation & workers
Refine CameraConfigDialog behavior to prevent races and invalid states: only clean up the scan worker when it's not running; disable the Add Camera button unless the selected item is a DetectedCamera; improve probe worker cancellation and waiting to avoid concurrent probes; emit the multi-camera model copy instead of a deepcopy when settings change. Add _enabled_count_with and enforce MAX_CAMERAS when enabling a camera and before closing the dialog. Clamp crop coordinates and only apply crop when the rectangle is valid to avoid invalid-frame cropping. Small comment/organization tweaks included.
1 parent 3676ba1 commit f66553d

1 file changed

Lines changed: 49 additions & 10 deletions

File tree

dlclivegui/gui/camera_config/camera_config_dialog.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)