diff --git a/bec_widgets/utils/forms_from_types/forms.py b/bec_widgets/utils/forms_from_types/forms.py index 36ba6551..f8d07492 100644 --- a/bec_widgets/utils/forms_from_types/forms.py +++ b/bec_widgets/utils/forms_from_types/forms.py @@ -109,7 +109,7 @@ class TypedForm(BECWidget, QWidget): label.setProperty("_model_field_name", item.name) label.setToolTip(item.info.description or item.name) grid.addWidget(label, row, 0) - widget = self._widget_from_type(item.item_type, self._widget_types)(parent=self, spec=item) + widget = self._widget_from_type(item, self._widget_types)(parent=self, spec=item) widget.valueChanged.connect(self.value_changed) widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) grid.addWidget(widget, row, 1) diff --git a/bec_widgets/utils/forms_from_types/items.py b/bec_widgets/utils/forms_from_types/items.py index 18d53e6b..babd17fa 100644 --- a/bec_widgets/utils/forms_from_types/items.py +++ b/bec_widgets/utils/forms_from_types/items.py @@ -4,13 +4,14 @@ import typing from abc import abstractmethod from decimal import Decimal from types import GenericAlias, UnionType -from typing import Callable, Final, Generic, Literal, NamedTuple, TypeVar +from typing import Callable, Final, Generic, Literal, NamedTuple, OrderedDict, TypeVar, get_args from bec_lib.logger import bec_logger from bec_qthemes import material_icon from pydantic import BaseModel, ConfigDict, Field, field_validator from pydantic.fields import FieldInfo from pydantic_core import PydanticUndefined +from PySide6.QtWidgets import QComboBox from qtpy.QtCore import Signal # type: ignore from qtpy.QtWidgets import ( QApplication, @@ -435,39 +436,75 @@ class ListFormItem(DynamicFormItem): self._repop(value) -WidgetTypeRegistry = dict[ - str, tuple[Callable[[type | UnionType | None], bool], type[DynamicFormItem]] -] +class StrLiteralFormItem(DynamicFormItem): + def _add_main_widget(self) -> None: + self._main_widget = QComboBox() + self._options = get_args(self._spec.info.annotation) + for opt in self._options: + self._main_widget.addItem(opt) + self._layout.addWidget(self._main_widget) -DEFAULT_WIDGET_TYPES: Final[WidgetTypeRegistry] = { - "str": (lambda anno: anno in [str, str | None, None], StrFormItem), - "int": (lambda anno: anno in [int, int | None], IntFormItem), + def getValue(self): + return self._main_widget.currentText() + + def setValue(self, value: str | None): + if value is None: + self.clear() + for i in range(self._main_widget.count()): + if self._main_widget.itemText(i) == value: + self._main_widget.setCurrentIndex(i) + return + raise ValueError(f"Cannot set value: {value}, options are: {self._options}") + + def clear(self): + self._main_widget.setCurrentIndex(-1) + + +WidgetTypeRegistry = OrderedDict[str, tuple[Callable[[FormItemSpec], bool], type[DynamicFormItem]]] + +DEFAULT_WIDGET_TYPES: Final[WidgetTypeRegistry] = OrderedDict() | { + # dict literals are ordered already but TypedForm subclasses may modify coppies of this dict + # and delete/insert keys or change the order + "literal_str": ( + lambda spec: type(spec.info.annotation) is type(Literal[""]) + and set(type(arg) for arg in get_args(spec.info.annotation)) == {str}, + StrLiteralFormItem, + ), + "str": (lambda spec: spec.item_type in [str, str | None, None], StrFormItem), + "int": (lambda spec: spec.item_type in [int, int | None], IntFormItem), "float_decimal": ( - lambda anno: anno in [float, float | None, Decimal, Decimal | None], + lambda spec: spec.item_type in [float, float | None, Decimal, Decimal | None], FloatDecimalFormItem, ), - "bool": (lambda anno: anno in [bool, bool | None], BoolFormItem), + "bool": (lambda spec: spec.item_type in [bool, bool | None], BoolFormItem), "dict": ( - lambda anno: anno in [dict, dict | None] - or (isinstance(anno, GenericAlias) and anno.__origin__ is dict), + lambda spec: spec.item_type in [dict, dict | None] + or (isinstance(spec.item_type, GenericAlias) and spec.item_type.__origin__ is dict), DictFormItem, ), "list": ( - lambda anno: anno in [list, list | None] - or (isinstance(anno, GenericAlias) and anno.__origin__ is list), + lambda spec: spec.item_type in [list, list | None] + or (isinstance(spec.item_type, GenericAlias) and spec.item_type.__origin__ is list), ListFormItem, ), + "set": ( + lambda spec: spec.item_type in [set, set | None] + or (isinstance(spec.item_type, GenericAlias) and spec.item_type.__origin__ is set), + SetFormItem, + ), } def widget_from_type( - annotation: type | UnionType | None, widget_types: WidgetTypeRegistry | None = None + spec: FormItemSpec, widget_types: WidgetTypeRegistry | None = None ) -> type[DynamicFormItem]: widget_types = widget_types or DEFAULT_WIDGET_TYPES for predicate, widget_type in widget_types.values(): - if predicate(annotation): + if predicate(spec): return widget_type - logger.warning(f"Type {annotation} is not (yet) supported in metadata form creation.") + logger.warning( + f"Type {spec.item_type=} / {spec.info.annotation=} is not (yet) supported in dynamic form creation." + ) return StrFormItem diff --git a/bec_widgets/widgets/services/device_browser/device_item/device_config_dialog.py b/bec_widgets/widgets/services/device_browser/device_item/device_config_dialog.py index bfeec42c..710d15fb 100644 --- a/bec_widgets/widgets/services/device_browser/device_item/device_config_dialog.py +++ b/bec_widgets/widgets/services/device_browser/device_item/device_config_dialog.py @@ -126,22 +126,28 @@ class DeviceConfigDialog(BECWidget, QDialog): logger.info("No changes made to device config") return logger.info(f"Sending request to update device config: {config}") - try: - self._start_waiting_display() - RID = self._config_helper.send_config_request( - action="update", config={self._device: config}, wait_for_response=False - ) - reply = self._config_helper.wait_for_config_reply( - RID, timeout=self._config_helper.suggested_timeout_s(config) - ) - self._config_helper.handle_update_reply(reply, RID) - self._stop_waiting_display() - except Exception as e: - self._stop_waiting_display() - logger.error(f"Error updating config: \n {''.join(traceback.format_exception(e))}") - finally: - self._fetch_config() - self._fill_form() + + self._start_waiting_display() + + def _communicate_update(): + try: + RID = self._config_helper.send_config_request( + action="update", config={self._device: config}, wait_for_response=False + ) + logger.info("Waiting for config reply") + reply = self._config_helper.wait_for_config_reply( + RID, timeout=self._config_helper.suggested_timeout_s(config) + ) + logger.info("Handling config reply") + self._config_helper.handle_update_reply(reply, RID) + except Exception as e: + self._stop_waiting_display() + logger.error(f"Error updating config: \n {''.join(traceback.format_exception(e))}") + finally: + self._fetch_config() + self._fill_form() + + Thread(target=_communicate_update).start() def _start_waiting_display(self): self._overlay_widget.setVisible(True) diff --git a/bec_widgets/widgets/services/device_browser/device_item/device_config_form.py b/bec_widgets/widgets/services/device_browser/device_item/device_config_form.py index f1f17fed..18155ae3 100644 --- a/bec_widgets/widgets/services/device_browser/device_item/device_config_form.py +++ b/bec_widgets/widgets/services/device_browser/device_item/device_config_form.py @@ -30,8 +30,11 @@ class DeviceConfigForm(PydanticModelForm): **kwargs, ) self._widget_types = DEFAULT_WIDGET_TYPES.copy() - self._widget_types["bool"] = (lambda anno: anno is bool, BoolToggleFormItem) - self._widget_types["optional_bool"] = (lambda anno: anno == bool | None, BoolFormItem) + self._widget_types["bool"] = (lambda spec: spec.item_type is bool, BoolToggleFormItem) + self._widget_types["optional_bool"] = ( + lambda spec: spec.item_type == bool | None, + BoolFormItem, + ) self._validity.setVisible(False) self._connect_to_theme_change() self.populate()