Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bec_widgets/cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -985,7 +985,7 @@ def dap_oversample(self):


class DapComboBox(RPCBase):
"""The DAPComboBox widget is an extension to the QComboBox with all avaialble DAP model from BEC."""
"""Editable combobox listing the available DAP models."""

@rpc_call
def select_y_axis(self, y_axis: str):
Expand All @@ -1011,7 +1011,7 @@ def select_fit_model(self, fit_name: str | None):
Slot to update the fit model.

Args:
default_device(str): Default device name.
fit_name(str): Fit model name.
"""


Expand Down
105 changes: 66 additions & 39 deletions bec_widgets/widgets/dap/dap_combo_box/dap_combo_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,19 @@

from bec_lib.logger import bec_logger
from qtpy.QtCore import Property, Signal, Slot
from qtpy.QtWidgets import QComboBox, QVBoxLayout, QWidget
from qtpy.QtWidgets import QComboBox

from bec_widgets.utils.bec_widget import BECWidget

logger = bec_logger.logger


class DapComboBox(BECWidget, QWidget):
class DapComboBox(BECWidget, QComboBox):
"""
The DAPComboBox widget is an extension to the QComboBox with all avaialble DAP model from BEC.
Editable combobox listing the available DAP models.

Args:
parent: Parent widget.
client: BEC client object.
gui_id: GUI ID.
default: Default device name.
The widget behaves as a plain QComboBox and keeps ``fit_model_combobox`` as an alias to itself
for backwards compatibility with older call sites.
"""

ICON_NAME = "data_exploration"
Expand Down Expand Up @@ -45,28 +42,29 @@ def __init__(
**kwargs,
):
super().__init__(parent=parent, client=client, gui_id=gui_id, **kwargs)
self.layout = QVBoxLayout(self)
self.fit_model_combobox = QComboBox(self)
self.layout.addWidget(self.fit_model_combobox)
self.layout.setContentsMargins(0, 0, 0, 0)
self._available_models = None
self.fit_model_combobox = self # Just for backwards compatibility with older call sites, the widget itself is the combobox
self._available_models: list[str] = []
self._x_axis = None
self._y_axis = None
self._is_valid_input = False

self.setEditable(True)

self.populate_fit_model_combobox()
self.fit_model_combobox.currentTextChanged.connect(self._update_current_fit)
# Set default fit model
self.currentTextChanged.connect(self._on_text_changed)
self.select_default_fit(default_fit)
self.check_validity(self.currentText())

def select_default_fit(self, default_fit: str | None):
def select_default_fit(self, default_fit: str | None = "GaussianModel"):
"""Set the default fit model.

Args:
default_fit(str): Default fit model.
"""
if self._validate_dap_model(default_fit):
self.select_fit_model(default_fit)
else:
self.select_fit_model("GaussianModel")
elif self.available_models:
self.select_fit_model(self.available_models[0])

@property
def available_models(self):
Expand Down Expand Up @@ -114,12 +112,40 @@ def y_axis(self, y_axis: str):
self._y_axis = y_axis
self.y_axis_updated.emit(y_axis)

def _update_current_fit(self, fit_name: str):
"""Update the current fit."""
@Slot(str)
def _on_text_changed(self, fit_name: str):
"""
Validate and emit updates for the current text.

Args:
fit_name(str): The current text in the combobox, representing the selected fit model.
"""
self.check_validity(fit_name)
if not self._is_valid_input:
return

self.fit_model_updated.emit(fit_name)
if self.x_axis is not None and self.y_axis is not None:
self.new_dap_config.emit(self._x_axis, self._y_axis, fit_name)

@Slot(str)
def check_validity(self, fit_name: str):
"""
Highlight invalid manual entries similarly to DeviceComboBox.

Args:
fit_name(str): The current text in the combobox, representing the selected fit model.
"""
if self._validate_dap_model(fit_name):
self._is_valid_input = True
self.setStyleSheet("border: 1px solid transparent;")
else:
self._is_valid_input = False
if self.isEnabled():
self.setStyleSheet("border: 1px solid red;")
else:
self.setStyleSheet("border: 1px solid transparent;")

@Slot(str)
def select_x_axis(self, x_axis: str):
"""Slot to update the x axis.
Expand All @@ -128,7 +154,7 @@ def select_x_axis(self, x_axis: str):
x_axis(str): X axis.
"""
self.x_axis = x_axis
self._update_current_fit(self.fit_model_combobox.currentText())
self._on_text_changed(self.currentText())

@Slot(str)
def select_y_axis(self, y_axis: str):
Expand All @@ -138,25 +164,26 @@ def select_y_axis(self, y_axis: str):
y_axis(str): Y axis.
"""
self.y_axis = y_axis
self._update_current_fit(self.fit_model_combobox.currentText())
self._on_text_changed(self.currentText())

