Skip to content

Commit 6db3f74

Browse files
committed
Improve preview handling and refactor tests
Camera dialog: tighten preview lifecycle and disabled-control handling. Introduces a preview-starting flag, commits pending edits before starting preview, consolidates loader/start/stop flow, and treats exposure/gain as 0 when their controls are disabled so they won't trigger unnecessary restarts. Also fixes various preview UI/loader state transitions and button states during async start/stop. Tests: large refactor of test fixtures to provide deterministic fake backends and DLCLive doubles. Adds reusable test backend helpers (make_backend_class, temp_backend, register_fake_backend_session), a stable fake_backend_factory, and simplifies GUI autouse patches. Expands and reorganizes unit and end-to-end camera-config tests to cover preview start/stop, restart/no-restart semantics, scan cancellation, duplicate/max-camera guards, commit-on-select/OK behavior, crop validation, and backend capability handling. Updates gui conftest to use the new fake backend and DLCLive test doubles.
1 parent abed9d1 commit 6db3f74

File tree

5 files changed

+785
-285
lines changed

5 files changed

+785
-285
lines changed

dlclivegui/gui/camera_config_dialog.py

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

Comments
 (0)