bec-widgets 1.25.1__py3-none-any.whl → 2.0.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.
Files changed (196) hide show
  1. .gitlab-ci.yml +3 -5
  2. CHANGELOG.md +631 -0
  3. PKG-INFO +3 -3
  4. bec_widgets/__init__.py +4 -0
  5. bec_widgets/applications/bw_launch.py +23 -0
  6. bec_widgets/applications/launch_window.py +430 -0
  7. bec_widgets/assets/app_icons/auto_update.png +0 -0
  8. bec_widgets/assets/app_icons/ui_loader_tile.png +0 -0
  9. bec_widgets/cli/__init__.py +0 -1
  10. bec_widgets/cli/client.py +1779 -2064
  11. bec_widgets/cli/client_utils.py +346 -174
  12. bec_widgets/cli/generate_cli.py +143 -37
  13. bec_widgets/cli/rpc/rpc_base.py +152 -21
  14. bec_widgets/cli/rpc/rpc_register.py +113 -6
  15. bec_widgets/cli/rpc/rpc_widget_handler.py +13 -11
  16. bec_widgets/cli/server.py +125 -239
  17. bec_widgets/examples/jupyter_console/jupyter_console_window.py +97 -145
  18. bec_widgets/examples/plugin_example_pyside/tictactoetaskmenu.py +1 -1
  19. bec_widgets/utils/bec_connector.py +190 -21
  20. bec_widgets/utils/bec_designer.py +7 -0
  21. bec_widgets/utils/bec_dispatcher.py +71 -4
  22. bec_widgets/utils/bec_plugin_helper.py +89 -0
  23. bec_widgets/utils/bec_signal_proxy.py +1 -1
  24. bec_widgets/utils/bec_widget.py +26 -10
  25. bec_widgets/utils/colors.py +1 -1
  26. bec_widgets/{qt_utils → utils}/compact_popup.py +2 -0
  27. bec_widgets/utils/container_utils.py +37 -12
  28. bec_widgets/utils/crosshair.py +25 -8
  29. bec_widgets/utils/entry_validator.py +3 -1
  30. bec_widgets/{qt_utils → utils}/error_popups.py +18 -0
  31. bec_widgets/{qt_utils → utils}/expandable_frame.py +2 -2
  32. bec_widgets/utils/forms_from_types/forms.py +182 -0
  33. bec_widgets/{widgets/editors/scan_metadata/_metadata_widgets.py → utils/forms_from_types/items.py} +41 -30
  34. bec_widgets/utils/generate_designer_plugin.py +40 -36
  35. bec_widgets/utils/linear_region_selector.py +2 -0
  36. bec_widgets/utils/name_utils.py +16 -0
  37. bec_widgets/{qt_utils → utils}/palette_viewer.py +2 -2
  38. bec_widgets/utils/plot_indicator_items.py +2 -5
  39. bec_widgets/utils/plugin_utils.py +47 -1
  40. bec_widgets/{qt_utils → utils}/round_frame.py +14 -14
  41. bec_widgets/utils/rpc_server.py +277 -0
  42. bec_widgets/utils/serialization.py +44 -0
  43. bec_widgets/{qt_utils → utils}/settings_dialog.py +26 -1
  44. bec_widgets/{qt_utils → utils}/side_panel.py +17 -10
  45. bec_widgets/{qt_utils → utils}/toolbar.py +69 -25
  46. bec_widgets/utils/ui_loader.py +8 -8
  47. bec_widgets/utils/widget_io.py +166 -25
  48. bec_widgets/widgets/containers/auto_update/auto_updates.py +364 -0
  49. bec_widgets/widgets/containers/dock/dock.py +157 -49
  50. bec_widgets/widgets/containers/dock/dock_area.py +186 -138
  51. bec_widgets/widgets/containers/layout_manager/layout_manager.py +2 -1
  52. bec_widgets/widgets/containers/main_window/addons/web_links.py +15 -0
  53. bec_widgets/widgets/containers/main_window/main_window.py +189 -41
  54. bec_widgets/widgets/control/buttons/button_abort/button_abort.py +3 -4
  55. bec_widgets/widgets/control/buttons/button_reset/button_reset.py +3 -4
  56. bec_widgets/widgets/control/buttons/button_resume/button_resume.py +3 -3
  57. bec_widgets/widgets/control/buttons/stop_button/stop_button.py +18 -7
  58. bec_widgets/widgets/control/device_control/position_indicator/position_indicator.py +22 -3
  59. bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +31 -13
  60. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +3 -1
  61. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.ui +27 -4
  62. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +5 -2
  63. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +97 -31
  64. bec_widgets/widgets/control/device_control/positioner_box/positioner_control_line/positioner_control_line.ui +11 -4
  65. bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +2 -3
  66. bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +29 -4
  67. bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +1 -0
  68. bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +2 -2
  69. bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +2 -2
  70. bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py +1 -2
  71. bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.py +1 -2
  72. bec_widgets/widgets/control/scan_control/scan_control.py +7 -5
  73. bec_widgets/widgets/control/scan_control/scan_group_box.py +28 -5
  74. bec_widgets/widgets/dap/dap_combo_box/dap_combo_box.py +1 -2
  75. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog.py +3 -4
  76. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog_vertical.ui +14 -8
  77. bec_widgets/widgets/editors/console/console.py +1 -1
  78. bec_widgets/widgets/editors/{scan_metadata/additional_metadata_table.py → dict_backed_table.py} +29 -6
  79. bec_widgets/widgets/editors/scan_metadata/__init__.py +0 -7
  80. bec_widgets/widgets/editors/scan_metadata/_util.py +1 -1
  81. bec_widgets/widgets/{plots/motor_map/register_bec_motor_map_widget.py → editors/scan_metadata/register_scan_metadata.py} +2 -4
  82. bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +42 -136
  83. bec_widgets/widgets/editors/scan_metadata/scan_metadata.pyproject +1 -0
  84. bec_widgets/widgets/{plots/multi_waveform/bec_multi_waveform_widget_plugin.py → editors/scan_metadata/scan_metadata_plugin.py} +9 -9
  85. bec_widgets/widgets/editors/text_box/text_box.py +2 -3
  86. bec_widgets/widgets/editors/website/website.py +2 -2
  87. bec_widgets/widgets/games/minesweeper.py +3 -2
  88. bec_widgets/widgets/plots/image/image.py +960 -0
  89. bec_widgets/widgets/plots/image/image.pyproject +1 -0
  90. bec_widgets/widgets/plots/image/image_item.py +279 -0
  91. bec_widgets/widgets/plots/{motor_map/bec_motor_map_widget_plugin.py → image/image_plugin.py} +11 -13
  92. bec_widgets/widgets/{containers/figure/plots → plots}/image/image_processor.py +31 -64
  93. bec_widgets/widgets/plots/image/{register_bec_image_widget.py → register_image.py} +2 -2
  94. bec_widgets/widgets/plots/image/toolbar_bundles/image_selection.py +59 -0
  95. bec_widgets/widgets/plots/image/toolbar_bundles/processing.py +79 -0
  96. bec_widgets/widgets/plots/motor_map/motor_map.py +832 -0
  97. bec_widgets/widgets/plots/motor_map/motor_map.pyproject +1 -0
  98. bec_widgets/widgets/plots/motor_map/motor_map_plugin.py +54 -0
  99. bec_widgets/widgets/plots/{multi_waveform/register_bec_multi_waveform_widget.py → motor_map/register_motor_map.py} +2 -4
  100. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py +129 -0
  101. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui +120 -0
  102. bec_widgets/widgets/plots/motor_map/toolbar_bundles/motor_selection.py +70 -0
  103. bec_widgets/widgets/plots/multi_waveform/multi_waveform.py +508 -0
  104. bec_widgets/widgets/plots/multi_waveform/multi_waveform.pyproject +1 -0
  105. bec_widgets/widgets/plots/multi_waveform/multi_waveform_plugin.py +54 -0
  106. bec_widgets/widgets/plots/multi_waveform/register_multi_waveform.py +15 -0
  107. bec_widgets/widgets/plots/multi_waveform/settings/control_panel.py +144 -0
  108. bec_widgets/widgets/plots/multi_waveform/settings/multi_waveform_controls.ui +164 -0
  109. bec_widgets/widgets/plots/multi_waveform/toolbar_bundles/monitor_selection.py +65 -0
  110. bec_widgets/widgets/{plots_next_gen → plots}/plot_base.py +321 -40
  111. bec_widgets/widgets/plots/{waveform/register_bec_waveform_widget.py → scatter_waveform/register_scatter_waveform.py} +3 -3
  112. bec_widgets/widgets/plots/scatter_waveform/scatter_curve.py +197 -0
  113. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py +553 -0
  114. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.pyproject +1 -0
  115. bec_widgets/widgets/plots/{image/bec_image_widget_plugin.py → scatter_waveform/scatter_waveform_plugin.py} +9 -13
  116. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_setting.py +138 -0
  117. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_horizontal.ui +195 -0
  118. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_vertical.ui +204 -0
  119. bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings.py +8 -8
  120. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/mouse_interactions.py +4 -18
  121. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/plot_export.py +14 -3
  122. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/roi_bundle.py +6 -1
  123. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/save_state.py +2 -2
  124. bec_widgets/widgets/{containers/figure/plots/waveform/waveform_curve.py → plots/waveform/curve.py} +119 -49
  125. bec_widgets/widgets/plots/waveform/register_waveform.py +15 -0
  126. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_setting.py +125 -0
  127. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +576 -0
  128. bec_widgets/widgets/plots/waveform/utils/__init__.py +0 -0
  129. bec_widgets/widgets/plots/waveform/utils/roi_manager.py +84 -0
  130. bec_widgets/widgets/plots/waveform/waveform.py +1794 -0
  131. bec_widgets/widgets/plots/waveform/waveform.pyproject +1 -0
  132. bec_widgets/widgets/plots/waveform/{bec_waveform_widget_plugin.py → waveform_plugin.py} +9 -13
  133. bec_widgets/widgets/progress/bec_progressbar/bec_progressbar.py +1 -2
  134. bec_widgets/widgets/progress/ring_progress_bar/ring.py +11 -10
  135. bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +24 -14
  136. bec_widgets/widgets/services/bec_queue/bec_queue.py +13 -11
  137. bec_widgets/widgets/services/bec_status_box/bec_status_box.py +3 -4
  138. bec_widgets/widgets/services/device_browser/device_browser.py +5 -2
  139. bec_widgets/widgets/services/device_browser/device_item/device_item.py +1 -1
  140. bec_widgets/widgets/utility/logpanel/logpanel.py +36 -17
  141. bec_widgets/widgets/utility/spinbox/decimal_spinbox.py +3 -3
  142. bec_widgets/widgets/utility/visual/color_button/color_button.py +1 -1
  143. bec_widgets/widgets/utility/visual/colormap_widget/colormap_widget.py +4 -6
  144. bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py +4 -8
  145. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/METADATA +3 -3
  146. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/RECORD +168 -153
  147. pyproject.toml +3 -3
  148. bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +0 -198
  149. bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +0 -615
  150. bec_widgets/applications/bec_app.py +0 -84
  151. bec_widgets/cli/auto_updates.py +0 -168
  152. bec_widgets/widgets/containers/figure/__init__.py +0 -1
  153. bec_widgets/widgets/containers/figure/figure.py +0 -796
  154. bec_widgets/widgets/containers/figure/plots/axis_settings.py +0 -91
  155. bec_widgets/widgets/containers/figure/plots/axis_settings.ui +0 -256
  156. bec_widgets/widgets/containers/figure/plots/image/image.py +0 -772
  157. bec_widgets/widgets/containers/figure/plots/image/image_item.py +0 -337
  158. bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py +0 -525
  159. bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py +0 -340
  160. bec_widgets/widgets/containers/figure/plots/plot_base.py +0 -505
  161. bec_widgets/widgets/containers/figure/plots/waveform/waveform.py +0 -1563
  162. bec_widgets/widgets/plots/image/bec_image_widget.pyproject +0 -1
  163. bec_widgets/widgets/plots/image/image_widget.py +0 -515
  164. bec_widgets/widgets/plots/motor_map/bec_motor_map_widget.pyproject +0 -1
  165. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.py +0 -56
  166. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.ui +0 -108
  167. bec_widgets/widgets/plots/motor_map/motor_map_widget.py +0 -234
  168. bec_widgets/widgets/plots/multi_waveform/bec_multi_waveform_widget.pyproject +0 -1
  169. bec_widgets/widgets/plots/multi_waveform/multi_waveform_controls.ui +0 -99
  170. bec_widgets/widgets/plots/multi_waveform/multi_waveform_widget.py +0 -536
  171. bec_widgets/widgets/plots/waveform/bec_waveform_widget.pyproject +0 -1
  172. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.py +0 -336
  173. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.ui +0 -372
  174. bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py +0 -25
  175. bec_widgets/widgets/plots/waveform/waveform_widget.py +0 -751
  176. /bec_widgets/{qt_utils → utils}/collapsible_panel_manager.py +0 -0
  177. /bec_widgets/{applications/alignment → utils/forms_from_types}/__init__.py +0 -0
  178. /bec_widgets/{qt_utils → utils}/redis_message_waiter.py +0 -0
  179. /bec_widgets/{applications/alignment/alignment_1d → widgets/containers/auto_update}/__init__.py +0 -0
  180. /bec_widgets/{qt_utils → widgets/containers/main_window/addons}/__init__.py +0 -0
  181. /bec_widgets/widgets/{containers/figure/plots → plots/image/toolbar_bundles}/__init__.py +0 -0
  182. /bec_widgets/widgets/{containers/figure/plots/image → plots/motor_map/settings}/__init__.py +0 -0
  183. /bec_widgets/widgets/{containers/figure/plots/motor_map → plots/motor_map/toolbar_bundles}/__init__.py +0 -0
  184. /bec_widgets/widgets/{containers/figure/plots/multi_waveform → plots/multi_waveform/settings}/__init__.py +0 -0
  185. /bec_widgets/widgets/{containers/figure/plots/waveform → plots/multi_waveform/toolbar_bundles}/__init__.py +0 -0
  186. /bec_widgets/widgets/plots/{motor_map/motor_map_dialog → scatter_waveform}/__init__.py +0 -0
  187. /bec_widgets/widgets/plots/{waveform/waveform_popups → scatter_waveform/settings}/__init__.py +0 -0
  188. /bec_widgets/widgets/plots/{waveform/waveform_popups/curve_dialog → setting_menus}/__init__.py +0 -0
  189. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_horizontal.ui +0 -0
  190. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_vertical.ui +0 -0
  191. /bec_widgets/widgets/plots/{waveform/waveform_popups/dap_summary_dialog → toolbar_bundles}/__init__.py +0 -0
  192. /bec_widgets/widgets/{plots_next_gen/setting_menus → plots/waveform/settings}/__init__.py +0 -0
  193. /bec_widgets/widgets/{plots_next_gen/toolbar_bundles → plots/waveform/settings/curve_settings}/__init__.py +0 -0
  194. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/WHEEL +0 -0
  195. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/entry_points.txt +0 -0
  196. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ from decimal import Decimal
