diff --git a/bec_widgets/widgets/plots/image/image.py b/bec_widgets/widgets/plots/image/image.py index c6098579..31c1c258 100644 --- a/bec_widgets/widgets/plots/image/image.py +++ b/bec_widgets/widgets/plots/image/image.py @@ -137,13 +137,7 @@ class Image(ImageBase): lambda: ImageLayerConfig(monitor=None, monitor_type="auto", source="auto") ) super().__init__( - parent=parent, - main_image=ImageItem(parent_image=self), - config=config, - client=client, - gui_id=gui_id, - popups=popups, - **kwargs, + parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs ) self.layer_removed.connect(self._on_layer_removed) self.scan_id = None @@ -498,6 +492,7 @@ class Image(ImageBase): """ Disconnect the image update signals and clean up the image. """ + self.layer_removed.disconnect(self._on_layer_removed) for layer_name in list(self.subscriptions.keys()): config = self.subscriptions[layer_name] if config.monitor is not None: diff --git a/bec_widgets/widgets/plots/image/image_base.py b/bec_widgets/widgets/plots/image/image_base.py index 0382465b..1d110025 100644 --- a/bec_widgets/widgets/plots/image/image_base.py +++ b/bec_widgets/widgets/plots/image/image_base.py @@ -80,10 +80,12 @@ class ImageLayerManager: def __init__( self, + parent: ImageBase, plot_item: pg.PlotItem, on_add: SignalInstance | None = None, on_remove: SignalInstance | None = None, ): + self.parent = parent self.plot_item = plot_item self.on_add = on_add self.on_remove = on_remove @@ -92,7 +94,6 @@ class ImageLayerManager: def add( self, name: str, - image: ImageItem, z_position: int | Literal["top", "bottom"] | None = None, sync: ImageLayerSync | None = None, **kwargs, @@ -107,14 +108,17 @@ class ImageLayerManager: sync (ImageLayerSync | None): The synchronization settings for the image layer. **kwargs: ImageLayerSync settings. Only used if sync is None. """ + if name in self.layers: + raise ValueError(f"Layer with name '{name}' already exists.") if sync is None: sync = ImageLayerSync(**kwargs) if z_position is None or z_position == "top": z_position = self._get_top_z_position() elif z_position == "bottom": z_position = self._get_bottom_z_position() + image = ImageItem(parent_image=self.parent, object_name=name) image.setZValue(z_position) - image.destroyed.connect(lambda: self._remove_destroyed_layer(name)) + image.removed.connect(lambda: self._remove_destroyed_layer(name)) self.layers[name] = ImageLayer(name=name, image=image, sync=sync) self.plot_item.addItem(image) @@ -131,30 +135,28 @@ class ImageLayerManager: Args: layer (str): The name of the layer to remove. """ - self.remove(layer, pop=True) + self.remove(layer) if self.on_remove is not None: self.on_remove.emit(layer) - def remove(self, layer: ImageLayer | str, pop=True): + def remove(self, layer: ImageLayer | str): """ Remove an image layer from the widget. Args: layer (ImageLayer | str): The image layer to remove. Can be the layer object or the name of the layer. - pop (bool): Whether to remove the layer from the manager's layers dictionary. If False, the layer is only removed from the plot item. """ if isinstance(layer, str): name = layer else: name = layer.name - if pop: - removed_layer = self.layers.pop(name, None) - else: - removed_layer = self.layers.get(name, None) + + removed_layer = self.layers.pop(name, None) + if not removed_layer: return self.plot_item.removeItem(removed_layer.image) - removed_layer.image.remove() + removed_layer.image.remove(emit=False) removed_layer.image.deleteLater() removed_layer.image = None @@ -162,9 +164,9 @@ class ImageLayerManager: """ Clear all image layers from the manager. """ - for layer in self.layers.values(): + for layer in list(self.layers.keys()): # Remove each layer from the plot item and delete it - self.remove(layer, pop=False) + self.remove(layer) self.layers.clear() def _get_top_z_position(self) -> int: @@ -234,11 +236,9 @@ class ImageBase(PlotBase): layer_added = Signal(str) layer_removed = Signal(str) - def __init__(self, *args, main_image: ImageItem, **kwargs): + def __init__(self, *args, **kwargs): """ Initialize the ImageBase widget. - Args: - main_image (ImageItem): The main image item to be displayed. This is the main image layer, also used as reference for autoranging. """ self.x_roi = None self.y_roi = None @@ -248,9 +248,9 @@ class ImageBase(PlotBase): # Headless controller keeps the canonical list. self.roi_manager_dialog = None self.layers: ImageLayerManager = ImageLayerManager( - self.plot_item, on_add=self.layer_added, on_remove=self.layer_removed + self, plot_item=self.plot_item, on_add=self.layer_added, on_remove=self.layer_removed ) - self.layers.add("main", main_image) + self.layers.add("main") self.autorange = True self.autorange_mode = "mean" @@ -644,7 +644,7 @@ class ImageBase(PlotBase): else: x = coordinates[1] y = coordinates[2] - image = self.main_image.image + image = self.layers["main"].image.image if image is None: return max_row, max_col = image.shape[0] - 1, image.shape[1] - 1 diff --git a/bec_widgets/widgets/plots/image/image_item.py b/bec_widgets/widgets/plots/image/image_item.py index 32091d64..92f8a687 100644 --- a/bec_widgets/widgets/plots/image/image_item.py +++ b/bec_widgets/widgets/plots/image/image_item.py @@ -43,6 +43,7 @@ class ImageItemConfig(ConnectionConfig): # TODO review config class ImageItem(BECConnector, pg.ImageItem): + RPC = True USER_ACCESS = [ "color_map", @@ -69,6 +70,7 @@ class ImageItem(BECConnector, pg.ImageItem): ] vRangeChangedManually = Signal(tuple) + removed = Signal() def __init__( self, @@ -274,6 +276,8 @@ class ImageItem(BECConnector, pg.ImageItem): self.buffer = [] self.max_len = 0 - def remove(self): + def remove(self, emit: bool = True): self.clear() super().remove() + if emit: + self.removed.emit() diff --git a/tests/unit_tests/test_image_layer.py b/tests/unit_tests/test_image_layer.py index e6cf6939..dc313151 100644 --- a/tests/unit_tests/test_image_layer.py +++ b/tests/unit_tests/test_image_layer.py @@ -10,7 +10,7 @@ from bec_widgets.widgets.plots.image.image_item import ImageItem @pytest.fixture() def image_layer_manager(): """Fixture to create an instance of ImageLayer.""" - layer = ImageLayerManager(plot_item=mock.MagicMock(spec=pg.PlotItem)) + layer = ImageLayerManager(parent=None, plot_item=mock.MagicMock(spec=pg.PlotItem)) yield layer layer.clear() @@ -23,27 +23,22 @@ def test_image_layer_manager_initialization(image_layer_manager): def test_add_image_layer(image_layer_manager): """Test adding an image layer to the ImageLayerManager.""" - image = ImageItem() - layer = image_layer_manager.add(name="Test Layer", image=image) + layer = image_layer_manager.add(name="Test Layer") assert layer.image.zValue() == 0 - image2 = ImageItem() - layer2 = image_layer_manager.add(name="Test Layer 2", image=image2) + layer2 = image_layer_manager.add(name="Test Layer 2") assert layer2.image.zValue() == 1 - image3 = ImageItem() - layer3 = image_layer_manager.add(name="Test Layer 3", image=image3, z_position="top") + layer3 = image_layer_manager.add(name="Test Layer 3", z_position="top") assert layer3.image.zValue() == 2 - image4 = ImageItem() - layer4 = image_layer_manager.add(name="Test Layer 4", image=image4, z_position="bottom") + layer4 = image_layer_manager.add(name="Test Layer 4", z_position="bottom") assert layer4.image.zValue() == -1 def test_remove_image_layer(image_layer_manager): """Test removing an image layer from the ImageLayerManager.""" - image = ImageItem() - layer = image_layer_manager.add(name="Test Layer", image=image) + layer = image_layer_manager.add(name="Test Layer") assert len(image_layer_manager) == 1 image_layer_manager.remove(layer) @@ -52,8 +47,7 @@ def test_remove_image_layer(image_layer_manager): def test_clear_image_layers(image_layer_manager): """Test clearing all image layers from the ImageLayerManager.""" - image = ImageItem() - layer = image_layer_manager.add(name="Test Layer", image=image) + layer = image_layer_manager.add(name="Test Layer") assert len(image_layer_manager) == 1 image_layer_manager.clear() @@ -62,8 +56,7 @@ def test_clear_image_layers(image_layer_manager): def test_image_layer_manager_getitem(image_layer_manager): """Test getting an image layer by index.""" - image = ImageItem() - layer = image_layer_manager.add(name="Test Layer", image=image) + layer = image_layer_manager.add(name="Test Layer") assert image_layer_manager["Test Layer"] == layer with pytest.raises(TypeError): @@ -75,10 +68,8 @@ def test_image_layer_manager_getitem(image_layer_manager): def test_image_layer_iteration(image_layer_manager): """Test iterating over image layers.""" - image = ImageItem() - layer = image_layer_manager.add(name="Test Layer", image=image) + layer = image_layer_manager.add(name="Test Layer") assert list(image_layer_manager) == [layer] - image2 = ImageItem() - layer2 = image_layer_manager.add(name="Test Layer 2", image=image2) + layer2 = image_layer_manager.add(name="Test Layer 2") assert list(image_layer_manager) == [layer, layer2] diff --git a/tests/unit_tests/test_image_view_next_gen.py b/tests/unit_tests/test_image_view_next_gen.py index 2d3fd7ae..3385c8eb 100644 --- a/tests/unit_tests/test_image_view_next_gen.py +++ b/tests/unit_tests/test_image_view_next_gen.py @@ -117,8 +117,8 @@ def test_image_setup_image_2d(qtbot, mocked_client): bec_image_view = create_widget(qtbot, Image, client=mocked_client) bec_image_view.image(monitor="eiger", monitor_type="2d") assert bec_image_view.monitor == "eiger" - assert bec_image_view.main_image.config.source == "device_monitor_2d" - assert bec_image_view.main_image.config.monitor_type == "2d" + assert bec_image_view.subscriptions["main"].source == "device_monitor_2d" + assert bec_image_view.subscriptions["main"].monitor_type == "2d" assert bec_image_view.main_image.raw_data is None assert bec_image_view.main_image.image is None @@ -127,8 +127,8 @@ def test_image_setup_image_1d(qtbot, mocked_client): bec_image_view = create_widget(qtbot, Image, client=mocked_client) bec_image_view.image(monitor="eiger", monitor_type="1d") assert bec_image_view.monitor == "eiger" - assert bec_image_view.main_image.config.source == "device_monitor_1d" - assert bec_image_view.main_image.config.monitor_type == "1d" + assert bec_image_view.subscriptions["main"].source == "device_monitor_1d" + assert bec_image_view.subscriptions["main"].monitor_type == "1d" assert bec_image_view.main_image.raw_data is None assert bec_image_view.main_image.image is None @@ -137,8 +137,8 @@ def test_image_setup_image_auto(qtbot, mocked_client): bec_image_view = create_widget(qtbot, Image, client=mocked_client) bec_image_view.image(monitor="eiger", monitor_type="auto") assert bec_image_view.monitor == "eiger" - assert bec_image_view.main_image.config.source == "auto" - assert bec_image_view.main_image.config.monitor_type == "auto" + assert bec_image_view.subscriptions["main"].source == "auto" + assert bec_image_view.subscriptions["main"].monitor_type == "auto" assert bec_image_view.main_image.raw_data is None assert bec_image_view.main_image.image is None @@ -235,8 +235,8 @@ def test_setup_image_from_toolbar(qtbot, mocked_client): bec_image_view.selection_bundle.dim_combo_box.setCurrentText("2d") assert bec_image_view.monitor == "eiger" - assert bec_image_view.main_image.config.source == "device_monitor_2d" - assert bec_image_view.main_image.config.monitor_type == "2d" + assert bec_image_view.subscriptions["main"].source == "device_monitor_2d" + assert bec_image_view.subscriptions["main"].monitor_type == "2d" assert bec_image_view.main_image.raw_data is None assert bec_image_view.main_image.image is None