1
0
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:
2026-01-20 12:20:10 +01:00
committed by Jan Wyzula
parent 3b7bad85d3
commit 24dbb885f6
6 changed files with 84 additions and 27 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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