Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions dlclivegui/gui/camera_config/camera_config_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ def dlc_camera_id(self, value: str | None) -> None:
self._dlc_camera_id = value
self._refresh_camera_labels()

def showEvent(self, event):
super().showEvent(event)
try:
# Reset cleanup guard so close cleanup runs for each session
self._cleanup_done = False
except Exception:
pass
Comment thread
C-Achard marked this conversation as resolved.
Outdated

# Rebuild the working copy from the latest “accepted” settings
self._working_settings = self._multi_camera_settings.model_copy(deep=True)
self._current_edit_index = None
Comment thread
C-Achard marked this conversation as resolved.
self._populate_from_settings()

# Maintain overlay geometry when resizing
def resizeEvent(self, event):
super().resizeEvent(event)
Expand Down Expand Up @@ -304,13 +317,14 @@ def _update_button_states(self) -> None:

active_row = self.active_cameras_list.currentRow()
has_active_selection = active_row >= 0
allow_structure_edits = has_active_selection and not scan_running

self.remove_camera_btn.setEnabled(allow_structure_edits)
self.move_up_btn.setEnabled(allow_structure_edits and active_row > 0)
self.move_down_btn.setEnabled(allow_structure_edits and active_row < self.active_cameras_list.count() - 1)
# During loading, preview button becomes "Cancel Loading"
# Allow removing/moving active cameras even during scanning
self.remove_camera_btn.setEnabled(has_active_selection)
self.move_up_btn.setEnabled(has_active_selection and active_row > 0)
self.move_down_btn.setEnabled(has_active_selection and active_row < self.active_cameras_list.count() - 1)

self.preview_btn.setEnabled(has_active_selection or self._preview.state == PreviewState.LOADING)

available_row = self.available_cameras_list.currentRow()
self.add_camera_btn.setEnabled(available_row >= 0 and not scan_running)
Comment thread
C-Achard marked this conversation as resolved.
Outdated

Expand Down Expand Up @@ -1014,6 +1028,9 @@ def _populate_from_settings(self) -> None:
item.setForeground(Qt.GlobalColor.gray)
self.active_cameras_list.addItem(item)

if self.active_cameras_list.count() > 0:
self.active_cameras_list.setCurrentRow(0)

self._refresh_available_cameras()
self._update_button_states()

Expand Down Expand Up @@ -1076,6 +1093,7 @@ def _on_ok_clicked(self) -> None:
if self._working_settings.cameras and not active:
QMessageBox.warning(self, "No Active Cameras", "Please enable at least one camera or remove all cameras.")
return
self._multi_camera_settings = self._working_settings.model_copy(deep=True)
self.settings_changed.emit(copy.deepcopy(self._working_settings))

self._on_close_cleanup()
Expand Down
96 changes: 96 additions & 0 deletions tests/gui/camera_config/test_cam_dialog_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,3 +424,99 @@ def slow_run(self):

qtbot.waitUntil(lambda: dialog._preview.loader is None and dialog._preview.state == PreviewState.IDLE, timeout=2000)
assert dialog._preview.backend is None


@pytest.mark.gui
def test_remove_active_camera_works_while_scan_running(dialog, qtbot, monkeypatch):
"""
Regression test for:
- 'When coming back to camera config after choosing a camera, it cannot be removed'
Root cause: scan_running disabled structure edits (Remove/Move).
Expected: Remove works even while discovery scan is running.
"""

# Slow down camera detection so scan stays RUNNING long enough for interaction
def slow_detect(backend, max_devices=10, should_cancel=None, progress_cb=None, **kwargs):
for i in range(50):
if should_cancel and should_cancel():
break
if progress_cb:
progress_cb(f"Scanning… {i}")
time.sleep(0.02)
return [
DetectedCamera(index=0, label=f"{backend}-X"),
DetectedCamera(index=1, label=f"{backend}-Y"),
]

monkeypatch.setattr(CameraFactory, "detect_cameras", staticmethod(slow_detect))

# Ensure an active row is selected
dialog.active_cameras_list.setCurrentRow(0)
qtbot.waitUntil(lambda: dialog.active_cameras_list.currentRow() == 0, timeout=1000)

initial_active = dialog.active_cameras_list.count()
initial_model = len(dialog._working_settings.cameras)
assert initial_active == initial_model == 1

# Trigger scan; wait until scan controls indicate it's running
qtbot.mouseClick(dialog.refresh_btn, Qt.LeftButton)
qtbot.waitUntil(lambda: dialog._is_scan_running(), timeout=1000)
qtbot.waitUntil(lambda: dialog.scan_cancel_btn.isVisible(), timeout=1000)

# EXPECTATION: remove button should be enabled even during scan
# (This will fail until _update_button_states is changed to not block remove/move during scan)
qtbot.waitUntil(lambda: dialog.remove_camera_btn.isEnabled(), timeout=1000)
Comment thread
C-Achard marked this conversation as resolved.
Outdated

# Remove the selected active camera during scan
qtbot.mouseClick(dialog.remove_camera_btn, Qt.LeftButton)

assert dialog.active_cameras_list.count() == initial_active - 1
assert len(dialog._working_settings.cameras) == initial_model - 1

# Clean up: cancel scan so teardown doesn't hang waiting for scan completion
if dialog.scan_cancel_btn.isVisible() and dialog.scan_cancel_btn.isEnabled():
qtbot.mouseClick(dialog.scan_cancel_btn, Qt.LeftButton)

qtbot.waitUntil(lambda: not dialog._is_scan_running(), timeout=3000)


@pytest.mark.gui
def test_ok_updates_internal_multicamera_settings(dialog, qtbot):
"""
Regression test for:
- 'adding another camera and hitting OK does not add the new extra camera'
when caller reads dialog._multi_camera_settings after closing.

Expected:
- OK emits updated settings
- dialog._multi_camera_settings is updated to match accepted settings
"""

# Ensure backend combo matches the active camera backend, so duplicate logic behaves consistently
_select_backend_for_active_cam(dialog, cam_row=0)

# Scan and add a non-duplicate camera (index 1)
_run_scan_and_wait(dialog, qtbot, timeout=2000)
dialog.available_cameras_list.setCurrentRow(1)
qtbot.mouseClick(dialog.add_camera_btn, Qt.LeftButton)

qtbot.waitUntil(lambda: dialog.active_cameras_list.count() == 2, timeout=1000)
assert len(dialog._working_settings.cameras) == 2

# Click OK and capture emitted settings
with qtbot.waitSignal(dialog.settings_changed, timeout=2000) as sig:
qtbot.mouseClick(dialog.ok_btn, Qt.LeftButton)

emitted = sig.args[0]
assert isinstance(emitted, MultiCameraSettings)
assert len(emitted.cameras) == 2

# Check: internal source-of-truth must match accepted state
# (This will fail until _on_ok_clicked updates self._multi_camera_settings)
Comment thread
C-Achard marked this conversation as resolved.
Outdated
assert dialog._multi_camera_settings is not None
assert len(dialog._multi_camera_settings.cameras) == 2

# Optional: ensure camera identities match (names/index/backend)
assert [(c.backend, int(c.index)) for c in dialog._multi_camera_settings.cameras] == [
(c.backend, int(c.index)) for c in emitted.cameras
]