From 46d7e3f5170a5f8b444043bc49651921816f7003 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 10 Jun 2025 15:18:24 +0200 Subject: [PATCH] feat(roi): rois can be lock to be not moved by mouse --- bec_widgets/cli/client.py | 62 +++++++++++++++++++ bec_widgets/widgets/plots/image/image_base.py | 4 ++ bec_widgets/widgets/plots/roi/image_roi.py | 44 ++++++++++++- tests/unit_tests/test_image_rois.py | 26 ++++++++ 4 files changed, 134 insertions(+), 2 deletions(-) diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index 338b0035..e545815f 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -530,6 +530,26 @@ class BaseROI(RPCBase): str: The current name of the ROI. """ + @property + @rpc_call + def movable(self) -> "bool": + """ + Gets whether this ROI is movable. + + Returns: + bool: True if the ROI can be moved, False otherwise. + """ + + @movable.setter + @rpc_call + def movable(self) -> "bool": + """ + Gets whether this ROI is movable. + + Returns: + bool: True if the ROI can be moved, False otherwise. + """ + @property @rpc_call def line_color(self) -> "str": @@ -639,6 +659,26 @@ class CircularROI(RPCBase): str: The current name of the ROI. """ + @property + @rpc_call + def movable(self) -> "bool": + """ + Gets whether this ROI is movable. + + Returns: + bool: True if the ROI can be moved, False otherwise. + """ + + @movable.setter + @rpc_call + def movable(self) -> "bool": + """ + Gets whether this ROI is movable. + + Returns: + bool: True if the ROI can be moved, False otherwise. + """ + @property @rpc_call def line_color(self) -> "str": @@ -1494,6 +1534,7 @@ class Image(RPCBase): line_width: "int | None" = 5, pos: "tuple[float, float] | None" = (10, 10), size: "tuple[float, float] | None" = (50, 50), + movable: "bool" = True, **pg_kwargs, ) -> "RectangularROI | CircularROI": """ @@ -1505,6 +1546,7 @@ class Image(RPCBase): line_width(int): The line width of the ROI. pos(tuple): The position of the ROI. size(tuple): The size of the ROI. + movable(bool): Whether the ROI is movable. **pg_kwargs: Additional arguments for the ROI. Returns: @@ -2664,6 +2706,26 @@ class RectangularROI(RPCBase): str: The current name of the ROI. """ + @property + @rpc_call + def movable(self) -> "bool": + """ + Gets whether this ROI is movable. + + Returns: + bool: True if the ROI can be moved, False otherwise. + """ + + @movable.setter + @rpc_call + def movable(self) -> "bool": + """ + Gets whether this ROI is movable. + + Returns: + bool: True if the ROI can be moved, False otherwise. + """ + @property @rpc_call def line_color(self) -> "str": diff --git a/bec_widgets/widgets/plots/image/image_base.py b/bec_widgets/widgets/plots/image/image_base.py index e6bce71e..c2081cdc 100644 --- a/bec_widgets/widgets/plots/image/image_base.py +++ b/bec_widgets/widgets/plots/image/image_base.py @@ -559,6 +559,7 @@ class ImageBase(PlotBase): line_width: int | None = 5, pos: tuple[float, float] | None = (10, 10), size: tuple[float, float] | None = (50, 50), + movable: bool = True, **pg_kwargs, ) -> RectangularROI | CircularROI: """ @@ -570,6 +571,7 @@ class ImageBase(PlotBase): line_width(int): The line width of the ROI. pos(tuple): The position of the ROI. size(tuple): The size of the ROI. + movable(bool): Whether the ROI is movable. **pg_kwargs: Additional arguments for the ROI. Returns: @@ -584,6 +586,7 @@ class ImageBase(PlotBase): parent_image=self, line_width=line_width, label=name, + movable=movable, **pg_kwargs, ) elif kind == "circle": @@ -593,6 +596,7 @@ class ImageBase(PlotBase): parent_image=self, line_width=line_width, label=name, + movable=movable, **pg_kwargs, ) else: diff --git a/bec_widgets/widgets/plots/roi/image_roi.py b/bec_widgets/widgets/plots/roi/image_roi.py index aa578ba3..50eaedf4 100644 --- a/bec_widgets/widgets/plots/roi/image_roi.py +++ b/bec_widgets/widgets/plots/roi/image_roi.py @@ -107,6 +107,8 @@ class BaseROI(BECConnector): USER_ACCESS = [ "label", "label.setter", + "movable", + "movable.setter", "line_color", "line_color.setter", "line_width", @@ -164,12 +166,13 @@ class BaseROI(BECConnector): self._line_color = line_color or "#ffffff" self._line_width = line_width self._description = True - self._movable = True # allow moving by default + self._movable = movable self.setPen(mkPen(self._line_color, width=self._line_width)) # Reset Handles to avoid inherited handles from pyqtgraph self.remove_scale_handles() # remove any existing handles from pyqtgraph.RectROI - self.add_scale_handle() # add custom scale handles + if movable: + self.add_scale_handle() # add custom scale handles def set_parent(self, parent: Image): """ @@ -189,6 +192,39 @@ class BaseROI(BECConnector): """ return self.parent_image + @property + def movable(self) -> bool: + """ + Gets whether this ROI is movable. + + Returns: + bool: True if the ROI can be moved, False otherwise. + """ + return self._movable + + @movable.setter + def movable(self, value: bool): + """ + Sets whether this ROI is movable. + + If the new value is different from the current value, this method updates + the internal state and emits the penChanged signal. + + Args: + value (bool): True to make the ROI movable, False to make it fixed. + """ + if value != self._movable: + self._movable = value + # All relevant properties from pyqtgraph to block movement + self.translatable = value + self.rotatable = value + self.resizable = value + self.removable = value + if value: + self.add_scale_handle() # add custom scale handles + else: + self.remove_scale_handles() # remove custom scale handles + @property def label(self) -> str: """ @@ -411,6 +447,7 @@ class RectangularROI(BaseROI, pg.RectROI): label: str | None = None, line_color: str | None = None, line_width: int = 5, + movable: bool = True, resize_handles: bool = True, **extra_pg, ): @@ -441,6 +478,7 @@ class RectangularROI(BaseROI, pg.RectROI): pos=pos, size=size, pen=pen, + movable=movable, **extra_pg, ) @@ -588,6 +626,7 @@ class CircularROI(BaseROI, pg.CircleROI): label: str | None = None, line_color: str | None = None, line_width: int = 5, + movable: bool = True, **extra_pg, ): """ @@ -619,6 +658,7 @@ class CircularROI(BaseROI, pg.CircleROI): pos=pos, size=size, pen=pen, + movable=movable, **extra_pg, ) self.sigRegionChanged.connect(self._on_region_changed) diff --git a/tests/unit_tests/test_image_rois.py b/tests/unit_tests/test_image_rois.py index 646a8a9d..01728e05 100644 --- a/tests/unit_tests/test_image_rois.py +++ b/tests/unit_tests/test_image_rois.py @@ -205,3 +205,29 @@ def test_roi_set_position(bec_image_widget_with_roi): pos = roi.pos() assert int(pos.x()) == 10 assert int(pos.y()) == 15 + + +def test_roi_movable_property(bec_image_widget_with_roi, qtbot): + """Verify BaseROI.movable toggles flags, handles, and emits a signal.""" + _widget, roi, _ = bec_image_widget_with_roi + + # defaults – ROI is movable + assert roi.movable + assert roi.translatable and roi.rotatable and roi.resizable and roi.removable + assert len(roi.handles) > 0 + + # lock it + with qtbot.waitSignal(roi.movableChanged) as blocker: + roi.movable = False + assert blocker.args == [False] + assert not roi.movable + assert not (roi.translatable or roi.rotatable or roi.resizable or roi.removable) + assert len(roi.handles) == 0 + + # unlock again + with qtbot.waitSignal(roi.movableChanged) as blocker: + roi.movable = True + assert blocker.args == [True] + assert roi.movable + assert roi.translatable and roi.rotatable and roi.resizable and roi.removable + assert len(roi.handles) > 0