1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-03-30 05:18:36 +02:00

fix(dap_combobox): rewritten as proper combobox

This commit is contained in:
2026-03-19 11:28:48 +01:00
committed by Jan Wyzula
parent 79af15a88b
commit 90222f3082
3 changed files with 80 additions and 42 deletions

View File

@@ -985,7 +985,7 @@ class Curve(RPCBase):
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):
@@ -1011,7 +1011,7 @@ class DapComboBox(RPCBase):
Slot to update the fit model.
Args:
default_device(str): Default device name.
fit_name(str): Fit model name.
"""

View File

@@ -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"
@@ -45,19 +42,20 @@ class DapComboBox(BECWidget, QWidget):
**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.populate_fit_model_combobox()
self.fit_model_combobox.currentTextChanged.connect(self._update_current_fit)
# Set default fit model
self.select_default_fit(default_fit)
self._is_valid_input = False
def select_default_fit(self, default_fit: str | None):
self.setEditable(True)
self.populate_fit_model_combobox()
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 = "GaussianModel"):
"""Set the default fit model.
Args:
@@ -65,8 +63,6 @@ class DapComboBox(BECWidget, QWidget):
"""
if self._validate_dap_model(default_fit):
self.select_fit_model(default_fit)
elif self._validate_dap_model("GaussianModel"):
self.select_fit_model("GaussianModel")
elif self.available_models:
self.select_fit_model(self.available_models[0])
@@ -116,12 +112,40 @@ class DapComboBox(BECWidget, QWidget):
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.
@@ -130,7 +154,7 @@ class DapComboBox(BECWidget, QWidget):
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):
@@ -140,26 +164,26 @@ class DapComboBox(BECWidget, QWidget):
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
available_plugins = getattr(getattr(self.client, "dap", None), "_available_dap_plugins", {})
self.available_models = [model for model in available_plugins.keys()]
self.fit_model_combobox.clear()
self.fit_model_combobox.addItems(self.available_models)
self.clear()
self.addItems(self.available_models)
def _validate_dap_model(self, model: str | None) -> bool:
"""Validate the DAP model.
@@ -169,23 +193,23 @@ class DapComboBox(BECWidget, QWidget):
"""
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_())

View File

@@ -17,6 +17,8 @@ def dap_combobox(qtbot, mocked_client):
def test_dap_combobox_init(dap_combobox):
"""Test DapComboBox init."""
assert dap_combobox.fit_model_combobox is dap_combobox
assert dap_combobox.isEditable() is True
assert dap_combobox.fit_model_combobox.currentText() == "GaussianModel"
assert dap_combobox.available_models == ["GaussianModel", "LorentzModel", "SineModel"]
assert dap_combobox._validate_dap_model("GaussianModel") is True
@@ -81,3 +83,15 @@ def test_dap_combobox_init_without_available_models(qtbot, mocked_client):
assert widget.available_models == []
assert widget.fit_model_combobox.count() == 0
assert widget.fit_model_combobox.currentText() == ""
def test_dap_combobox_invalid_manual_entry_highlighted(dap_combobox):
dap_combobox.setCurrentText("not-a-model")
assert dap_combobox.is_valid_input is False
assert "red" in dap_combobox.styleSheet()
dap_combobox.setCurrentText("GaussianModel")
assert dap_combobox.is_valid_input is True
assert "transparent" in dap_combobox.styleSheet()