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

feat: generate combobox for literal str

This commit is contained in:
2025-06-13 10:30:26 +02:00
committed by David Perl
parent b0d03c0648
commit 138d4cabbd
4 changed files with 81 additions and 35 deletions

View File

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

View File

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

View File

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

View File

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