4
+ from types import NoneType
5
+
6
+ from bec_lib.logger import bec_logger
7
+ from bec_qthemes import material_icon
8
+ from pydantic import BaseModel, ValidationError
9
+ from qtpy.QtCore import Signal # type: ignore
10
+ from qtpy.QtWidgets import QGridLayout, QLabel, QLayout, QVBoxLayout, QWidget
11
+
12
+ from bec_widgets.utils.bec_widget import BECWidget
13
+ from bec_widgets.utils.compact_popup import CompactPopupWidget
14
+ from bec_widgets.utils.forms_from_types.items import FormItemSpec, widget_from_type
15
+
16
+ logger = bec_logger.logger
17
+
18
+
19
+ class TypedForm(BECWidget, QWidget):
20
+ PLUGIN = True
21
+ ICON_NAME = "list_alt"
22
+
23
+ value_changed = Signal()
24
+
25
+ RPC = False
26
+
27
+ def __init__(
28
+ self,
29
+ parent=None,
30
+ items: list[tuple[str, type]] | None = None,
31
+ form_item_specs: list[FormItemSpec] | None = None,
32
+ client=None,
33
+ **kwargs,
34
+ ):
35
+ """Widget with a list of form items based on a list of types.
36
+
37
+ 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
+
44
+ """
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")
49
+ super().__init__(parent=parent, client=client, **kwargs)
50
+ self._items = (
51
+ form_item_specs
52
+ if form_item_specs is not None
53
+ else [
54
+ FormItemSpec(name=name, item_type=item_type)
55
+ for name, item_type in items # type: ignore
56
+ ]
57
+ )
58
+ self._layout = QVBoxLayout()
59
+ self._layout.setContentsMargins(0, 0, 0, 0)
60
+ self.setLayout(self._layout)
61
+
62
+ self._form_grid_container = QWidget(parent=self)
63
+ self._form_grid = QWidget(parent=self._form_grid_container)
64
+ self._layout.addWidget(self._form_grid_container)
65
+ self._form_grid_container.setLayout(QVBoxLayout())
66
+ self._form_grid.setLayout(self._new_grid_layout())
67
+
68
+ self.populate()
69
+
70
+ def populate(self):
71
+ self._clear_grid()
72
+ for r, item in enumerate(self._items):
73
+ self._add_griditem(item, r)
74
+
75
+ def _add_griditem(self, item: FormItemSpec, row: int):
76
+ grid = self._form_grid.layout()
77
+ label = QLabel(item.name)
78
+ label.setProperty("_model_field_name", item.name)
79
+ label.setToolTip(item.info.description or item.name)
80
+ grid.addWidget(label, row, 0)
81
+ widget = widget_from_type(item.item_type)(parent=self, spec=item)
82
+ widget.valueChanged.connect(self.value_changed)
83
+ grid.addWidget(widget, row, 1)
84
+
85
+ def _dict_from_grid(self) -> dict[str, str | int | float | Decimal | bool]:
86
+ grid: QGridLayout = self._form_grid.layout() # type: ignore
87
+ 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())
94
+ }
95
+
96
+ def _clear_grid(self):
97
+ if (old_layout := self._form_grid.layout()) is not None:
98
+ while old_layout.count():
99
+ item = old_layout.takeAt(0)
100
+ widget = item.widget()
101
+ if widget is not None:
102
+ widget.deleteLater()
103
+ old_layout.deleteLater()
104
+ self._form_grid.deleteLater()
105
+ self._form_grid = QWidget()
106
+
107
+ self._form_grid.setLayout(self._new_grid_layout())
108
+ self._form_grid_container.layout().addWidget(self._form_grid)
109
+
110
+ self._form_grid.adjustSize()
111
+ self._form_grid_container.adjustSize()
112
+ self.adjustSize()
113
+
114
+ def _new_grid_layout(self):
115
+ new_grid = QGridLayout()
116
+ new_grid.setContentsMargins(0, 0, 0, 0)
117
+ new_grid.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
118
+ return new_grid
119
+
120
+
121
+ class PydanticModelForm(TypedForm):
122
+ metadata_updated = Signal(dict)
123
+ metadata_cleared = Signal(NoneType)
124
+
125
+ def __init__(self, parent=None, metadata_model: type[BaseModel] = None, client=None, **kwargs):
126
+ """
127
+ A form generated from a pydantic model.
128
+
129
+ Args:
130
+ metadata_model (type[BaseModel]): the model class for which to generate a form.
131
+ """
132
+ self._md_schema = metadata_model
133
+ super().__init__(parent=parent, form_item_specs=self._form_item_specs(), client=client)
134
+
135
+ self._validity = CompactPopupWidget()
136
+ self._validity.compact_view = True # type: ignore
137
+ self._validity.label = "Metadata validity" # type: ignore
138
+ self._validity.compact_show_popup.setIcon(
139
+ material_icon(icon_name="info", size=(10, 10), convert_to_pixmap=False)
140
+ )
141
+ self._validity_message = QLabel("Not yet validated")
142
+ self._validity.addWidget(self._validity_message)
143
+ self._layout.addWidget(self._validity)
144
+ self.value_changed.connect(self.validate_form)
145
+
146
+ def set_schema(self, schema: type[BaseModel]):
147
+ self._md_schema = schema
148
+ self.populate()
149
+
150
+ def _form_item_specs(self):
151
+ return [
152
+ FormItemSpec(name=name, info=info, item_type=info.annotation)
153
+ for name, info in self._md_schema.model_fields.items()
154
+ ]
155
+
156
+ def update_items_from_schema(self):
157
+ self._items = self._form_item_specs()
158
+
159
+ def populate(self):
160
+ self.update_items_from_schema()
161
+ super().populate()
162
+
163
+ def get_form_data(self):
164
+ """Get the entered metadata as a dict."""
165
+ return self._dict_from_grid()
166
+
167
+ def validate_form(self, *_) -> bool:
168
+ """validate the currently entered metadata against the pydantic schema.
169
+ If successful, returns on metadata_emitted and returns true.
170
+ Otherwise, emits on metadata_cleared and returns false."""
171
+ try:
172
+ metadata_dict = self.get_form_data()
173
+ self._md_schema.model_validate(metadata_dict)
174
+ self._validity.set_global_state("success")
175
+ self._validity_message.setText("No errors!")
176
+ self.metadata_updated.emit(metadata_dict)
177
+ return True
178
+ except ValidationError as e:
179
+ self._validity.set_global_state("emergency")
180
+ self._validity_message.setText(str(e))
181
+ self.metadata_cleared.emit(None)
182
+ return False
@@ -2,11 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  from abc import abstractmethod
4
4
  from decimal import Decimal
