bec-widgets 2.10.3__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 +84 -0
- PKG-INFO +1 -1
- bec_widgets/cli/client.py +5 -5
- bec_widgets/tests/utils.py +2 -2
- bec_widgets/utils/clickable_label.py +13 -0
- bec_widgets/utils/colors.py +7 -4
- bec_widgets/utils/expandable_frame.py +58 -7
- bec_widgets/utils/forms_from_types/forms.py +107 -28
- bec_widgets/utils/forms_from_types/items.py +88 -12
- bec_widgets/utils/forms_from_types/styles.py +21 -0
- bec_widgets/utils/generate_designer_plugin.py +10 -21
- bec_widgets/widgets/control/scan_control/scan_control.py +1 -1
- bec_widgets/widgets/editors/dict_backed_table.py +31 -11
- bec_widgets/widgets/editors/scan_metadata/_util.py +7 -4
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +10 -4
- bec_widgets/widgets/plots/image/image.py +128 -856
- bec_widgets/widgets/plots/image/image_base.py +1062 -0
- bec_widgets/widgets/plots/image/image_item.py +7 -6
- bec_widgets/widgets/services/device_browser/device_browser.py +61 -29
- bec_widgets/widgets/services/device_browser/device_item/device_item.py +97 -19
- bec_widgets/widgets/services/device_browser/util.py +11 -0
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/METADATA +1 -1
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/RECORD +27 -23
- pyproject.toml +1 -1
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/WHEEL +0 -0
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
@@ -1,6 +1,90 @@
|
|
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
|
+
|
37
|
+
## v2.11.0 (2025-06-04)
|
38
|
+
|
39
|
+
### Bug Fixes
|
40
|
+
|
41
|
+
- **image item**: Propagate remove call to parent class
|
42
|
+
([`e211e4d`](https://github.com/bec-project/bec_widgets/commit/e211e4d7161cc4fc4b2f7cd18f058e070f5b4b7a))
|
43
|
+
|
44
|
+
- **image layer**: Add layer main if it does not exist
|
45
|
+
([`7eb2f54`](https://github.com/bec-project/bec_widgets/commit/7eb2f54e0ed556e0c30a4e14ded75e32dcf3d531))
|
46
|
+
|
47
|
+
- **image_item**: Do not disconnect the monitor from within the image item
|
48
|
+
([`4c0bd97`](https://github.com/bec-project/bec_widgets/commit/4c0bd977fc2b82680bbace763f5ffb19ed664f72))
|
49
|
+
|
50
|
+
### Features
|
51
|
+
|
52
|
+
- **image_layer**: Add default name for image layers
|
53
|
+
([`4a343b2`](https://github.com/bec-project/bec_widgets/commit/4a343b204112c53e593e9bb43642d21f268dfa85))
|
54
|
+
|
55
|
+
### Refactoring
|
56
|
+
|
57
|
+
- **image**: Disconnect when layer is removed
|
58
|
+
([`8a299a8`](https://github.com/bec-project/bec_widgets/commit/8a299a8268f3c21bbdc6629ad1f1f50a0aa0948b))
|
59
|
+
|
60
|
+
- **image**: Introduce image base and image layer; rename vrange to v_range
|
61
|
+
([`10f292d`](https://github.com/bec-project/bec_widgets/commit/10f292def9d1551bca0d8f63c0a94799c08ff507))
|
62
|
+
|
63
|
+
- **image**: Move image item creation to layer manager
|
64
|
+
([`c2b0c8c`](https://github.com/bec-project/bec_widgets/commit/c2b0c8c4336302ec4a7807c31b3f3b78a413c1aa))
|
65
|
+
|
66
|
+
- **image**: Removed access to image item config
|
67
|
+
([`99ecf6a`](https://github.com/bec-project/bec_widgets/commit/99ecf6a18f2e87d68f3de3abf56d97f7e6467912))
|
68
|
+
|
69
|
+
- **image_base**: Move default color map to image layer
|
70
|
+
([`92b89e7`](https://github.com/bec-project/bec_widgets/commit/92b89e72750fc0ab72ea51f865032133c49a7f18))
|
71
|
+
|
72
|
+
- **image_base**: Renamed layers to layer_manager and added public methods for accessing the layer
|
73
|
+
manager
|
74
|
+
([`92dade0`](https://github.com/bec-project/bec_widgets/commit/92dade09508ff3940e0b5dc99917302d61b03bc8))
|
75
|
+
|
76
|
+
- **image_item**: Emit object name with removed signal
|
77
|
+
([`a4f3117`](https://github.com/bec-project/bec_widgets/commit/a4f311794132c6c24370cb2f5b5e0725b12587fd))
|
78
|
+
|
79
|
+
- **image_item**: Removed outdated image item config
|
80
|
+
([`3e789ca`](https://github.com/bec-project/bec_widgets/commit/3e789ca35b6d0cf2d8ae9677bc65b7f0ca4eabc7))
|
81
|
+
|
82
|
+
### Testing
|
83
|
+
|
84
|
+
- Improve error message for widgets that are not properly cleaned up
|
85
|
+
([`7c47505`](https://github.com/bec-project/bec_widgets/commit/7c47505c5a147885ca2e854e13c1eb3fddaf5489))
|
86
|
+
|
87
|
+
|
4
88
|
## v2.10.3 (2025-06-04)
|
5
89
|
|
6
90
|
### Bug Fixes
|
PKG-INFO
CHANGED
bec_widgets/cli/client.py
CHANGED
@@ -1252,16 +1252,16 @@ class Image(RPCBase):
|
|
1252
1252
|
|
1253
1253
|
@property
|
1254
1254
|
@rpc_call
|
1255
|
-
def
|
1255
|
+
def v_range(self) -> "QPointF":
|
1256
1256
|
"""
|
1257
|
-
|
1257
|
+
Set the v_range of the main image.
|
1258
1258
|
"""
|
1259
1259
|
|
1260
|
-
@
|
1260
|
+
@v_range.setter
|
1261
1261
|
@rpc_call
|
1262
|
-
def
|
1262
|
+
def v_range(self) -> "QPointF":
|
1263
1263
|
"""
|
1264
|
-
|
1264
|
+
Set the v_range of the main image.
|
1265
1265
|
"""
|
1266
1266
|
|
1267
1267
|
@property
|
bec_widgets/tests/utils.py
CHANGED
@@ -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)
|
bec_widgets/utils/colors.py
CHANGED
@@ -15,12 +15,15 @@ if TYPE_CHECKING: # pragma: no cover
|
|
15
15
|
from bec_qthemes._main import AccentColors
|
16
16
|
|
17
17
|
|
18
|
-
def
|
18
|
+
def get_theme_name():
|
19
19
|
if QApplication.instance() is None or not hasattr(QApplication.instance(), "theme"):
|
20
|
-
|
20
|
+
return "dark"
|
21
21
|
else:
|
22
|
-
|
23
|
-
|
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__(
|
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
|
-
|
35
|
-
self.
|
36
|
-
self.
|
37
|
-
self._title_layout.addWidget(self.
|
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.
|
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
|
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.
|
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 =
|
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]]):
|
39
|
-
|
40
|
-
form_item_specs (list[FormItemSpec]):
|
41
|
-
|
42
|
-
|
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
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
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
|
-
|
89
|
-
.
|
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__(
|
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
|
-
|
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.
|
133
|
-
|
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(
|
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
|
|