0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 03:01:50 +02:00

feat: add dap_combobox

This commit is contained in:
2024-09-02 17:09:58 +02:00
committed by wyzula_j
parent 3a5d7d0796
commit cc691d4039
12 changed files with 407 additions and 2 deletions

View File

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

View File

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

View File

@ -0,0 +1 @@
{'files': ['dap_combo_box.py']}

View File

@ -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 = """
<ui language='c++'>
<widget class='DapComboBox' name='dap_combo_box'>
</widget>
</ui>
"""
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()

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

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

View File

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

View File

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

View File

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