Skip to content

Commit af3564f

Browse files
committed
Refine crop validation and camera UI commit flow
Config: tighten CameraSettings crop handling — treat all-zeros as "no crop", allow x1/y1 == 0 to mean "to edge", and only enforce x1>x0 / y1>y0 when x1/y1 are explicitly >0. GUI (camera_config_dialog): remove backend apply_transforms tweak in probe worker; add robust auto-commit behavior to prevent losing/accepting invalid edits when switching selection, adding/removing/reordering cameras or closing the dialog. Introduce _commit_pending_edits() which attempts to apply pending changes (and shows a warning on failure), make _apply_camera_settings() return success as a boolean, and block UI actions when validation fails. Also auto-applies pending settings on OK and ensures previews are stopped appropriately. Main window: ensure camera.properties is a dict and set backend-namespaced properties; set fast_start in backend namespace (and initialize properties safely) instead of the old top-level quick default. These changes improve UX by validating/committing edits proactively and make crop semantics clearer and more flexible.
1 parent c17e1f2 commit af3564f

3 files changed

Lines changed: 85 additions & 11 deletions

File tree

dlclivegui/config.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,20 @@ def _coerce_gain(cls, v):
8585
def _validate_crop(self):
8686
for f in ("crop_x0", "crop_y0", "crop_x1", "crop_y1"):
8787
setattr(self, f, max(0, int(getattr(self, f))))
88-
# Optional: if any crop is set, enforce x1>x0 and y1>y0
89-
if any([self.crop_x0, self.crop_y0, self.crop_x1, self.crop_y1]):
90-
if not (self.crop_x1 > self.crop_x0 and self.crop_y1 > self.crop_y0):
91-
raise ValueError("Invalid crop rectangle: require x1>x0 and y1>y0 when cropping is enabled.")
88+
89+
# No crop
90+
if self.crop_x0 == self.crop_y0 == self.crop_x1 == self.crop_y1 == 0:
91+
return self
92+
93+
# Allow x1/y1 == 0 to mean "to edge"
94+
# If x1 is explicitly set (>0), it must be > x0
95+
if self.crop_x1 > 0 and self.crop_x1 <= self.crop_x0:
96+
raise ValueError("Invalid crop rectangle: require x1 > x0 (or x1=0 for 'to edge').")
97+
98+
# If y1 is explicitly set (>0), it must be > y0
99+
if self.crop_y1 > 0 and self.crop_y1 <= self.crop_y0:
100+
raise ValueError("Invalid crop rectangle: require y1 > y0 (or y1=0 for 'to edge').")
101+
92102
return self
93103

94104
def get_crop_region(self) -> tuple[int, int, int, int] | None:

dlclivegui/gui/camera_config_dialog.py

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,6 @@ def __init__(self, cam: CameraSettings, parent: QWidget | None = None):
166166
ns = self._cam.properties.setdefault(self._cam.backend.lower(), {})
167167
if isinstance(ns, dict):
168168
ns["fast_start"] = False
169-
# Basler implements transforms in the backend
170-
# but preview already takes care of rotation/crop
171-
# so we disable transform application in probe to avoid double transforms and speed up probe
172-
ns["apply_transforms"] = False
173169

174170
def request_cancel(self):
175171
self._cancel = True
@@ -975,10 +971,25 @@ def _on_available_camera_double_clicked(self, item: QListWidgetItem) -> None:
975971
self._add_selected_camera()
976972

977973
def _on_active_camera_selected(self, row: int) -> None:
974+
prev_row = self._current_edit_index
975+
976+
# If switching away from a previous camera, commit pending edits first
977+
if prev_row is not None and prev_row != row:
978+
if not self._commit_pending_edits(reason="before switching camera selection"):
979+
# Revert selection back to previous row so the user stays on the invalid camera
980+
try:
981+
self.active_cameras_list.blockSignals(True)
982+
self.active_cameras_list.setCurrentRow(prev_row)
983+
finally:
984+
self.active_cameras_list.blockSignals(False)
985+
return
986+
978987
# Stop any running preview when selection changes
979988
if self._preview_active:
980989
self._stop_preview()
990+
981991
self._current_edit_index = row
992+
982993
self._update_button_states()
983994
if row < 0 or row >= self.active_cameras_list.count():
984995
self._clear_settings_form()
@@ -1301,6 +1312,8 @@ def _on_probe_finished(self) -> None:
13011312
self._probe_worker = None
13021313

13031314
def _add_selected_camera(self) -> None:
1315+
if not self._commit_pending_edits(reason="before adding a new camera"):
1316+
return
13041317
row = self.available_cameras_list.currentRow()
13051318
if row < 0:
13061319
return
@@ -1356,6 +1369,8 @@ def _add_selected_camera(self) -> None:
13561369
self._start_probe_for_camera(new_cam)
13571370