5
- from typing import TYPE_CHECKING, Callable, get_args
5
+ from types import UnionType
6
+ from typing import Callable, Protocol
6
7
 
7
8
  from bec_lib.logger import bec_logger
8
9
  from bec_qthemes import material_icon
9
- from pydantic import BaseModel, Field
10
+ from pydantic import BaseModel, ConfigDict, Field
11
+ from pydantic.fields import FieldInfo
10
12
  from qtpy.QtCore import Signal # type: ignore
11
13
  from qtpy.QtWidgets import (
12
14
  QApplication,
@@ -33,12 +35,22 @@ from bec_widgets.widgets.editors.scan_metadata._util import (
33
35
  field_precision,
34
36
  )
35
37
 
36
- if TYPE_CHECKING:
37
- from pydantic.fields import FieldInfo
38
-
39
38
  logger = bec_logger.logger
40
39
 
41
40
 
41
+ class FormItemSpec(BaseModel):
42
+ """
43
+ The specification for an item in a dynamically generated form. Uses a pydantic FieldInfo
44
+ to store most annotation info, since one of the main purposes is to store data for
45
+ forms genrated from pydantic models, but can also be composed from other sources or by hand.
46
+ """
47
+
48
+ model_config = ConfigDict(arbitrary_types_allowed=True)
49
+ item_type: type | UnionType
50
+ name: str
51
+ info: FieldInfo = FieldInfo()
52
+
53
+
42
54
  class ClearableBoolEntry(QWidget):
43
55
  stateChanged = Signal()
44
56
 
@@ -82,21 +94,20 @@ class ClearableBoolEntry(QWidget):
82
94
  self._false.setToolTip(tooltip)
83
95
 
84
96
 
85
- class MetadataWidget(QWidget):
86
-
97
+ class DynamicFormItem(QWidget):
87
98
  valueChanged = Signal()
88
99
 
89
- def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
100
+ def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
90
101
  super().__init__(parent)
91
- self._info = info
102
+ self._spec = spec
92
103
  self._layout = QHBoxLayout()
93
104
  self._layout.setContentsMargins(0, 0, 0, 0)
94
105
  self._layout.setSizeConstraint(QLayout.SizeConstraint.SetMaximumSize)
95
- self._default = field_default(self._info)
96
- self._desc = self._info.description
106
+ self._default = field_default(self._spec.info)
107
+ self._desc = self._spec.info.description
97
108
  self.setLayout(self._layout)
98
109
  self._add_main_widget()
99
- if clearable_required(info):
110
+ if clearable_required(spec.info):
100
111
  self._add_clear_button()
101
112
 
102
113
  @abstractmethod
@@ -127,15 +138,15 @@ class MetadataWidget(QWidget):
127
138
  self.valueChanged.emit()
128
139
 
129
140
 
130
- class StrMetadataField(MetadataWidget):
131
- def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
132
- super().__init__(info, parent)
141
+ class StrMetadataField(DynamicFormItem):
142
+ def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
143
+ super().__init__(parent=parent, spec=spec)
133
144
  self._main_widget.textChanged.connect(self._value_changed)
134
145
 
135
146
  def _add_main_widget(self) -> None:
136
147
  self._main_widget = QLineEdit()
137
148
  self._layout.addWidget(self._main_widget)
138
- min_length, max_length = field_minlen(self._info), field_maxlen(self._info)
149
+ min_length, max_length = (field_minlen(self._spec.info), field_maxlen(self._spec.info))
139
150
  if max_length:
140
151
  self._main_widget.setMaxLength(max_length)
141
152
  self._main_widget.setToolTip(
@@ -156,15 +167,15 @@ class StrMetadataField(MetadataWidget):
156
167
  self._main_widget.setText(value)
157
168
 
158
169
 
159
- class IntMetadataField(MetadataWidget):
160
- def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
161
- super().__init__(info, parent)
170
+ class IntMetadataField(DynamicFormItem):
171
+ def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
172
+ super().__init__(parent=parent, spec=spec)
162
173
  self._main_widget.textChanged.connect(self._value_changed)
163
174
 
164
175
  def _add_main_widget(self) -> None:
165
176
  self._main_widget = QSpinBox()
166
177
  self._layout.addWidget(self._main_widget)
167
- min_, max_ = field_limits(self._info, int)
178
+ min_, max_ = field_limits(self._spec.info, int)
168
179
  self._main_widget.setMinimum(min_)
169
180
  self._main_widget.setMaximum(max_)
170
181
  self._main_widget.setToolTip(f"(range {min_} to {max_}){self._describe()}")
@@ -185,18 +196,18 @@ class IntMetadataField(MetadataWidget):
185
196
  self._main_widget.setValue(value)
186
197
 
187
198
 
188
- class FloatDecimalMetadataField(MetadataWidget):
189
- def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
190
- super().__init__(info, parent)
199
+ class FloatDecimalMetadataField(DynamicFormItem):
200
+ def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
201
+ super().__init__(parent=parent, spec=spec)
191
202
  self._main_widget.textChanged.connect(self._value_changed)
192
203
 
193
204
  def _add_main_widget(self) -> None:
194
205
  self._main_widget = QDoubleSpinBox()
195
206
  self._layout.addWidget(self._main_widget)
196
- min_, max_ = field_limits(self._info, int)
207
+ min_, max_ = field_limits(self._spec.info, int)
197
208
  self._main_widget.setMinimum(min_)
198
209
  self._main_widget.setMaximum(max_)
199
- precision = field_precision(self._info)
210
+ precision = field_precision(self._spec.info)
200
211
  if precision:
201
212
  self._main_widget.setDecimals(precision)
202
213
  minstr = f"{float(min_):.3f}" if abs(min_) <= 1000 else f"{float(min_):.3e}"
@@ -219,13 +230,13 @@ class FloatDecimalMetadataField(MetadataWidget):
219
230
  self._main_widget.setValue(value)
220
231
 
221
232
 
222
- class BoolMetadataField(MetadataWidget):
223
- def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
224
- super().__init__(info, parent)
233
+ class BoolMetadataField(DynamicFormItem):
234
+ def __init__(self, *, parent: QWidget | None = None, spec: FormItemSpec) -> None:
235
+ super().__init__(parent=parent, spec=spec)
225
236
  self._main_widget.stateChanged.connect(self._value_changed)
226
237
 
227
238
  def _add_main_widget(self) -> None:
228
- if clearable_required(self._info):
239
+ if clearable_required(self._spec.info):
229
240
  self._main_widget = ClearableBoolEntry()
230
241
  else:
231
242
  self._main_widget = QCheckBox()
@@ -240,7 +251,7 @@ class BoolMetadataField(MetadataWidget):
240
251
  self._main_widget.setChecked(value)
241
252
 
242
253
 
243
- def widget_from_type(annotation: type | None) -> Callable[[FieldInfo], MetadataWidget]:
254
+ def widget_from_type(annotation: type | UnionType | None) -> type[DynamicFormItem]:
244
255
  if annotation in [str, str | None]:
245
256
  return StrMetadataField
246
257
  if annotation in [int, int | None]:
@@ -1,17 +1,30 @@
1
1
  import inspect
2
2
  import os
3
3
  import re
4
+ from typing import NamedTuple
4
5
 
5
6
  from qtpy.QtCore import QObject
6
7
 
8
+ from bec_widgets.utils.name_utils import pascal_to_snake
9
+
7
10
  EXCLUDED_PLUGINS = ["BECConnector", "BECDockArea", "BECDock", "BECFigure"]
8
11
 
9
12
 
13
+ class PluginFilenames(NamedTuple):
14
+ register: str
15
+ plugin: str
16
+ pyproj: str
17
+
18
+
19
+ def plugin_filenames(name: str) -> PluginFilenames:
20
+ return PluginFilenames(f"register_{name}.py", f"{name}_plugin.py", f"{name}.pyproject")
21
+
22
+
10
23
  class DesignerPluginInfo:
11
24
  def __init__(self, plugin_class):
12
25
  self.plugin_class = plugin_class
13
26
  self.plugin_name_pascal = plugin_class.__name__
14
- self.plugin_name_snake = self.pascal_to_snake(self.plugin_name_pascal)
27
+ self.plugin_name_snake = pascal_to_snake(self.plugin_name_pascal)
15
28
  self.widget_import = f"from {plugin_class.__module__} import {self.plugin_name_pascal}"
16
29
  plugin_module = (
17
30
  ".".join(plugin_class.__module__.split(".")[:-1]) + f".{self.plugin_name_snake}_plugin"
@@ -27,21 +40,6 @@ class DesignerPluginInfo:
27
40
 
28
41
  self.base_path = os.path.dirname(inspect.getfile(plugin_class))
29
42
 
30
- @staticmethod
31
- def pascal_to_snake(name: str) -> str:
32
- """
33
- Convert PascalCase to snake_case.
34
-
35
- Args:
36
- name (str): The name to be converted.
37
-
38
- Returns:
39
- str: The converted name.
40
- """
41
- s1 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
42
- s2 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s1)
43
- return s2.lower()
44
-
45
43
 
46
44
  class DesignerPluginGenerator:
47
45
  def __init__(self, widget: type):
@@ -53,11 +51,15 @@ class DesignerPluginGenerator:
53
51
  self._excluded = True
54
52
  return
55
53
 
56
- self.templates = {}
54
+ self.templates: dict[str, str] = {}
57
55
  self.template_path = os.path.join(
58
56
  os.path.dirname(os.path.abspath(__file__)), "plugin_templates"
59
57
  )
60
58
 
59
+ @property
60
+ def filenames(self):
61
+ return plugin_filenames(self.info.plugin_name_snake)
62
+
61
63
  def run(self, validate=True):
62
64
  if self._excluded:
63
65
  print(f"Plugin {self.widget.__name__} is excluded from generation.")
@@ -107,31 +109,33 @@ class DesignerPluginGenerator:
107
109
  or bool(init_source.find("super().__init__(parent)") > 0)
108
110
  )
109
111
 
112
+ # for the new style classes, we only have one super call. We can therefore check if the
113
+ # number of __init__ calls is 2 (the class itself and the super class)
114
+ num_inits = re.findall(r"__init__", init_source)
115
+ if len(num_inits) == 2 and not super_init_found:
116
+ super_init_found = bool(
117
+ init_source.find("super().__init__(parent=parent") > 0
118
+ or init_source.find("super().__init__(parent,") > 0
119
+ or init_source.find("super().__init__(parent)") > 0
120
+ )
121
+
110
122
  if not cls_init_found and not super_init_found:
111
123
  raise ValueError(
112
124
  f"Widget class {self.widget.__name__} must call the super constructor with parent."
113
125
  )
114
126
 
127
+ def _write_file(self, name: str, contents: str):
128
+ with open(os.path.join(self.info.base_path, name), "w", encoding="utf-8") as f:
129
+ f.write(contents)
130
+
131
+ def _format(self, name: str):
132
+ return self.templates[name].format(**self.info.__dict__)
133
+
115
134
  def _write_templates(self):
116
- self._write_register()
117
- self._write_plugin()
118
- self._write_pyproject()
119
-
120
- def _write_register(self):
121
- file_path = os.path.join(self.info.base_path, f"register_{self.info.plugin_name_snake}.py")
122
- with open(file_path, "w", encoding="utf-8") as f:
123
- f.write(self.templates["register"].format(**self.info.__dict__))
124
-
125
- def _write_plugin(self):
126
- file_path = os.path.join(self.info.base_path, f"{self.info.plugin_name_snake}_plugin.py")
127
- with open(file_path, "w", encoding="utf-8") as f:
128
- f.write(self.templates["plugin"].format(**self.info.__dict__))
129
-
130
- def _write_pyproject(self):
131
- file_path = os.path.join(self.info.base_path, f"{self.info.plugin_name_snake}.pyproject")
132
- out = {"files": [f"{self.info.plugin_class.__module__.split('.')[-1]}.py"]}
133
- with open(file_path, "w", encoding="utf-8") as f:
134
- f.write(str(out))
135
+ self._write_file(self.filenames.register, self._format("register"))
136
+ self._write_file(self.filenames.plugin, self._format("plugin"))
137
+ pyproj = str({"files": [f"{self.info.plugin_class.__module__.split('.')[-1]}.py"]})
138
+ self._write_file(self.filenames.pyproj, pyproj)
135
139
 
136
140
  def _load_templates(self):
137
141
  for file in os.listdir(self.template_path):
@@ -1,6 +1,8 @@
1
1
  """ Module for a thin wrapper (LinearRegionWrapper) around the LinearRegionItem in pyqtgraph.
2
2
  The class is mainly designed for usage with the BECWaveform and 1D plots. """
3
3
 
4
+ from __future__ import annotations
5
+
4
6
  import pyqtgraph as pg
5
7
  from qtpy.QtCore import QObject, Signal, Slot
6
8
  from qtpy.QtGui import QColor
@@ -0,0 +1,16 @@
1
+ import re
2
+
3
+
4
+ def pascal_to_snake(name: str) -> str:
5
+ """
6
+ Convert PascalCase to snake_case.
7
+
8
+ Args:
9
+ name (str): The name to be converted.
10
+
11
+ Returns:
12
+ str: The converted name.
13
+ """
14
+ s1 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
15
+ s2 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s1)
16
+ return s2.lower()
@@ -22,10 +22,10 @@ class PaletteViewer(BECWidget, QWidget):
22
22
  """
23
23
 
24
24
  ICON_NAME = "palette"
25
+ RPC = False
25
26
 
26
27
  def __init__(self, *args, parent=None, **kwargs):
27
- super().__init__(*args, theme_update=True, **kwargs)
28
- QWidget.__init__(self, parent=parent)
28
+ super().__init__(parent=parent, theme_update=True, **kwargs)
29
29
  self.setFixedSize(400, 600)
30
30
  layout = QVBoxLayout(self)
31
31
  dark_mode_button = DarkModeButton(self)
@@ -148,10 +148,7 @@ class BECTickItem(BECIndicatorItem):
148
148
  def cleanup(self) -> None:
149
149
  """Cleanup the item"""
150
150
  self.remove_from_plot()
151
- if self.tick_item is not None:
152
- self.tick_item.close()
153
- self.tick_item.deleteLater()
154
- self.tick_item = None
151
+ self.tick_item = None
155
152
 
156
153
 
157
154
  class BECArrowItem(BECIndicatorItem):
@@ -174,7 +171,7 @@ class BECArrowItem(BECIndicatorItem):
174
171
 
175
172
  def __init__(self, plot_item: pg.PlotItem = None, parent=None):
176
173
  super().__init__(plot_item=plot_item, parent=parent)
177
- self.arrow_item = pg.ArrowItem(parent=parent)
174
+ self.arrow_item = pg.ArrowItem()
178
175
  self.arrow_item.skip_auto_range = True
179
176
  self._pos = (0, 0)
180
177
  self.arrow_item.setVisible(False)
@@ -1,7 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  import importlib
2
4
  import inspect
3
5
  import os
4
6
  from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING
5
8
 
6
9
  from bec_lib.plugin_helper import _get_available_plugins
7
10
  from qtpy.QtWidgets import QGraphicsWidget, QWidget
@@ -9,6 +12,9 @@ from qtpy.QtWidgets import QGraphicsWidget, QWidget
9
12
  from bec_widgets.utils import BECConnector
10
13
  from bec_widgets.utils.bec_widget import BECWidget
11
14
 
15
+ if TYPE_CHECKING: # pragma: no cover
16
+ from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
17
+
12
18
 
13
19
  def get_plugin_widgets() -> dict[str, BECConnector]:
14
20
  """
