From cc691d4039bde710e78f362d2f0e712f9e8f196f Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 2 Sep 2024 17:09:58 +0200 Subject: [PATCH] feat: add dap_combobox --- bec_widgets/cli/client.py | 30 +++ bec_widgets/widgets/dap_combo_box/__init__.py | 0 .../widgets/dap_combo_box/dap_combo_box.py | 176 ++++++++++++++++++ .../dap_combo_box/dap_combo_box.pyproject | 1 + .../dap_combo_box/dap_combo_box_plugin.py | 54 ++++++ .../dap_combo_box/register_dap_combo_box.py | 15 ++ .../widgets/waveform/waveform_widget.py | 1 - .../widget_screenshots/dap_combo_box.png | Bin 0 -> 10649 bytes .../widgets/dap_combo_box/dap_combo_box.md | 46 +++++ .../user/widgets/lmfit_dialog/lmfit_dialog.md | 2 +- docs/user/widgets/widgets.md | 9 + tests/unit_tests/test_dap_combobox.py | 75 ++++++++ 12 files changed, 407 insertions(+), 2 deletions(-) create mode 100644 bec_widgets/widgets/dap_combo_box/__init__.py create mode 100644 bec_widgets/widgets/dap_combo_box/dap_combo_box.py create mode 100644 bec_widgets/widgets/dap_combo_box/dap_combo_box.pyproject create mode 100644 bec_widgets/widgets/dap_combo_box/dap_combo_box_plugin.py create mode 100644 bec_widgets/widgets/dap_combo_box/register_dap_combo_box.py create mode 100644 docs/assets/widget_screenshots/dap_combo_box.png create mode 100644 docs/user/widgets/dap_combo_box/dap_combo_box.md create mode 100644 tests/unit_tests/test_dap_combobox.py diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index 05886513..dcbb3b9d 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -22,6 +22,7 @@ class Widgets(str, enum.Enum): BECQueue = "BECQueue" BECStatusBox = "BECStatusBox" BECWaveformWidget = "BECWaveformWidget" + DapComboBox = "DapComboBox" DarkModeButton = "DarkModeButton" DeviceBrowser = "DeviceBrowser" DeviceComboBox = "DeviceComboBox" @@ -2312,6 +2313,35 @@ class BECWaveformWidget(RPCBase): """ +class DapComboBox(RPCBase): + @rpc_call + def select_y_axis(self, y_axis: str): + """ + Receive update signal for the y axis. + + Args: + y_axis(str): Y axis. + """ + + @rpc_call + def select_x_axis(self, x_axis: str): + """ + Receive update signal for the x axis. + + Args: + x_axis(str): X axis. + """ + + @rpc_call + def select_fit(self, fit_name: str | None): + """ + Select current fit. + + Args: + default_device(str): Default device name. + """ + + class DarkModeButton(RPCBase): @rpc_call def toggle_dark_mode(self) -> "None": diff --git a/bec_widgets/widgets/dap_combo_box/__init__.py b/bec_widgets/widgets/dap_combo_box/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bec_widgets/widgets/dap_combo_box/dap_combo_box.py b/bec_widgets/widgets/dap_combo_box/dap_combo_box.py new file mode 100644 index 00000000..ba6f1b6c --- /dev/null +++ b/bec_widgets/widgets/dap_combo_box/dap_combo_box.py @@ -0,0 +1,176 @@ +""" Module for DapComboBox widget class to select a DAP model from a combobox. """ + +from bec_lib.logger import bec_logger +from qtpy.QtCore import Property, Signal, Slot +from qtpy.QtWidgets import QComboBox, QVBoxLayout, QWidget + +from bec_widgets.utils.bec_widget import BECWidget + +logger = bec_logger.logger + + +class DapComboBox(BECWidget, QWidget): + """ + ComboBox widget for device input with autocomplete for device names. + + Args: + parent: Parent widget. + client: BEC client object. + gui_id: GUI ID. + default: Default device name. + """ + + ICON_NAME = "data_exploration" + + USER_ACCESS = ["select_y_axis", "select_x_axis", "select_fit"] + + add_dap_model = Signal(str, str, str) + update_x_axis = Signal(str) + update_y_axis = Signal(str) + update_fit_model = Signal(str) + + def __init__( + self, parent=None, client=None, gui_id: str | None = None, default_fit: str | None = None + ): + super().__init__(client=client, gui_id=gui_id) + QWidget.__init__(self, parent=parent) + self.layout = QVBoxLayout(self) + self.combobox = QComboBox(self) + self.layout.addWidget(self.combobox) + self.layout.setContentsMargins(0, 0, 0, 0) + self._available_models = None + self._x_axis = None + self._y_axis = None + self.populate_combobox() + self.combobox.currentTextChanged.connect(self._update_current_fit) + # Set default fit model + self.select_default_fit(default_fit) + + def select_default_fit(self, default_fit: str | None): + """Set the default fit model. + + Args: + default_fit(str): Default fit model. + """ + if self._validate_dap_model(default_fit): + self.select_fit(default_fit) + else: + self.select_fit("GaussianModel") + + @property + def available_models(self): + """Available models property.""" + return self._available_models + + @available_models.setter + def available_models(self, available_models: list[str]): + """Set the available models. + + Args: + available_models(list[str]): Available models. + """ + self._available_models = available_models + + @Property(str) + def x_axis(self): + """X axis property.""" + return self._x_axis + + @x_axis.setter + def x_axis(self, x_axis: str): + """Set the x axis. + + Args: + x_axis(str): X axis. + """ + # TODO add validator for x axis -> Positioner? or also device (must be monitored)!! + self._x_axis = x_axis + self.update_x_axis.emit(x_axis) + + @Property(str) + def y_axis(self): + """Y axis property.""" + # TODO add validator for y axis -> Positioner & Device? Must be a monitored device!! + return self._y_axis + + @y_axis.setter + def y_axis(self, y_axis: str): + """Set the y axis. + + Args: + y_axis(str): Y axis. + """ + self._y_axis = y_axis + self.update_y_axis.emit(y_axis) + + def _update_current_fit(self, fit_name: str): + """Update the current fit.""" + self.update_fit_model.emit(fit_name) + if self.x_axis is not None and self.y_axis is not None: + self.add_dap_model.emit(self._x_axis, self._y_axis, fit_name) + + @Slot(str) + def select_x_axis(self, x_axis: str): + """Receive update signal for the x axis. + + Args: + x_axis(str): X axis. + """ + self.x_axis = x_axis + + @Slot(str) + def select_y_axis(self, y_axis: str): + """Receive update signal for the y axis. + + Args: + y_axis(str): Y axis. + """ + self.y_axis = y_axis + + @Slot(str) + def select_fit(self, fit_name: str | None): + """ + Select current fit. + + Args: + default_device(str): Default device name. + """ + if not self._validate_dap_model(fit_name): + raise ValueError(f"Fit {fit_name} is not valid.") + self.combobox.setCurrentText(fit_name) + + def populate_combobox(self): + """Populate the combobox with the devices.""" + self.available_models = [model for model in self.client.dap._available_dap_plugins.keys()] + self.combobox.clear() + self.combobox.addItems(self.available_models) + + def _validate_dap_model(self, model: str | None) -> bool: + """Validate the DAP model. + + Args: + model(str): Model name. + """ + if model is None: + return False + if model not in self.available_models: + return False + return True + + +def main(): + import sys + + from qtpy.QtWidgets import QApplication + + from bec_widgets.utils.colors import set_theme + + app = QApplication(sys.argv) + set_theme("auto") + widget = DapComboBox() + widget.show() + sys.exit(app.exec_()) + + +if __name__ == "__main__": + main() diff --git a/bec_widgets/widgets/dap_combo_box/dap_combo_box.pyproject b/bec_widgets/widgets/dap_combo_box/dap_combo_box.pyproject new file mode 100644 index 00000000..b1ed5a51 --- /dev/null +++ b/bec_widgets/widgets/dap_combo_box/dap_combo_box.pyproject @@ -0,0 +1 @@ +{'files': ['dap_combo_box.py']} \ No newline at end of file diff --git a/bec_widgets/widgets/dap_combo_box/dap_combo_box_plugin.py b/bec_widgets/widgets/dap_combo_box/dap_combo_box_plugin.py new file mode 100644 index 00000000..30ea3898 --- /dev/null +++ b/bec_widgets/widgets/dap_combo_box/dap_combo_box_plugin.py @@ -0,0 +1,54 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from qtpy.QtDesigner import QDesignerCustomWidgetInterface + +from bec_widgets.utils.bec_designer import designer_material_icon +from bec_widgets.widgets.dap_combo_box.dap_combo_box import DapComboBox + +DOM_XML = """ + + + + +""" + + +class DapComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover + def __init__(self): + super().__init__() + self._form_editor = None + + def createWidget(self, parent): + t = DapComboBox(parent) + return t + + def domXml(self): + return DOM_XML + + def group(self): + return "BEC Selection Widgets" + + def icon(self): + return designer_material_icon(DapComboBox.ICON_NAME) + + def includeFile(self): + return "dap_combo_box" + + def initialize(self, form_editor): + self._form_editor = form_editor + + def isContainer(self): + return False + + def isInitialized(self): + return self._form_editor is not None + + def name(self): + return "DapComboBox" + + def toolTip(self): + return "" + + def whatsThis(self): + return self.toolTip() diff --git a/bec_widgets/widgets/dap_combo_box/register_dap_combo_box.py b/bec_widgets/widgets/dap_combo_box/register_dap_combo_box.py new file mode 100644 index 00000000..c3865500 --- /dev/null +++ b/bec_widgets/widgets/dap_combo_box/register_dap_combo_box.py @@ -0,0 +1,15 @@ +def main(): # pragma: no cover + from qtpy import PYSIDE6 + + if not PYSIDE6: + print("PYSIDE6 is not available in the environment. Cannot patch designer.") + return + from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection + + from bec_widgets.widgets.dap_combo_box.dap_combo_box_plugin import DapComboBoxPlugin + + QPyDesignerCustomWidgetCollection.addCustomWidget(DapComboBoxPlugin()) + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/bec_widgets/widgets/waveform/waveform_widget.py b/bec_widgets/widgets/waveform/waveform_widget.py index 36ecae0b..e3be47fe 100644 --- a/bec_widgets/widgets/waveform/waveform_widget.py +++ b/bec_widgets/widgets/waveform/waveform_widget.py @@ -343,7 +343,6 @@ class BECWaveformWidget(BECWidget, QWidget): x_entry: str | None = None, y_entry: str | None = None, color: str | None = None, - # dap: str = "GaussianModel", validate_bec: bool = True, **kwargs, ) -> BECCurve: diff --git a/docs/assets/widget_screenshots/dap_combo_box.png b/docs/assets/widget_screenshots/dap_combo_box.png new file mode 100644 index 0000000000000000000000000000000000000000..b5565a2e961ee87ec07f5ee4e0219ab44b989194 GIT binary patch literal 10649 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 F|9?)gs;B?} literal 0 HcmV?d00001 diff --git a/docs/user/widgets/dap_combo_box/dap_combo_box.md b/docs/user/widgets/dap_combo_box/dap_combo_box.md new file mode 100644 index 00000000..8ec91f6c --- /dev/null +++ b/docs/user/widgets/dap_combo_box/dap_combo_box.md @@ -0,0 +1,46 @@ +(user.widgets.dap_combo_box)= + +# DAP Combobox + +````{tab} Overview + +The [`DAP ComboBox`](/api_reference/_autosummary/bec_widgets.widgets.dap_combo_box.dap_combo_box.DAPComboBox) is a widget that extends the functionality of a standard `QComboBox` to allow the user to select a DAP process from a list of DAP processes. +The widget provides a set of signals and slots to allow the user to interact with the selection of a DAP process, including a signal to send a signal that can be hooked up to the `add_dap(str, str, str)` slot of the [`add_dap`](/api_reference/_autosummary/bec_widgets.widgets.waveform.waveform_widget.BECWaveformWidget.rst#bec_widgets.widgets.waveform.waveform_widget.BECWaveformWidget.add_dap) from the BECWaveformWidget to add a DAP process. + +## Key Features: +- **Select DAP model**: Selection of all active DAP models from BEC. +- **Signal/Slot Interaction**: Signals to add DAP process to BECWaveformWidget. +```{figure} /assets/widget_screenshots/dap_combo_box.png +--- +name: lmfit_dialog +--- +LMFit Dialog +``` +```` +````{tab} Summary of Signals +The following signals are emitted by the `DAP ComboBox` widget: +- `add_dap_model(str, str, str)` : Signal to add a DAP model to the BECWaveformWidget +- `update_x_axis(str)` : Signal to emit the current x axis +- `update_y_axis(str)` : Signal to emit the current y axis +- `update_fit_model(str)` : Signal to emit the current fit model +```` +````{tab} Summary of Slots +The following slots are available for the `DAP ComboBox` widget: +- `select_x_axis(str)` : Slot to select the current x axis, emits the `update_x_axis` signal +- `select_y_axis(str)` : Slot to select the current y axis, emits the `update_y_axis` signal +- `select_fit(str)` : Slot to select the current fit model, emits the `update_fit_model` signal. If x and y axis are set, it will also emit the `add_dap_model` signal. +```` +````{tab} API +```{eval-rst} +.. include:: /api_reference/_autosummary/bec_widgets.widgets.dap_combo_box.dap_combo_box.DAPCombobox.rst +``` +```` + + + + + + + + + diff --git a/docs/user/widgets/lmfit_dialog/lmfit_dialog.md b/docs/user/widgets/lmfit_dialog/lmfit_dialog.md index 29141d66..ae498c92 100644 --- a/docs/user/widgets/lmfit_dialog/lmfit_dialog.md +++ b/docs/user/widgets/lmfit_dialog/lmfit_dialog.md @@ -4,7 +4,7 @@ ````{tab} Overview -The [`LMFit Dialog`](/api_reference/_autosummary/bec_widgets.widgets.lmfit_dialog.lmfit_dialog.LMFitDialog) is a widget that is developed to be used togther with the [`BECWaveformWidget`](/api_reference/_autosummary/bec_widgets.widgets.waveform.waveform_widget.BECWaveformWidget). The `BECWaveformWidget` allows user to submit a fit request to BEC's [DAP server](https://bec.readthedocs.io/en/latest/developer/getting_started/architecture.html) choosing from a selection of [LMFit models](https://lmfit.github.io/lmfit-py/builtin_models.html#) to fit monitored data sources. The `LMFit Dialog` provides an interface to monitor these fits, including statistics and fit parameters in real time. +The [`LMFit Dialog`](/api_reference/_autosummary/bec_widgets.widgets.lmfit_dialog.lmfit_dialog.LMFitDialog) is a widget that is developed to be used together with the [`BECWaveformWidget`](/api_reference/_autosummary/bec_widgets.widgets.waveform.waveform_widget.BECWaveformWidget). The `BECWaveformWidget` allows user to submit a fit request to BEC's [DAP server](https://bec.readthedocs.io/en/latest/developer/getting_started/architecture.html) choosing from a selection of [LMFit models](https://lmfit.github.io/lmfit-py/builtin_models.html#) to fit monitored data sources. The `LMFit Dialog` provides an interface to monitor these fits, including statistics and fit parameters in real time. Within the `BECWaveformWidget`, the dialog is accessible via the toolbar and will be automatically linked to the current waveform widget. For a more customised use, we can embed the `LMFit Dialog` in a larger GUI using the *BEC Designer*. In this case, one has to connect the [`update_summary_tree`](/api_reference/_autosummary/bec_widgets.widgets.lmfit_dialog.lmfit_dialog.LMFitDialog.rst#bec_widgets.widgets.lmfit_dialog.lmfit_dialog.LMFitDialog.update_summary_tree) slot of the LMFit Dialog to the [`dap_summary_update`](/api_reference/_autosummary/bec_widgets.widgets.waveform.waveform_widget.BECWaveformWidget.rst#bec_widgets.widgets.waveform.waveform_widget.BECWaveformWidget.dap_summary_update) signal of the BECWaveformWidget to ensure its functionality. diff --git a/docs/user/widgets/widgets.md b/docs/user/widgets/widgets.md index 43853b89..b0f7da2f 100644 --- a/docs/user/widgets/widgets.md +++ b/docs/user/widgets/widgets.md @@ -206,6 +206,14 @@ Display position of motor withing its limits. Display DAP summaries of LMFit models in a window. ``` + +```{grid-item-card} DAP ComboBox +:link: user.widgets.dap_combo_box +:link-type: ref +:img-top: /assets/widget_screenshots/dap_combo_box.png + +Select DAP model from a list of DAP processes. +``` ```` ```{toctree} @@ -234,5 +242,6 @@ spinner/spinner.md device_input/device_input.md position_indicator/position_indicator.md lmfit_dialog/lmfit_dialog.md +dap_combo_box/dap_combo_box.md ``` \ No newline at end of file diff --git a/tests/unit_tests/test_dap_combobox.py b/tests/unit_tests/test_dap_combobox.py new file mode 100644 index 00000000..362e8aef --- /dev/null +++ b/tests/unit_tests/test_dap_combobox.py @@ -0,0 +1,75 @@ +from unittest import mock + +import pytest + +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(scope="function") +def dap_combobox(qtbot, mocked_client): + """DapComboBox fixture.""" + models = ["GaussianModel", "LorentzModel", "SineModel"] + mocked_client.dap._available_dap_plugins.keys.return_value = models + widget = create_widget(qtbot, DapComboBox, client=mocked_client) + return widget + + +def test_dap_combobox_init(dap_combobox): + """Test DapComboBox init.""" + assert dap_combobox.combobox.currentText() == "GaussianModel" + assert dap_combobox.available_models == ["GaussianModel", "LorentzModel", "SineModel"] + assert dap_combobox._validate_dap_model("GaussianModel") is True + assert dap_combobox._validate_dap_model("somemodel") is False + assert dap_combobox._validate_dap_model(None) is False + + +def test_dap_combobox_set_axis(dap_combobox): + """Test DapComboBox set axis.""" + # Container to store the messages + container = [] + + def my_callback(msg: str): + """Calback function to store the messages.""" + container.append(msg) + + dap_combobox.update_x_axis.connect(my_callback) + dap_combobox.update_y_axis.connect(my_callback) + dap_combobox.select_x_axis("x_axis") + assert dap_combobox.x_axis == "x_axis" + dap_combobox.select_y_axis("y_axis") + assert dap_combobox.y_axis == "y_axis" + assert container[0] == "x_axis" + assert container[1] == "y_axis" + + +def test_dap_combobox_select_fit(dap_combobox): + """Test DapComboBox select fit.""" + # Container to store the messages + container = [] + + def my_callback(msg: str): + """Calback function to store the messages.""" + container.append(msg) + + dap_combobox.update_fit_model.connect(my_callback) + dap_combobox.select_fit("LorentzModel") + assert dap_combobox.combobox.currentText() == "LorentzModel" + assert container[0] == "LorentzModel" + + +def test_dap_combobox_currentTextchanged(dap_combobox): + """Test DapComboBox currentTextChanged.""" + # Container to store the messages + container = [] + + def my_callback(msg: str): + """Calback function to store the messages.""" + container.append(msg) + + assert dap_combobox.combobox.currentText() == "GaussianModel" + dap_combobox.update_fit_model.connect(my_callback) + dap_combobox.combobox.setCurrentText("SineModel") + assert container[0] == "SineModel"