diff --git a/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py b/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py index c5d7ebc8..6ed92c3c 100644 --- a/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +++ b/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py @@ -5,10 +5,10 @@ It is a preliminary version of the GUI, which will be added to the main branch a import os from typing import Optional -from bec_lib.device import Positioner, Signal +from bec_lib.device import Positioner as BECPositioner +from bec_lib.device import Signal as BECSignal from bec_lib.endpoints import MessageEndpoints -from qtpy.QtCore import QSize -from qtpy.QtCore import Signal as pyqtSignal +from qtpy.QtCore import QSize, Signal from qtpy.QtGui import QIcon from qtpy.QtWidgets import QCheckBox, QDoubleSpinBox, QPushButton, QSpinBox, QVBoxLayout, QWidget @@ -33,7 +33,7 @@ class Alignment1D(BECWidget, QWidget): """Alignment GUI to perform 1D scans""" # Emit a signal when a motion is ongoing - motion_is_active = pyqtSignal(bool) + motion_is_active = Signal(bool) def __init__( self, parent: Optional[QWidget] = None, client=None, gui_id: Optional[str] = None @@ -198,14 +198,14 @@ class Alignment1D(BECWidget, QWidget): def _setup_motor_combobox(self) -> None: """Setup motor selection""" # FIXME after changing the filtering in the combobox - motors = [name for name in self.dev if isinstance(self.dev.get(name), Positioner)] + motors = [name for name in self.dev if isinstance(self.dev.get(name), BECPositioner)] self.ui.device_combobox.setCurrentText(motors[0]) self.ui.device_combobox.set_device_filter("Positioner") def _setup_signal_combobox(self) -> None: """Setup signal selection""" # FIXME after changing the filtering in the combobox - signals = [name for name in self.dev if isinstance(self.dev.get(name), Signal)] + signals = [name for name in self.dev if isinstance(self.dev.get(name), BECSignal)] self.ui.device_combobox_2.setCurrentText(signals[0]) self.ui.device_combobox_2.set_device_filter("Signal") diff --git a/bec_widgets/utils/bec_signal_proxy.py b/bec_widgets/utils/bec_signal_proxy.py new file mode 100644 index 00000000..949e3def --- /dev/null +++ b/bec_widgets/utils/bec_signal_proxy.py @@ -0,0 +1,54 @@ +""" This custom class is a thin wrapper around the SignalProxy class to allow signal calls to be blocked. +Unblocking the proxy needs to be done through the slot unblock_proxy. The most likely use case for this class is +when the callback function is potentially initiating a slower progress, i.e. requesting a data analysis routine to +analyse data. Requesting a new fit may lead to request piling up and an overall slow done of performance. This proxy +will allow you to decide by yourself when to unblock and execute the callback again.""" + +from pyqtgraph import SignalProxy +from qtpy.QtCore import Signal, Slot + + +class BECSignalProxy(SignalProxy): + """Thin wrapper around the SignalProxy class to allow signal calls to be blocked, but args still being stored + + Args: + *args: Arguments to pass to the SignalProxy class + rateLimit (int): The rateLimit of the proxy + **kwargs: Keyword arguments to pass to the SignalProxy class + + Example: + >>> proxy = BECSignalProxy(signal, rate_limit=25, slot=callback)""" + + is_blocked = Signal(bool) + + def __init__(self, *args, rateLimit=25, **kwargs): + super().__init__(*args, rateLimit=rateLimit, **kwargs) + self._blocking = False + self.old_args = None + self.new_args = None + + @property + def blocked(self): + """Returns if the proxy is blocked""" + return self._blocking + + @blocked.setter + def blocked(self, value: bool): + self._blocking = value + self.is_blocked.emit(value) + + def signalReceived(self, *args): + """Receive signal, store the args and call signalReceived from the parent class if not blocked""" + self.new_args = args + if self.blocked is True: + return + self.blocked = True + self.old_args = args + super().signalReceived(*args) + + @Slot() + def unblock_proxy(self): + """Unblock the proxy, and call the signalReceived method in case there was an update of the args.""" + self.blocked = False + if self.new_args != self.old_args: + self.signalReceived(*self.new_args) diff --git a/bec_widgets/utils/plot_indicator_items.py b/bec_widgets/utils/plot_indicator_items.py index 46017737..c32a3941 100644 --- a/bec_widgets/utils/plot_indicator_items.py +++ b/bec_widgets/utils/plot_indicator_items.py @@ -241,12 +241,6 @@ class BECArrowItem(BECIndicatorItem): self.plot_item.removeItem(self.arrow_item) self.item_on_plot = False - def check_log(self): - """Checks if the x or y axis is in log scale and updates the internal state accordingly.""" - self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked() - self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked() - self.set_position(self._pos) - def cleanup(self) -> None: """Cleanup the item""" self.remove_from_plot() diff --git a/bec_widgets/widgets/figure/plots/waveform/waveform.py b/bec_widgets/widgets/figure/plots/waveform/waveform.py index 0c9fd81e..4b6571a7 100644 --- a/bec_widgets/widgets/figure/plots/waveform/waveform.py +++ b/bec_widgets/widgets/figure/plots/waveform/waveform.py @@ -30,31 +30,6 @@ from bec_widgets.widgets.figure.plots.waveform.waveform_curve import ( logger = bec_logger.logger -class BECSignalProxy(pg.SignalProxy): - """Thin wrapper around the SignalProxy class to allow signal calls to be blocked, but args still being stored""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.blocking = False - - def signalReceived(self, *args): - """Received signa, but store the args""" - self.args = args - if self.blocking: - return - # self.blocking = True - super().signalReceived(*args) - - # def flush(self): - # """If there is a signal queued send it out""" - # super().flush() - - @Slot() - def unblock_proxy(self): - """Unblock the proxy""" - self.blocking = False - - class Waveform1DConfig(SubplotConfig): color_palette: Optional[str] = Field( "magma", description="The color palette of the figure widget.", validate_default=True @@ -147,7 +122,7 @@ class BECWaveform(BECPlotBase): self.proxy_update_plot = pg.SignalProxy( self.scan_signal_update, rateLimit=25, slot=self._update_scan_curves ) - self.proxy_update_dap = BECSignalProxy( + self.proxy_update_dap = pg.SignalProxy( self.scan_signal_update, rateLimit=25, slot=self.refresh_dap ) self.async_signal_update.connect(self.replot_async_curve) @@ -1211,8 +1186,6 @@ class BECWaveform(BECPlotBase): @Slot(dict, dict) def update_dap(self, msg, metadata): """Callback for DAP response message.""" - if self.proxy_update_dap is not None: - self.proxy_update_dap.unblock_proxy() # pylint: disable=unused-variable scan_id, x_name, x_entry, y_name, y_entry = msg["dap_request"].content["config"]["args"] diff --git a/bec_widgets/widgets/stop_button/stop_button.py b/bec_widgets/widgets/stop_button/stop_button.py index dbddb226..1a5a042c 100644 --- a/bec_widgets/widgets/stop_button/stop_button.py +++ b/bec_widgets/widgets/stop_button/stop_button.py @@ -50,7 +50,7 @@ class StopButton(BECWidget, QWidget): self.queue.request_scan_halt() -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover import sys from qtpy.QtWidgets import QApplication diff --git a/tests/unit_tests/test_utils_bec_signal_proxy.py b/tests/unit_tests/test_utils_bec_signal_proxy.py new file mode 100644 index 00000000..0b73f85c --- /dev/null +++ b/tests/unit_tests/test_utils_bec_signal_proxy.py @@ -0,0 +1,75 @@ +from unittest import mock + +import pyqtgraph as pg +import pytest + +from bec_widgets.utils.bec_signal_proxy import BECSignalProxy +from bec_widgets.widgets.dap_combo_box.dap_combo_box import DapComboBox + +from .client_mocks import mocked_client +from .conftest import create_widget + + +@pytest.fixture +def dap_combo_box(qtbot, mocked_client): + """Fixture for TextBox widget to test BECSignalProxy with a simple widget""" + with mock.patch( + "bec_widgets.widgets.dap_combo_box.dap_combo_box.DapComboBox._validate_dap_model", + return_value=True, + ): + widget = create_widget(qtbot, DapComboBox, client=mocked_client) + yield widget + + +def test_bec_signal_proxy(qtbot, dap_combo_box): + """Test BECSignalProxy""" + proxy_container = [] + + def proxy_callback(*args): + """A simple callback function for the proxy""" + proxy_container.append(args) + + container = [] + + def all_callbacks(*args): + """A simple callback function for all signal calls""" + container.append(args) + + proxy = BECSignalProxy(dap_combo_box.x_axis_updated, rateLimit=25, slot=proxy_callback) + pg_proxy = pg.SignalProxy(dap_combo_box.x_axis_updated, rateLimit=25, slot=all_callbacks) + qtbot.wait(200) + # Test that the proxy is blocked + assert container == [] + assert proxy_container == [] + # Set first value + dap_combo_box.x_axis = "samx" + qtbot.waitSignal(dap_combo_box.x_axis_updated, timeout=1000) + qtbot.wait(100) + assert container == [(("samx",),)] + assert proxy.blocked is True + assert proxy_container == [(("samx",),)] + # Set new value samy + dap_combo_box.x_axis = "samy" + qtbot.waitSignal(dap_combo_box.x_axis_updated, timeout=1000) + qtbot.wait(100) + assert container == [(("samx",),), (("samy",),)] + assert proxy.blocked is True + assert proxy_container == [(("samx",),)] + # Set new value samz + dap_combo_box.x_axis = "samz" + qtbot.waitSignal(dap_combo_box.x_axis_updated, timeout=1000) + qtbot.wait(100) + assert container == [(("samx",),), (("samy",),), (("samz",),)] + assert proxy.blocked is True + proxy.unblock_proxy() + qtbot.waitSignal(proxy.is_blocked, timeout=1000) + qtbot.wait(100) + assert proxy.blocked is True + assert proxy_container == [(("samx",),), (("samz",),)] + # Unblock the proxy again, no new argument received. + # The callback should not be called again. + proxy.unblock_proxy() + qtbot.waitSignal(proxy.is_blocked, timeout=1000) + qtbot.wait(100) + assert proxy.blocked is False + assert proxy_container == [(("samx",),), (("samz",),)]