@@ -45,6 +51,40 @@ def _filter_plugins(obj):
45
51
  return inspect.isclass(obj) and issubclass(obj, BECConnector)
46
52
 
47
53
 
54
+ def get_plugin_auto_updates() -> dict[str, type[AutoUpdates]]:
55
+ """
56
+ Get all available auto update classes from the plugin directory. AutoUpdates must inherit from AutoUpdate and be
57
+ placed in the plugin repository's bec_widgets/auto_updates directory. The entry point for the auto updates is
58
+ specified in the respective pyproject.toml file using the following key:
59
+ [project.entry-points."bec.widgets.auto_updates"]
60
+ plugin_widgets_update = "<beamline_name>.bec_widgets.auto_updates"
61
+
62
+ e.g.
63
+ [project.entry-points."bec.widgets.auto_updates"]
64
+ plugin_widgets_update = "pxiii_bec.bec_widgets.auto_updates"
65
+
66
+ Returns:
67
+ dict[str, AutoUpdates]: A dictionary of widget names and their respective classes.
68
+ """
69
+ modules = _get_available_plugins("bec.widgets.auto_updates")
70
+ loaded_plugins = {}
71
+ for module in modules:
72
+ mods = inspect.getmembers(module, predicate=_filter_auto_updates)
73
+ for name, mod_cls in mods:
74
+ if name in loaded_plugins:
75
+ print(f"Duplicated auto update {name}.")
76
+ loaded_plugins[name] = mod_cls
77
+ return loaded_plugins
78
+
79
+
80
+ def _filter_auto_updates(obj):
81
+ from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
82
+
83
+ return (
84
+ inspect.isclass(obj) and issubclass(obj, AutoUpdates) and not obj.__name__ == "AutoUpdates"
85
+ )
86
+
87
+
48
88
  @dataclass