13581371
def _remove_selected_camera(self) -> None:
1372+
if not self._commit_pending_edits(reason="before removing a camera"):
1373+
return
13591374
row = self.active_cameras_list.currentRow()
13601375
if row < 0:
13611376
return
@@ -1368,6 +1383,8 @@ def _remove_selected_camera(self) -> None:
13681383
self._update_button_states()
13691384

13701385
def _move_camera_up(self) -> None:
1386+
if not self._commit_pending_edits(reason="before reordering cameras"):
1387+
return
13711388
row = self.active_cameras_list.currentRow()
13721389
if row <= 0:
13731390
return
@@ -1379,6 +1396,8 @@ def _move_camera_up(self) -> None:
13791396
self._refresh_camera_labels()
13801397

13811398
def _move_camera_down(self) -> None:
1399+
if not self._commit_pending_edits(reason="before reordering cameras"):
1400+
return
13821401
row = self.active_cameras_list.currentRow()
13831402
if row < 0 or row >= self.active_cameras_list.count() - 1:
13841403
return
@@ -1389,10 +1408,39 @@ def _move_camera_down(self) -> None:
13891408
cams[row], cams[row + 1] = cams[row + 1], cams[row]
13901409
self._refresh_camera_labels()
13911410

1392-
def _apply_camera_settings(self) -> None:
1411+
def _commit_pending_edits(self, *, reason: str = "") -> bool:
1412+
"""
1413+
Auto-apply pending edits (if any) before context-changing actions.
1414+
Returns True if it's safe to proceed, False if validation failed.
1415+
"""
1416+
# No selection → nothing to commit
1417+
if self._current_edit_index is None or self._current_edit_index < 0:
1418+
return True
1419+
1420+
# If Apply button isn't enabled, assume no pending edits
1421+
if not self.apply_settings_btn.isEnabled():
1422+
return True
1423+
1424+
try:
1425+
self._append_status(f"[Auto-Apply] Committing pending edits ({reason})…")
1426+
ok = self._apply_camera_settings()
1427+
return bool(ok)
1428+
except Exception as exc:
1429+
# _apply_camera_settings already shows a QMessageBox in many cases,
1430+
# but we add a clear guardrail here in case it doesn't.
1431+
QMessageBox.warning(
1432+
self,
1433+
"Unsaved / Invalid Settings",
1434+
"Your current camera settings are not valid and cannot be applied yet.\n\n"
1435+
"Please fix the highlighted fields (e.g. crop rectangle) or press Reset.\n\n"
1436+
f"Details: {exc}",
1437+
)
1438+
return False
1439+
1440+
def _apply_camera_settings(self) -> bool:
13931441
if self._loading_active:
13941442
self._append_status("[Apply] Preview is loading; please wait or cancel loading first.")
1395-
return
1443+
return False
13961444
try:
13971445
for sb in (
13981446
self.cam_fps,
@@ -1487,9 +1535,12 @@ def _cam_diff(old: CameraSettings, new: CameraSettings) -> dict:
14871535
else:
14881536
self._append_status("[Apply] Applied without restart (crop/rotation update is live).")
14891537

1538+
return True
1539+
14901540
except Exception as exc:
14911541
LOGGER.exception("Apply camera settings failed")
14921542
QMessageBox.warning(self, "Apply Settings Error", str(exc))
1543+
return False
14931544

14941545
def _update_button_states(self) -> None:
14951546
active_row = self.active_cameras_list.currentRow()
@@ -1503,6 +1554,15 @@ def _update_button_states(self) -> None:
15031554
self.add_camera_btn.setEnabled(available_row >= 0)
15041555

15051556
def _on_ok_clicked(self) -> None:
1557+
# Auto-apply pending edits before saving
1558+
if not self._commit_pending_edits(reason="before going back to the main window"):
1559+
return
1560+
try:
1561+
if self.apply_settings_btn.isEnabled():
1562+
self._append_status("[OK button] Auto-applying pending settings before closing dialog.")
1563+
self._apply_camera_settings()
1564+
except Exception:
1565+
LOGGER.exception("[OK button] Auto-apply failed")
15061566
self._stop_preview()
15071567
active = self._working_settings.get_active_cameras()
15081568
if self._working_settings.cameras and not active:

dlclivegui/gui/main_window.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1454,7 +1454,11 @@ def _start_preview(self) -> None:
14541454
# Store active settings for single camera mode (for DLC, recording frame rate, etc.)
14551455
self._active_camera_settings = active_cams[0] if active_cams else None
14561456
for cam in active_cams:
1457-
cam.properties.setdefault("fast_start", True)
1457+
if not isinstance(cam.properties, dict):
1458+
cam.properties = {}
1459+
ns = cam.properties.setdefault((cam.backend or "").lower(), {})
1460+
if isinstance(ns, dict):
1461+
ns["fast_start"] = False
14581462

14591463
self.multi_camera_controller.start(active_cams)
14601464
self._update_inference_buttons()

0 commit comments

Comments
 (0)