mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-03-04 16:02:51 +01:00
feat(plot_base): plot_base, image and heatmap widget adopted to property-toolbar sync
This commit is contained in:
@@ -1453,7 +1453,7 @@ class Heatmap(ImageBase):
|
||||
# Post Processing
|
||||
################################################################################
|
||||
|
||||
@SafeProperty(bool)
|
||||
@SafeProperty(bool, auto_emit=True)
|
||||
def fft(self) -> bool:
|
||||
"""
|
||||
Whether FFT postprocessing is enabled.
|
||||
@@ -1470,7 +1470,7 @@ class Heatmap(ImageBase):
|
||||
"""
|
||||
self.main_image.fft = enable
|
||||
|
||||
@SafeProperty(bool)
|
||||
@SafeProperty(bool, auto_emit=True)
|
||||
def log(self) -> bool:
|
||||
"""
|
||||
Whether logarithmic scaling is applied.
|
||||
@@ -1504,7 +1504,7 @@ class Heatmap(ImageBase):
|
||||
"""
|
||||
self.main_image.num_rotation_90 = value
|
||||
|
||||
@SafeProperty(bool)
|
||||
@SafeProperty(bool, auto_emit=True)
|
||||
def transpose(self) -> bool:
|
||||
"""
|
||||
Whether the image is transposed.
|
||||
|
||||
@@ -336,7 +336,7 @@ class Image(ImageBase):
|
||||
# Post Processing
|
||||
################################################################################
|
||||
|
||||
@SafeProperty(bool)
|
||||
@SafeProperty(bool, auto_emit=True)
|
||||
def fft(self) -> bool:
|
||||
"""
|
||||
Whether FFT postprocessing is enabled.
|
||||
@@ -353,7 +353,7 @@ class Image(ImageBase):
|
||||
"""
|
||||
self.main_image.fft = enable
|
||||
|
||||
@SafeProperty(bool)
|
||||
@SafeProperty(bool, auto_emit=True)
|
||||
def log(self) -> bool:
|
||||
"""
|
||||
Whether logarithmic scaling is applied.
|
||||
@@ -387,7 +387,7 @@ class Image(ImageBase):
|
||||
"""
|
||||
self.main_image.num_rotation_90 = value
|
||||
|
||||
@SafeProperty(bool)
|
||||
@SafeProperty(bool, auto_emit=True)
|
||||
def transpose(self) -> bool:
|
||||
"""
|
||||
Whether the image is transposed.
|
||||
|
||||
@@ -300,9 +300,14 @@ def image_processing(components: ToolbarComponents) -> ToolbarBundle:
|
||||
class ImageProcessingConnection(BundleConnection):
|
||||
"""
|
||||
Connection class for the image processing toolbar bundle.
|
||||
|
||||
Provides bidirectional synchronization between toolbar actions and widget properties:
|
||||
- Toolbar clicks → Update properties
|
||||
- Property changes → Update toolbar (via property_changed signal)
|
||||
"""
|
||||
|
||||
def __init__(self, components: ToolbarComponents, target_widget=None):
|
||||
super().__init__(parent=components.toolbar)
|
||||
self.bundle_name = "image_processing"
|
||||
self.components = components
|
||||
self.target_widget = target_widget
|
||||
@@ -315,7 +320,6 @@ class ImageProcessingConnection(BundleConnection):
|
||||
raise AttributeError(
|
||||
"Target widget must implement 'fft', 'log', 'transpose', and 'num_rotation_90' attributes."
|
||||
)
|
||||
super().__init__()
|
||||
self.fft = components.get_action("image_processing_fft")
|
||||
self.log = components.get_action("image_processing_log")
|
||||
self.transpose = components.get_action("image_processing_transpose")
|
||||
@@ -324,6 +328,11 @@ class ImageProcessingConnection(BundleConnection):
|
||||
self.reset = components.get_action("image_processing_reset")
|
||||
self._connected = False
|
||||
|
||||
# Register property sync methods for bidirectional sync
|
||||
self.register_checked_action_sync("fft", self.fft)
|
||||
self.register_checked_action_sync("log", self.log)
|
||||
self.register_checked_action_sync("transpose", self.transpose)
|
||||
|
||||
@SafeSlot()
|
||||
def toggle_fft(self):
|
||||
checked = self.fft.action.isChecked()
|
||||
@@ -367,8 +376,11 @@ class ImageProcessingConnection(BundleConnection):
|
||||
def connect(self):
|
||||
"""
|
||||
Connect the actions to the target widget's methods.
|
||||
Enables bidirectional sync: toolbar ↔ properties.
|
||||
"""
|
||||
self._connected = True
|
||||
|
||||
# Toolbar → Property connections
|
||||
self.fft.action.triggered.connect(self.toggle_fft)
|
||||
self.log.action.triggered.connect(self.toggle_log)
|
||||
self.transpose.action.triggered.connect(self.toggle_transpose)
|
||||
@@ -376,15 +388,25 @@ class ImageProcessingConnection(BundleConnection):
|
||||
self.left.action.triggered.connect(self.rotate_left)
|
||||
self.reset.action.triggered.connect(self.reset_settings)
|
||||
|
||||
# Property → Toolbar connections
|
||||
self.connect_property_sync(self.target_widget)
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
Disconnect the actions from the target widget's methods.
|
||||
"""
|
||||
if not self._connected:
|
||||
return
|
||||
|
||||
# Disconnect toolbar → property
|
||||
self.fft.action.triggered.disconnect(self.toggle_fft)
|
||||
self.log.action.triggered.disconnect(self.toggle_log)
|
||||
self.transpose.action.triggered.disconnect(self.toggle_transpose)
|
||||
self.right.action.triggered.disconnect(self.rotate_right)
|
||||
self.left.action.triggered.disconnect(self.rotate_left)
|
||||
self.reset.action.triggered.disconnect(self.reset_settings)
|
||||
|
||||
# Disconnect property → toolbar
|
||||
self.disconnect_property_sync(self.target_widget)
|
||||
|
||||
self._connected = False
|
||||
|
||||
@@ -446,7 +446,7 @@ class PlotBase(BECWidget, QWidget):
|
||||
else:
|
||||
logger.warning(f"Property {key} not found.")
|
||||
|
||||
@SafeProperty(str, doc="The title of the axes.")
|
||||
@SafeProperty(str, auto_emit=True, doc="The title of the axes.")
|
||||
def title(self) -> str:
|
||||
"""
|
||||
Set title of the plot.
|
||||
@@ -462,9 +462,8 @@ class PlotBase(BECWidget, QWidget):
|
||||
value(str): The title to set.
|
||||
"""
|
||||
self.plot_item.setTitle(value)
|
||||
self.property_changed.emit("title", value)
|
||||
|
||||
@SafeProperty(str, doc="The text of the x label")
|
||||
@SafeProperty(str, auto_emit=True, doc="The text of the x label")
|
||||
def x_label(self) -> str:
|
||||
"""
|
||||
The set label for the x-axis.
|
||||
@@ -481,7 +480,6 @@ class PlotBase(BECWidget, QWidget):
|
||||
"""
|
||||
self._user_x_label = value
|
||||
self._apply_x_label()
|
||||
self.property_changed.emit("x_label", self._user_x_label)
|
||||
|
||||
@property
|
||||
def x_label_suffix(self) -> str:
|
||||
@@ -535,7 +533,7 @@ class PlotBase(BECWidget, QWidget):
|
||||
if self.plot_item.getAxis("bottom").isVisible():
|
||||
self.plot_item.setLabel("bottom", text=final_label)
|
||||
|
||||
@SafeProperty(str, doc="The text of the y label")
|
||||
@SafeProperty(str, auto_emit=True, doc="The text of the y label")
|
||||
def y_label(self) -> str:
|
||||
"""
|
||||
The set label for the y-axis.
|
||||
@@ -551,7 +549,6 @@ class PlotBase(BECWidget, QWidget):
|
||||
"""
|
||||
self._user_y_label = value
|
||||
self._apply_y_label()
|
||||
self.property_changed.emit("y_label", value)
|
||||
|
||||
@property
|
||||
def y_label_suffix(self) -> str:
|
||||
@@ -772,7 +769,7 @@ class PlotBase(BECWidget, QWidget):
|
||||
"""
|
||||
self.y_limits = (self.y_lim[0], value)
|
||||
|
||||
@SafeProperty(bool, doc="Show grid on the x-axis.")
|
||||
@SafeProperty(bool, auto_emit=True, doc="Show grid on the x-axis.")
|
||||
def x_grid(self) -> bool:
|
||||
"""
|
||||
Show grid on the x-axis.
|
||||
@@ -788,9 +785,8 @@ class PlotBase(BECWidget, QWidget):
|
||||
value(bool): The value to set.
|
||||
"""
|
||||
self.plot_item.showGrid(x=value)
|
||||
self.property_changed.emit("x_grid", value)
|
||||
|
||||
@SafeProperty(bool, doc="Show grid on the y-axis.")
|
||||
@SafeProperty(bool, auto_emit=True, doc="Show grid on the y-axis.")
|
||||
def y_grid(self) -> bool:
|
||||
"""
|
||||
Show grid on the y-axis.
|
||||
@@ -806,9 +802,8 @@ class PlotBase(BECWidget, QWidget):
|
||||
value(bool): The value to set.
|
||||
"""
|
||||
self.plot_item.showGrid(y=value)
|
||||
self.property_changed.emit("y_grid", value)
|
||||
|
||||
@SafeProperty(bool, doc="Set X-axis to log scale if True, linear if False.")
|
||||
@SafeProperty(bool, auto_emit=True, doc="Set X-axis to log scale if True, linear if False.")
|
||||
def x_log(self) -> bool:
|
||||
"""
|
||||
Set X-axis to log scale if True, linear if False.
|
||||
@@ -824,9 +819,8 @@ class PlotBase(BECWidget, QWidget):
|
||||
value(bool): The value to set.
|
||||
"""
|
||||
self.plot_item.setLogMode(x=value)
|
||||
self.property_changed.emit("x_log", value)
|
||||
|
||||
@SafeProperty(bool, doc="Set Y-axis to log scale if True, linear if False.")
|
||||
@SafeProperty(bool, auto_emit=True, doc="Set Y-axis to log scale if True, linear if False.")
|
||||
def y_log(self) -> bool:
|
||||
"""
|
||||
Set Y-axis to log scale if True, linear if False.
|
||||
@@ -842,9 +836,8 @@ class PlotBase(BECWidget, QWidget):
|
||||
value(bool): The value to set.
|
||||
"""
|
||||
self.plot_item.setLogMode(y=value)
|
||||
self.property_changed.emit("y_log", value)
|
||||
|
||||
@SafeProperty(bool, doc="Show the outer axes of the plot widget.")
|
||||
@SafeProperty(bool, auto_emit=True, doc="Show the outer axes of the plot widget.")
|
||||
def outer_axes(self) -> bool:
|
||||
"""
|
||||
Show the outer axes of the plot widget.
|
||||
@@ -863,9 +856,8 @@ class PlotBase(BECWidget, QWidget):
|
||||
self.plot_item.showAxis("right", value)
|
||||
|
||||
self._outer_axes_visible = value
|
||||
self.property_changed.emit("outer_axes", value)
|
||||
|
||||
@SafeProperty(bool, doc="Show inner axes of the plot widget.")
|
||||
@SafeProperty(bool, auto_emit=True, doc="Show inner axes of the plot widget.")
|
||||
def inner_axes(self) -> bool:
|
||||
"""
|
||||
Show inner axes of the plot widget.
|
||||
@@ -886,7 +878,6 @@ class PlotBase(BECWidget, QWidget):
|
||||
self._inner_axes_visible = value
|
||||
self._apply_x_label()
|
||||
self._apply_y_label()
|
||||
self.property_changed.emit("inner_axes", value)
|
||||
|
||||
@SafeProperty(bool, doc="Invert X axis.")
|
||||
def invert_x(self) -> bool:
|
||||
@@ -1110,7 +1101,9 @@ class PlotBase(BECWidget, QWidget):
|
||||
self.unhook_crosshair()
|
||||
|
||||
@SafeProperty(
|
||||
int, doc="Minimum decimal places for crosshair when dynamic precision is enabled."
|
||||
int,
|
||||
auto_emit=True,
|
||||
doc="Minimum decimal places for crosshair when dynamic precision is enabled.",
|
||||
)
|
||||
def minimal_crosshair_precision(self) -> int:
|
||||
"""
|
||||
@@ -1130,7 +1123,6 @@ class PlotBase(BECWidget, QWidget):
|
||||
self._minimal_crosshair_precision = value_int
|
||||
if self.crosshair is not None:
|
||||
self.crosshair.min_precision = value_int
|
||||
self.property_changed.emit("minimal_crosshair_precision", value_int)
|
||||
|
||||
@SafeSlot()
|
||||
def reset(self) -> None:
|
||||
|
||||
@@ -834,6 +834,24 @@ def test_device_properties_property_changed_signal(heatmap_widget):
|
||||
mock_handler.assert_any_call("x_device_name", "samx")
|
||||
|
||||
|
||||
def test_auto_emit_syncs_heatmap_toolbar_actions(heatmap_widget):
|
||||
from unittest.mock import Mock
|
||||
|
||||
fft_action = heatmap_widget.toolbar.components.get_action("image_processing_fft").action
|
||||
log_action = heatmap_widget.toolbar.components.get_action("image_processing_log").action
|
||||
|
||||
mock_handler = Mock()
|
||||
heatmap_widget.property_changed.connect(mock_handler)
|
||||
|
||||
heatmap_widget.fft = True
|
||||
heatmap_widget.log = True
|
||||
|
||||
assert fft_action.isChecked()
|
||||
assert log_action.isChecked()
|
||||
mock_handler.assert_any_call("fft", True)
|
||||
mock_handler.assert_any_call("log", True)
|
||||
|
||||
|
||||
def test_device_entry_validation_with_invalid_device(heatmap_widget):
|
||||
"""Test that invalid device names are handled gracefully."""
|
||||
# Try to set invalid device name
|
||||
|
||||
@@ -249,6 +249,31 @@ def test_toolbar_actions_presence(qtbot, mocked_client):
|
||||
assert bec_image_view.toolbar.components.exists("image_dim_combo")
|
||||
|
||||
|
||||
def test_auto_emit_syncs_image_toolbar_actions(qtbot, mocked_client):
|
||||
from unittest.mock import Mock
|
||||
|
||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||
fft_action = bec_image_view.toolbar.components.get_action("image_processing_fft").action
|
||||
log_action = bec_image_view.toolbar.components.get_action("image_processing_log").action
|
||||
transpose_action = bec_image_view.toolbar.components.get_action(
|
||||
"image_processing_transpose"
|
||||
).action
|
||||
|
||||
mock_handler = Mock()
|
||||
bec_image_view.property_changed.connect(mock_handler)
|
||||
|
||||
bec_image_view.fft = True
|
||||
bec_image_view.log = True
|
||||
bec_image_view.transpose = True
|
||||
|
||||
assert fft_action.isChecked()
|
||||
assert log_action.isChecked()
|
||||
assert transpose_action.isChecked()
|
||||
mock_handler.assert_any_call("fft", True)
|
||||
mock_handler.assert_any_call("log", True)
|
||||
mock_handler.assert_any_call("transpose", True)
|
||||
|
||||
|
||||
def test_image_processing_fft_toggle(qtbot, mocked_client):
|
||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||
bec_image_view.fft = True
|
||||
|
||||
Reference in New Issue
Block a user