@Slot(str)
def select_fit_model(self, fit_name: str | None):
"""Slot to update the fit model.

Args:
default_device(str): Default device name.
fit_name(str): Fit model name.
"""
if not self._validate_dap_model(fit_name):
raise ValueError(f"Fit {fit_name} is not valid.")
self.fit_model_combobox.setCurrentText(fit_name)
self.setCurrentText(fit_name)

def populate_fit_model_combobox(self):
"""Populate the fit_model_combobox with the devices."""
# pylint: disable=protected-access
self.available_models = [model for model in self.client.dap._available_dap_plugins.keys()]
self.fit_model_combobox.clear()
self.fit_model_combobox.addItems(self.available_models)
available_plugins = getattr(getattr(self.client, "dap", None), "_available_dap_plugins", {})
self.available_models = [model for model in available_plugins.keys()]
self.clear()
self.addItems(self.available_models)

def _validate_dap_model(self, model: str | None) -> bool:
"""Validate the DAP model.
Expand All @@ -166,23 +193,23 @@ def _validate_dap_model(self, model: str | None) -> bool:
"""
if model is None:
return False
if model not in self.available_models:
return False
return True
return model in self.available_models

@property
def is_valid_input(self) -> bool:
"""Whether the current text matches an available DAP model."""
return self._is_valid_input


if __name__ == "__main__": # pragma: no cover
# pylint: disable=import-outside-toplevel
import sys

from qtpy.QtWidgets import QApplication

from bec_widgets.utils.colors import apply_theme

app = QApplication([])
app = QApplication(sys.argv)
apply_theme("dark")
widget = QWidget()
widget.setFixedSize(200, 200)
layout = QVBoxLayout()
widget.setLayout(layout)
layout.addWidget(DapComboBox())
widget.show()
app.exec_()
dialog = DapComboBox()
dialog.show()
sys.exit(app.exec_())
32 changes: 17 additions & 15 deletions bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os

import shiboken6
from bec_lib.logger import bec_logger
from qtpy.QtCore import Signal
from qtpy.QtWidgets import QPushButton, QTreeWidgetItem, QVBoxLayout, QWidget
Expand Down Expand Up @@ -34,7 +35,7 @@ def __init__(
**kwargs,
):
"""
Initialises the LMFitDialog widget.
Initializes the LMFitDialog widget.

Args:
parent (QWidget): The parent widget.
Expand Down Expand Up @@ -77,8 +78,13 @@ def enable_actions(self) -> bool:
@enable_actions.setter
def enable_actions(self, enable: bool):
self._enable_actions = enable
for button in self.action_buttons.values():
valid_buttons = {}
for name, button in self.action_buttons.items():
if button is None or not shiboken6.isValid(button): # to fix cpp object deleted
continue
button.setEnabled(enable)
valid_buttons[name] = button
self.action_buttons = valid_buttons

@SafeProperty(list)
def active_action_list(self) -> list[str]:
Expand All @@ -89,16 +95,6 @@ def active_action_list(self) -> list[str]:
def active_action_list(self, actions: list[str]):
self._active_actions = actions

# This SafeSlot needed?
@SafeSlot(bool)
def set_actions_enabled(self, enable: bool) -> bool:
"""SafeSlot to enable the move to buttons.

Args:
enable (bool): Whether to enable the action buttons.
"""
self.enable_actions = enable

@SafeProperty(bool)
def always_show_latest(self):
"""SafeProperty to indicate if always the latest DAP update is displayed."""
Expand Down Expand Up @@ -163,7 +159,7 @@ def fit_curve_id(self, curve_id: str):
"""Setter for the currently displayed fit curve_id.

Args:
fit_curve_id (str): The curve_id of the fit curve to be displayed.
curve_id (str): The curve_id of the fit curve to be displayed.
"""
self._fit_curve_id = curve_id
self.selected_fit.emit(curve_id)
Expand All @@ -176,6 +172,11 @@ def remove_dap_data(self, curve_id: str):
curve_id (str): The curve_id of the DAP data to be removed.
"""
self.summary_data.pop(curve_id, None)
if self.fit_curve_id == curve_id:
self._fit_curve_id = None
self.action_buttons = {}
self.ui.summary_tree.clear()
self.ui.param_tree.clear()
self.refresh_curve_list()

@SafeSlot(str)
Expand Down Expand Up @@ -251,6 +252,7 @@ def update_param_tree(self, params):
params (list): List of LMFit parameters for the fit curve.
"""
self._move_buttons = []
self.action_buttons = {}
self.ui.param_tree.clear()
for param in params:
param_name = param[0]
Expand All @@ -269,9 +271,9 @@ def update_param_tree(self, params):
if param_name in self.active_action_list: # pylint: disable=unsupported-membership-test
# Create a push button to move the motor to a specific position
widget = QWidget()
button = QPushButton(f"Move to {param_name}")
button = QPushButton("Move")
button.clicked.connect(self._create_move_action(param_name, param[1]))
if self.enable_actions is True:
if self.enable_actions:
button.setEnabled(True)
else:
button.setEnabled(False)
Expand Down
Loading
Loading