49
89
  class BECClassInfo:
50
90
  name: str
@@ -58,7 +98,13 @@ class BECClassInfo:
58
98
 
59
99
  class BECClassContainer:
60
100
  def __init__(self):
61
- self._collection = []
101
+ self._collection: list[BECClassInfo] = []
102
+
103
+ def __repr__(self):
104
+ return str(list(cl.name for cl in self.collection))
105
+
106
+ def __iter__(self):
107
+ return self._collection.__iter__()
62
108
 
63
109
  def add_class(self, class_info: BECClassInfo):
64
110
  """
@@ -2,11 +2,10 @@ import pyqtgraph as pg
2
2
  from qtpy.QtCore import Property
3
3
  from qtpy.QtWidgets import QApplication, QFrame, QHBoxLayout, QVBoxLayout, QWidget
4
4
 
5
- from bec_widgets.utils.bec_widget import BECWidget
6
5
  from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
7
6
 
8
7
 
9
- class RoundedFrame(BECWidget, QFrame):
8
+ class RoundedFrame(QFrame):
10
9
  """
11
10
  A custom QFrame with rounded corners and optional theme updates.
12
11
  The frame can contain any QWidget, however it is mainly designed to wrap PlotWidgets to provide a consistent look and feel with other BEC Widgets.
