From 3bd33b93cd829d7aaf9cb447cf011506bf0612a6 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 2 Dec 2025 16:50:18 +0100 Subject: [PATCH] fix(image): combobox for device selection is repopulated with config update --- bec_widgets/widgets/plots/image/image.py | 23 +++++++--- tests/unit_tests/test_image_view_next_gen.py | 47 +++++++++++++------- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/bec_widgets/widgets/plots/image/image.py b/bec_widgets/widgets/plots/image/image.py index 3f2f9310..879705a4 100644 --- a/bec_widgets/widgets/plots/image/image.py +++ b/bec_widgets/widgets/plots/image/image.py @@ -124,6 +124,9 @@ class Image(ImageBase): self.async_update = False self.bec_dispatcher.connect_slot(self.on_scan_status, MessageEndpoints.scan_status()) self.bec_dispatcher.connect_slot(self.on_scan_progress, MessageEndpoints.scan_progress()) + self.bec_dispatcher.connect_slot( + self._populate_signals, MessageEndpoints.device_config_update() + ) ################################## ### Toolbar Initialization @@ -189,15 +192,21 @@ class Image(ImageBase): Has to be done with QTimer.singleShot to ensure the UI is fully initialized, needed for testing. """ self._populate_signals() - self._reverse_device_items() - self.device_combo_box.setCurrentText("") # set again default to empty string - def _populate_signals(self) -> None: + @SafeSlot(dict, dict) + def _populate_signals(self, data: dict | None = None, meta: dict | None = None) -> None: """ - Populate the device combo box with preview-signal devices in the - format '_' and store the tuple(device, signal) in - the item's userData for later use. + (Re)populate the device combo box with preview/async signals, + matching the initial setup logic. """ + self.device_combo_box.blockSignals(True) + self.device_combo_box.clear() + # Rebuild base device list via the combobox' own filtering logic + self.device_combo_box.update_devices_from_filters() + base_count = self.device_combo_box.count() + # Place an empty default entry between base devices and signal entries + self.device_combo_box.insertItem(base_count, "", None) + preview_signals = self.client.device_manager.get_bec_signals("PreviewSignal") async_signals = self.client.device_manager.get_bec_signals("AsyncSignal") all_signals = preview_signals + async_signals @@ -209,6 +218,8 @@ class Image(ImageBase): continue label = signal_config.get("obj_name", f"{device}_{signal}") self.device_combo_box.addItem(label, (device, signal, signal_config)) + self.device_combo_box.setCurrentText("") + self.device_combo_box.blockSignals(False) def _reverse_device_items(self) -> None: """ diff --git a/tests/unit_tests/test_image_view_next_gen.py b/tests/unit_tests/test_image_view_next_gen.py index cebb6ae4..7a76f942 100644 --- a/tests/unit_tests/test_image_view_next_gen.py +++ b/tests/unit_tests/test_image_view_next_gen.py @@ -730,21 +730,32 @@ def test_monitor_selection_populate_signals(qtbot, mocked_client, monkeypatch): monkeypatch.setattr(view.client, "device_manager", _FakeDM()) - initial_count = view.device_combo_box.count() - view._populate_signals() - # PreviewSignal + AsyncSignal entries were added - assert view.device_combo_box.count() == initial_count + 3 + # Base devices first, then empty separator, then signal entries + signal_texts = [] + separator_seen = False + for i in range(view.device_combo_box.count()): + data = view.device_combo_box.itemData(i) + text = view.device_combo_box.itemText(i) + if data is None and text == "": + separator_seen = True + continue + if separator_seen is False: + # base device entries + continue + # After separator we expect signal tuples + assert isinstance(data, tuple) + signal_texts.append(text) - # The first newly added item should carry tuple userData describing the device/signal - data = view.device_combo_box.itemData(initial_count) - assert isinstance(data, tuple) and data[0] == "eiger" - texts = [ - view.device_combo_box.itemText(i) - for i in range(initial_count, view.device_combo_box.count()) - ] - assert "async_device_img_async" in texts + assert {"eiger_img", "eiger_img2", "async_device_img_async"}.issubset(set(signal_texts)) + first_signal_idx = next( + i + for i in range(view.device_combo_box.count()) + if isinstance(view.device_combo_box.itemData(i), tuple) + ) + data = view.device_combo_box.itemData(first_signal_idx) + assert isinstance(data, tuple) and data[0] in ["eiger", "eiger2", "async_device"] def test_monitor_selection_adjust_and_connect(qtbot, mocked_client, monkeypatch): @@ -774,9 +785,13 @@ def test_monitor_selection_adjust_and_connect(qtbot, mocked_client, monkeypatch) # Execute the method under test view._adjust_and_connect() - # Expect exactly two items: preview label followed by the empty default - assert combo.count() == 2 - # Because of the reversal, the preview label comes first - assert combo.itemText(0) == "eiger_img" + # Base devices should appear first, then empty separator, then signals + sep_idx = next( + i for i in range(combo.count()) if combo.itemData(i) is None and combo.itemText(i) == "" + ) + first_signal_idx = sep_idx + 1 + assert isinstance(combo.itemData(first_signal_idx), tuple) + assert combo.itemText(first_signal_idx) == "eiger_img" + assert combo.itemText(sep_idx) == "" # Current selection remains empty assert combo.currentText() == ""