bec-widgets 2.11.0__py3-none-any.whl → 2.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
CHANGELOG.md CHANGED
@@ -1,6 +1,39 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v2.12.0 (2025-06-04)
5
+
6
+ ### Bug Fixes
7
+
8
+ - Exclude metadata from RPC
9
+ ([`718116a`](https://github.com/bec-project/bec_widgets/commit/718116afc3a724658c4cd57b76e93249a66a9ebd))
10
+
11
+ - Grid formatting in TypedForm
12
+ ([`5949121`](https://github.com/bec-project/bec_widgets/commit/594912136e2118de1a4de5213c2f668952f28a84))
13
+
14
+ - Make generate plugin robust to multiline init
15
+ ([`a10e6f7`](https://github.com/bec-project/bec_widgets/commit/a10e6f7820309d590e832f2bca44ca1db8ef72a1))
16
+
17
+ instead of str.find, use multiline regex with whitespace
18
+
19
+ - **device browser**: Mocks and utils for tests
20
+ ([`e0e26c2`](https://github.com/bec-project/bec_widgets/commit/e0e26c205bf930d680e01910f87489decc7fbcdb))
21
+
22
+ ### Features
23
+
24
+ - (#493) add dict to dynamic form types
25
+ ([`92d1d64`](https://github.com/bec-project/bec_widgets/commit/92d1d6435d6e8c05851804eb76605a4abeec01bb))
26
+
27
+ - (#493) add helpers to dynamic form widgets
28
+ ([`a25c1a8`](https://github.com/bec-project/bec_widgets/commit/a25c1a8039078c92789b717b3f8a553c75814c33))
29
+
30
+ - (#493) device browser to display config
31
+ ([`5188b38`](https://github.com/bec-project/bec_widgets/commit/5188b38c86f543d2abc742411b64fa127c6c0c16))
32
+
33
+ - Add clickable label util
34
+ ([`2dda58f`](https://github.com/bec-project/bec_widgets/commit/2dda58f7d2adf1f41c6ce4fad02d55bd9aa200fa))
35
+
36
+
4
37
  ## v2.11.0 (2025-06-04)
5
38
 
6
39
  ### Bug Fixes
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 2.11.0
3
+ Version: 2.12.0
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
@@ -184,8 +184,8 @@ class FakePositioner(BECPositioner):
184
184
  class Positioner(FakePositioner):
185
185
  """just placeholder for testing embedded isinstance check in DeviceCombobox"""
186
186
 
187
- def __init__(self, name="test", limits=None, read_value=1.0):
188
- super().__init__(name, limits, read_value)
187
+ def __init__(self, name="test", limits=None, read_value=1.0, enabled=True):
188
+ super().__init__(name, limits=limits, read_value=read_value, enabled=enabled)
189
189
 
190
190
 
191
191
  class Device(FakeDevice):
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from qtpy.QtCore import Signal
4
+ from qtpy.QtGui import QMouseEvent
5
+ from qtpy.QtWidgets import QLabel
6
+
7
+
8
+ class ClickableLabel(QLabel):
9
+ clicked = Signal()
10
+
11
+ def mouseReleaseEvent(self, ev: QMouseEvent) -> None:
12
+ self.clicked.emit()
13
+ return super().mouseReleaseEvent(ev)
@@ -15,12 +15,15 @@ if TYPE_CHECKING: # pragma: no cover
15
15
  from bec_qthemes._main import AccentColors
16
16
 
17
17
 
18
- def get_theme_palette():
18
+ def get_theme_name():
19
19
  if QApplication.instance() is None or not hasattr(QApplication.instance(), "theme"):
20
- theme = "dark"
20
+ return "dark"
21
21
  else:
22
- theme = QApplication.instance().theme.theme
23
- return bec_qthemes.load_palette(theme)
22
+ return QApplication.instance().theme.theme
23
+
24
+
25
+ def get_theme_palette():
26
+ return bec_qthemes.load_palette(get_theme_name())
24
27
 
25
28
 
26
29
  def get_accent_colors() -> AccentColors | None:
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from bec_qthemes import material_icon
4
+ from qtpy.QtCore import Signal
4
5
  from qtpy.QtWidgets import (
6
+ QApplication,
5
7
  QFrame,
6
8
  QHBoxLayout,
7
9
  QLabel,
@@ -12,15 +14,20 @@ from qtpy.QtWidgets import (
12
14
  QWidget,
13
15
  )
14
16
 
17
+ from bec_widgets.utils.clickable_label import ClickableLabel
15
18
  from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
16
19
 
17
20
 
18
21
  class ExpandableGroupFrame(QFrame):
19
22
 
23
+ expansion_state_changed = Signal()
24
+
20
25
  EXPANDED_ICON_NAME: str = "collapse_all"
21
26
  COLLAPSED_ICON_NAME: str = "expand_all"
22
27
 
23
- def __init__(self, title: str, parent: QWidget | None = None, expanded: bool = True) -> None:
28
+ def __init__(
29
+ self, parent: QWidget | None = None, title: str = "", expanded: bool = True, icon: str = ""
30
+ ) -> None:
24
31
  super().__init__(parent=parent)
25
32
  self._expanded = expanded
26
33
 
@@ -29,19 +36,28 @@ class ExpandableGroupFrame(QFrame):
29
36
  self._layout = QVBoxLayout()
30
37
  self._layout.setContentsMargins(0, 0, 0, 0)
31
38
  self.setLayout(self._layout)
39
+
32
40
  self._title_layout = QHBoxLayout()
33
41
  self._layout.addLayout(self._title_layout)
34
- self._expansion_button = QToolButton()
35
- self._update_icon()
36
- self._title = QLabel(f"<b>{title}</b>")
37
- self._title_layout.addWidget(self._expansion_button)
42
+
43
+ self._title = ClickableLabel(f"<b>{title}</b>")
44
+ self._title_icon = ClickableLabel()
45
+ self._title_layout.addWidget(self._title_icon)
38
46
  self._title_layout.addWidget(self._title)
47
+ self.icon_name = icon
48
+
49
+ self._title_layout.addStretch(1)
50
+
51
+ self._expansion_button = QToolButton()
52
+ self._update_expansion_icon()
53
+ self._title_layout.addWidget(self._expansion_button, stretch=1)
39
54
 
40
55
  self._contents = QWidget(self)
41
56
  self._layout.addWidget(self._contents)
42
57
 
43
58
  self._expansion_button.clicked.connect(self.switch_expanded_state)
44
59
  self.expanded = self._expanded # type: ignore
60
+ self.expansion_state_changed.emit()
45
61
 
46
62
  def set_layout(self, layout: QLayout) -> None:
47
63
  self._contents.setLayout(layout)
@@ -50,7 +66,8 @@ class ExpandableGroupFrame(QFrame):
50
66
  @SafeSlot()
51
67
  def switch_expanded_state(self):
52
68
  self.expanded = not self.expanded # type: ignore
53
- self._update_icon()
69
+ self._update_expansion_icon()
70
+ self.expansion_state_changed.emit()
54
71
 
55
72
  @SafeProperty(bool)
56
73
  def expanded(self): # type: ignore
@@ -61,8 +78,9 @@ class ExpandableGroupFrame(QFrame):
61
78
  self._expanded = expanded
62
79
  self._contents.setVisible(expanded)
63
80
  self.updateGeometry()
81
+ self.adjustSize()
64
82
 
65
- def _update_icon(self):
83
+ def _update_expansion_icon(self):
66
84
  self._expansion_button.setIcon(
67
85
  material_icon(icon_name=self.EXPANDED_ICON_NAME, size=(10, 10), convert_to_pixmap=False)
68
86
  if self.expanded
@@ -70,3 +88,36 @@ class ExpandableGroupFrame(QFrame):
70
88
  icon_name=self.COLLAPSED_ICON_NAME, size=(10, 10), convert_to_pixmap=False
71
89
  )
72
90
  )
91
+
92
+ @SafeProperty(str)
93
+ def icon_name(self): # type: ignore
94
+ return self._title_icon_name
95
+
96
+ @icon_name.setter
97
+ def icon_name(self, icon_name: str):
98
+ self._title_icon_name = icon_name
99
+ self._set_title_icon(self._title_icon_name)
100
+
101
+ def _set_title_icon(self, icon_name: str):
102
+ if icon_name:
103
+ self._title_icon.setVisible(True)
104
+ self._title_icon.setPixmap(
105
+ material_icon(icon_name=icon_name, size=(20, 20), convert_to_pixmap=True)
106
+ )
107
+ else:
108
+ self._title_icon.setVisible(False)
109
+
110
+
111
+ # Application example
112
+ if __name__ == "__main__": # pragma: no cover
113
+
114
+ app = QApplication([])
115
+ frame = ExpandableGroupFrame()
116
+ layout = QVBoxLayout()
117
+ frame.set_layout(layout)
118
+ layout.addWidget(QLabel("test1"))
119
+ layout.addWidget(QLabel("test2"))
120
+ layout.addWidget(QLabel("test3"))
121
+
122
+ frame.show()
123
+ app.exec()
@@ -2,70 +2,99 @@ from __future__ import annotations
2
2
 
3
3
  from decimal import Decimal
4
4
  from types import NoneType
5
+ from typing import NamedTuple
5
6
 
6
7
  from bec_lib.logger import bec_logger
7
8
  from bec_qthemes import material_icon
8
9
  from pydantic import BaseModel, ValidationError
9
10
  from qtpy.QtCore import Signal # type: ignore
10
- from qtpy.QtWidgets import QGridLayout, QLabel, QLayout, QVBoxLayout, QWidget
11
+ from qtpy.QtWidgets import QGridLayout, QLabel, QLayout, QSizePolicy, QVBoxLayout, QWidget
11
12
 
12
13
  from bec_widgets.utils.bec_widget import BECWidget
13
14
  from bec_widgets.utils.compact_popup import CompactPopupWidget
14
- from bec_widgets.utils.forms_from_types.items import FormItemSpec, widget_from_type
15
+ from bec_widgets.utils.error_popups import SafeProperty
16
+ from bec_widgets.utils.forms_from_types.items import (
17
+ DynamicFormItem,
18
+ DynamicFormItemType,
19
+ FormItemSpec,
20
+ widget_from_type,
21
+ )
15
22
 
16
23
  logger = bec_logger.logger
17
24
 
18
25
 
26
+ class GridRow(NamedTuple):
27
+ i: int
28
+ label: QLabel
29
+ widget: DynamicFormItem
30
+
31
+
19
32
  class TypedForm(BECWidget, QWidget):
20
33
  PLUGIN = True
21
34
  ICON_NAME = "list_alt"
22
35
 
23
36
  value_changed = Signal()
24
37
 
25
- RPC = False
38
+ RPC = True
39
+ USER_ACCESS = ["enabled", "enabled.setter"]
26
40
 
27
41
  def __init__(
28
42
  self,
29
43
  parent=None,
30
44
  items: list[tuple[str, type]] | None = None,
31
45
  form_item_specs: list[FormItemSpec] | None = None,
46
+ enabled: bool = True,
47
+ pretty_display: bool = False,
32
48
  client=None,
33
49
  **kwargs,
34
50
  ):
35
51
  """Widget with a list of form items based on a list of types.
36
52
 
37
53
  Args:
38
- items (list[tuple[str, type]]): list of tuples of a name for the field and its type.
39
- Should be a type supported by the logic in items.py
40
- form_item_specs (list[FormItemSpec]): list of form item specs, equivalent to items.
41
- only one of items or form_item_specs should be
42
- supplied.
43
-
54
+ items (list[tuple[str, type]]): list of tuples of a name for the field and its type.
55
+ Should be a type supported by the logic in items.py
56
+ form_item_specs (list[FormItemSpec]): list of form item specs, equivalent to items.
57
+ only one of items or form_item_specs should be
58
+ supplied.
59
+ enabled (bool, optional): whether fields are enabled for editing.
60
+ pretty_display (bool, optional): Whether to use a pretty display for the widget. Defaults to False. If True, disables the widget, doesn't add a clear button, and adapts the stylesheet for non-editable display.
44
61
  """
45
- if (items is not None and form_item_specs is not None) or (
46
- items is None and form_item_specs is None
47
- ):
48
- raise ValueError("Must specify one and only one of items and form_item_specs")
62
+ if items is not None and form_item_specs is not None:
63
+ logger.error(
64
+ "Must specify one and only one of items and form_item_specs! Ignoring `items`."
65
+ )
66
+ items = None
67
+ if items is None and form_item_specs is None:
68
+ logger.error("Must specify one and only one of items and form_item_specs!")
69
+ items = []
49
70
  super().__init__(parent=parent, client=client, **kwargs)
50
71
  self._items = (
51
72
  form_item_specs
52
73
  if form_item_specs is not None
53
74
  else [
54
- FormItemSpec(name=name, item_type=item_type)
75
+ FormItemSpec(name=name, item_type=item_type, pretty_display=pretty_display)
55
76
  for name, item_type in items # type: ignore
56
77
  ]
57
78
  )
79
+ self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
58
80
  self._layout = QVBoxLayout()
59
81
  self._layout.setContentsMargins(0, 0, 0, 0)
60
82
  self.setLayout(self._layout)
61
83
 
84
+ self._enabled: bool = enabled
85
+
62
86
  self._form_grid_container = QWidget(parent=self)
87
+ self._form_grid_container.setSizePolicy(
88
+ QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding
89
+ )
63
90
  self._form_grid = QWidget(parent=self._form_grid_container)
91
+ self._form_grid.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
64
92
  self._layout.addWidget(self._form_grid_container)
65
93
  self._form_grid_container.setLayout(QVBoxLayout())
66
94
  self._form_grid.setLayout(self._new_grid_layout())
67
95
 
68
96
  self.populate()
97
+ self.enabled = self._enabled # type: ignore # QProperty
69
98
 
70
99
  def populate(self):
71
100
  self._clear_grid()
@@ -80,17 +109,20 @@ class TypedForm(BECWidget, QWidget):
80
109
  grid.addWidget(label, row, 0)
81
110
  widget = widget_from_type(item.item_type)(parent=self, spec=item)
82
111
  widget.valueChanged.connect(self.value_changed)
112
+ widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
83
113
  grid.addWidget(widget, row, 1)
84
114
 
85
- def _dict_from_grid(self) -> dict[str, str | int | float | Decimal | bool]:
115
+ def enumerate_form_widgets(self):
116
+ """Return a generator over the rows of the form, with the row number, the label widget (to
117
+ which the field name is attached as a property), and the entry widget"""
86
118
  grid: QGridLayout = self._form_grid.layout() # type: ignore
119
+ for i in range(grid.rowCount()):
120
+ yield GridRow(i, grid.itemAtPosition(i, 0).widget(), grid.itemAtPosition(i, 1).widget())
121
+
122
+ def _dict_from_grid(self) -> dict[str, DynamicFormItemType]:
87
123
  return {
88
- grid.itemAtPosition(i, 0)
89
- .widget()
90
- .property("_model_field_name"): grid.itemAtPosition(i, 1)
91
- .widget()
92
- .getValue() # type: ignore # we only add 'DynamicFormItem's here
93
- for i in range(grid.rowCount())
124
+ row.label.property("_model_field_name"): row.widget.getValue()
125
+ for row in self.enumerate_form_widgets()
94
126
  }
95
127
 
96
128
  def _clear_grid(self):
@@ -103,10 +135,13 @@ class TypedForm(BECWidget, QWidget):
103
135
  old_layout.deleteLater()
104
136
  self._form_grid.deleteLater()
105
137
  self._form_grid = QWidget()
106
-
138
+ self._form_grid.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
107
139
  self._form_grid.setLayout(self._new_grid_layout())
108
140
  self._form_grid_container.layout().addWidget(self._form_grid)
109
141
 
142
+ self.update_size()
143
+
144
+ def update_size(self):
110
145
  self._form_grid.adjustSize()
111
146
  self._form_grid_container.adjustSize()
112
147
  self.adjustSize()
@@ -114,23 +149,52 @@ class TypedForm(BECWidget, QWidget):
114
149
  def _new_grid_layout(self):
115
150
  new_grid = QGridLayout()
116
151
  new_grid.setContentsMargins(0, 0, 0, 0)
117
- new_grid.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
118
152
  return new_grid
119
153
 
154
+ @property
155
+ def widget_dict(self):
156
+ return {
157
+ row.label.property("_model_field_name"): row.widget
158
+ for row in self.enumerate_form_widgets()
159
+ }
160
+
161
+ @SafeProperty(bool)
162
+ def enabled(self):
163
+ return self._enabled
164
+
165
+ @enabled.setter
166
+ def enabled(self, value: bool):
167
+ self._enabled = value
168
+ self.setEnabled(value)
169
+
120
170
 
121
171
  class PydanticModelForm(TypedForm):
122
172
  metadata_updated = Signal(dict)
123
173
  metadata_cleared = Signal(NoneType)
124
174
 
125
- def __init__(self, parent=None, metadata_model: type[BaseModel] = None, client=None, **kwargs):
175
+ def __init__(
176
+ self,
177
+ parent=None,
178
+ data_model: type[BaseModel] | None = None,
179
+ enabled: bool = True,
180
+ pretty_display: bool = False,
181
+ client=None,
182
+ **kwargs,
183
+ ):
126
184
  """
127
185
  A form generated from a pydantic model.
128
186
 
129
187
  Args:
130
- metadata_model (type[BaseModel]): the model class for which to generate a form.
188
+ data_model (type[BaseModel]): the model class for which to generate a form.
189
+ enabled (bool): whether fields are enabled for editing.
190
+ pretty_display (bool, optional): Whether to use a pretty display for the widget. Defaults to False. If True, disables the widget, doesn't add a clear button, and adapts the stylesheet for non-editable display.
191
+
131
192
  """
132
- self._md_schema = metadata_model
133
- super().__init__(parent=parent, form_item_specs=self._form_item_specs(), client=client)
193
+ self._pretty_display = pretty_display
194
+ self._md_schema = data_model
195
+ super().__init__(
196
+ parent=parent, form_item_specs=self._form_item_specs(), enabled=enabled, client=client
197
+ )
134
198
 
135
199
  self._validity = CompactPopupWidget()
136
200
  self._validity.compact_view = True # type: ignore
@@ -147,9 +211,24 @@ class PydanticModelForm(TypedForm):
147
211
  self._md_schema = schema
148
212
  self.populate()
149
213
 
214
+ def set_data(self, data: BaseModel):
215
+ """Fill the data for the form.
216
+
217
+ Args:
218
+ data (BaseModel): the data to enter into the form. Must be the same type as the
219
+ currently set schema, raises TypeError otherwise."""
220
+ if not self._md_schema:
221
+ raise ValueError("Schema not set - can't set data")
222
+ if not isinstance(data, self._md_schema):
223
+ raise TypeError(f"Supplied data {data} not of type {self._md_schema}")
224
+ for form_item in self.enumerate_form_widgets():
225
+ form_item.widget.setValue(getattr(data, form_item.label.property("_model_field_name")))
226
+
150
227
  def _form_item_specs(self):
151
228
  return [
152
- FormItemSpec(name=name, info=info, item_type=info.annotation)
229
+ FormItemSpec(
230
+ name=name, info=info, item_type=info.annotation, pretty_display=self._pretty_display
231
+ )
153
232
  for name, info in self._md_schema.model_fields.items()
154
233
  ]
155
234
 
@@ -2,12 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  from abc import abstractmethod
4
4
  from decimal import Decimal
5
- from types import UnionType
6
- from typing import Callable, Protocol
5
+ from types import GenericAlias, UnionType
6
+ from typing import Literal
7
7
 
8
8
  from bec_lib.logger import bec_logger
9
9
  from bec_qthemes import material_icon
10
- from pydantic import BaseModel, ConfigDict, Field
10
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
11
11
  from pydantic.fields import FieldInfo
12
12
  from qtpy.QtCore import Signal # type: ignore
13
13
  from qtpy.QtWidgets import (
@@ -21,11 +21,13 @@ from qtpy.QtWidgets import (
21
21
  QLayout,
22
22
  QLineEdit,
23
23
  QRadioButton,
24
+ QSizePolicy,
24
25
  QSpinBox,
25
26
  QToolButton,
26
27
  QWidget,
27
28
  )
28
29
 
30
+ from bec_widgets.widgets.editors.dict_backed_table import DictBackedTable
29
31
  from bec_widgets.widgets.editors.scan_metadata._util import (
30
32
  clearable_required,
31
33
  field_default,
@@ -46,9 +48,36 @@ class FormItemSpec(BaseModel):
46
48
  """
47
49
 
48
50
  model_config = ConfigDict(arbitrary_types_allowed=True)
49
- item_type: type | UnionType
51
+
52
+ item_type: type | UnionType | GenericAlias
50
53
  name: str
51
54
  info: FieldInfo = FieldInfo()
55
+ pretty_display: bool = Field(
56
+ default=False,
57
+ description="Whether to use a pretty display for the widget. Defaults to False. If True, disables the widget, doesn't add a clear button, and adapts the stylesheet for non-editable display.",
58
+ )
59
+
60
+ @field_validator("item_type", mode="before")
61
+ @classmethod
62
+ def _validate_type(cls, v):
63
+ allowed_primitives = [str, int, float, bool]
64
+ if isinstance(v, (type, UnionType)):
65
+ return v
66
+ if isinstance(v, GenericAlias):
67
+ if v.__origin__ in [list, dict] and all(
68
+ arg in allowed_primitives for arg in v.__args__
69
+ ):
70
+ return v
71
+ raise ValueError(
72
+ f"Generics of type {v} are not supported - only lists and dicts of primitive types {allowed_primitives}"
73
+ )
74
+ if type(v) is type(Literal[""]): # _LiteralGenericAlias is not exported from typing
75
+ arg_types = set(type(arg) for arg in v.__args__)
76
+ if len(arg_types) != 1:
77
+ raise ValueError("Mixtures of literal types are not supported!")
78
+ if (t := arg_types.pop()) in allowed_primitives:
79
+ return t
80
+ raise ValueError(f"Literals of type {t} are not supported")
52
81
 
53
82
 
54
83
  class ClearableBoolEntry(QWidget):
@@ -94,10 +123,20 @@ class ClearableBoolEntry(QWidget):
94
123
  self._false.setToolTip(tooltip)
95
124
 
96
125
 
126
+ DynamicFormItemType = str | int | float | Decimal | bool | dict
127
+
128
+
97
129
  class DynamicFormItem(QWidget):
98
130
  valueChanged = Signal()
99
131
 
100
132
  def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
133
+ """
134
+ Initializes the form item widget.
135
+
136
+ Args:
137
+ parent (QWidget | None, optional): The parent widget. Defaults to None.
138
+ spec (FormItemSpec): The specification for the form item.
139
+ """
101
140
  super().__init__(parent)
102
141
  self._spec = spec
103
142
  self._layout = QHBoxLayout()
@@ -107,11 +146,16 @@ class DynamicFormItem(QWidget):
107
146
  self._desc = self._spec.info.description
108
147
  self.setLayout(self._layout)
109
148
  self._add_main_widget()
110
- if clearable_required(spec.info):
111
- self._add_clear_button()
149
+ self._main_widget: QWidget
150
+ self._main_widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
151
+ if not spec.pretty_display:
152
+ if clearable_required(spec.info):
153
+ self._add_clear_button()
154
+ else:
155
+ self._set_pretty_display()
112
156
 
113
157
  @abstractmethod
114
- def getValue(self): ...
158
+ def getValue(self) -> DynamicFormItemType: ...
115
159
 
116
160
  @abstractmethod
117
161
  def setValue(self, value): ...
@@ -121,6 +165,9 @@ class DynamicFormItem(QWidget):
121
165
  """Add the main data entry widget to self._main_widget and appply any
122
166
  constraints from the field info"""
123
167
 
168
+ def _set_pretty_display(self):
169
+ self.setEnabled(False)
170
+
124
171
  def _describe(self, pad=" "):
125
172
  return pad + (self._desc if self._desc else "")
126
173
 
@@ -164,7 +211,7 @@ class StrMetadataField(DynamicFormItem):
164
211
  def setValue(self, value: str):
165
212
  if value is None:
166
213
  self._main_widget.setText("")
167
- self._main_widget.setText(value)
214
+ self._main_widget.setText(str(value))
168
215
 
169
216
 
170
217
  class IntMetadataField(DynamicFormItem):
@@ -202,12 +249,12 @@ class FloatDecimalMetadataField(DynamicFormItem):
202
249
  self._main_widget.textChanged.connect(self._value_changed)
203
250
 
204
251
  def _add_main_widget(self) -> None:
252
+ precision = field_precision(self._spec.info)
205
253
  self._main_widget = QDoubleSpinBox()
206
254
  self._layout.addWidget(self._main_widget)
207
- min_, max_ = field_limits(self._spec.info, int)
255
+ min_, max_ = field_limits(self._spec.info, float, precision)
208
256
  self._main_widget.setMinimum(min_)
209
257
  self._main_widget.setMaximum(max_)
210
- precision = field_precision(self._spec.info)
211
258
  if precision:
212
259
  self._main_widget.setDecimals(precision)
213
260
  minstr = f"{float(min_):.3f}" if abs(min_) <= 1000 else f"{float(min_):.3e}"
@@ -224,10 +271,10 @@ class FloatDecimalMetadataField(DynamicFormItem):
224
271
  return self._default
225
272
  return self._main_widget.value()
226
273
 
227
- def setValue(self, value: float):
274
+ def setValue(self, value: float | Decimal):
228
275
  if value is None:
229
276
  self._main_widget.clear()
230
- self._main_widget.setValue(value)
277
+ self._main_widget.setValue(float(value))
231
278
 
232
279
 
233
280
  class BoolMetadataField(DynamicFormItem):
@@ -251,6 +298,27 @@ class BoolMetadataField(DynamicFormItem):
251
298
  self._main_widget.setChecked(value)
252
299
 
253
300
 
301
+ class DictMetadataField(DynamicFormItem):
302
+ def __init__(self, *, parent: QWidget | None = None, spec: FormItemSpec) -> None:
303
+ super().__init__(parent=parent, spec=spec)
304
+ self._main_widget.data_changed.connect(self._value_changed)
305
+
306
+ def _set_pretty_display(self):
307
+ self._main_widget.set_button_visibility(False)
308
+ super()._set_pretty_display()
309
+
310
+ def _add_main_widget(self) -> None:
311
+ self._main_widget = DictBackedTable(self, [])
312
+ self._layout.addWidget(self._main_widget)
313
+ self._main_widget.setToolTip(self._describe(""))
314
+
315
+ def getValue(self):
316
+ return self._main_widget.dump_dict()
317
+
318
+ def setValue(self, value):
319
+ self._main_widget.replace_data(value)
320
+
321
+
254
322
  def widget_from_type(annotation: type | UnionType | None) -> type[DynamicFormItem]:
255
323
  if annotation in [str, str | None]:
256
324
  return StrMetadataField
@@ -260,6 +328,14 @@ def widget_from_type(annotation: type | UnionType | None) -> type[DynamicFormIte
260
328
  return FloatDecimalMetadataField
261
329
  if annotation in [bool, bool | None]:
262
330
  return BoolMetadataField
331
+ if annotation in [dict, dict | None] or (
332
+ isinstance(annotation, GenericAlias) and annotation.__origin__ is dict
333
+ ):
334
+ return DictMetadataField
335
+ if annotation in [list, list | None] or (
336
+ isinstance(annotation, GenericAlias) and annotation.__origin__ is list
337
+ ):
338
+ return StrMetadataField
263
339
  else:
264
340
  logger.warning(f"Type {annotation} is not (yet) supported in metadata form creation.")
265
341
  return StrMetadataField
@@ -0,0 +1,21 @@
1
+ import bec_qthemes
2
+
3
+
4
+ def pretty_display_theme(theme: str = "dark"):
5
+ palette = bec_qthemes.load_palette(theme)
6
+ foreground = palette.text().color().name()
7
+ background = palette.base().color().name()
8
+ border = palette.shadow().color().name()
9
+ accent = palette.accent().color().name()
10
+ return f"""
11
+ QWidget {{color: {foreground}; background-color: {background}}}
12
+ QLabel {{ font-weight: bold; }}
13
+ QLineEdit,QLabel,QTreeView {{ border-style: solid; border-width: 2px; border-color: {border} }}
14
+ QRadioButton {{ color: {foreground}; }}
15
+ QRadioButton::indicator::checked {{ color: {accent}; }}
16
+ QCheckBox {{ color: {accent}; }}
17
+ """
18
+
19
+
20
+ if __name__ == "__main__":
21
+ print(pretty_display_theme())