@@ -17,15 +16,12 @@ class RoundedFrame(BECWidget, QFrame):
17
16
  parent=None,
18
17
  content_widget: QWidget = None,
19
18
  background_color: str = None,
20
- theme_update: bool = True,
19
+ orientation: str = "horizontal",
21
20
  radius: int = 10,
22
- **kwargs,
23
21
  ):
24
- super().__init__(**kwargs)
25
22
  QFrame.__init__(self, parent)
26
23
 
27
24
  self.background_color = background_color
28
- self.theme_update = theme_update if background_color is None else False
29
25
  self._radius = radius
30
26
 
31
27
  # Apply rounded frame styling
@@ -33,8 +29,12 @@ class RoundedFrame(BECWidget, QFrame):
33
29
  self.setObjectName("roundedFrame")
34
30
 
35
31
  # Create a layout for the frame
36
- self.layout = QHBoxLayout(self)
37
- self.layout.setContentsMargins(5, 5, 5, 5) # Set 5px margin
32
+ if orientation == "vertical":
33
+ self.layout = QVBoxLayout(self)
34
+ self.layout.setContentsMargins(5, 5, 5, 5)
35
+ else:
36
+ self.layout = QHBoxLayout(self)
37
+ self.layout.setContentsMargins(5, 5, 5, 5) # Set 5px margin
38
38
 
39
39
  # Add the content widget to the layout
40
40
  if content_widget:
@@ -46,14 +46,14 @@ class RoundedFrame(BECWidget, QFrame):
46
46
  # Automatically apply initial styles to the GraphicalLayoutWidget if applicable
47
47
  self.apply_plot_widget_style()
48
48
 
49
- self._connect_to_theme_change()
50
-
51
49
  def apply_theme(self, theme: str):
52
50
  """
53
51
  Apply the theme to the frame and its content if theme updates are enabled.
54
52
  """
55
- if not self.theme_update:
56
- return
53
+ if self.content_widget is not None and isinstance(
54
+ self.content_widget, pg.GraphicsLayoutWidget
55
+ ):
56
+ self.content_widget.setBackground(self.background_color)
57
57
 
58
58
  # Update background color based on the theme
59
59
  if theme == "light":
@@ -129,8 +129,8 @@ class ExampleApp(QWidget): # pragma: no cover
129
129
  plot2.plot_item = plot_item_2
130
130
 
131
131
  # Wrap PlotWidgets in RoundedFrame
132
- rounded_plot1 = RoundedFrame(content_widget=plot1, theme_update=True)
133
- rounded_plot2 = RoundedFrame(content_widget=plot2, theme_update=True)
132
+ rounded_plot1 = RoundedFrame(parent=self, content_widget=plot1)
133
+ rounded_plot2 = RoundedFrame(parent=self, content_widget=plot2)
134
134
 
135
135
  # Add to layout
136
136
  layout.addWidget(dark_button)