bec-widgets 1.25.1__py3-none-any.whl → 2.0.1__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.
- .gitlab-ci.yml +3 -5
- CHANGELOG.md +639 -0
- PKG-INFO +3 -3
- bec_widgets/__init__.py +4 -0
- bec_widgets/applications/bw_launch.py +23 -0
- bec_widgets/applications/launch_window.py +430 -0
- bec_widgets/assets/app_icons/auto_update.png +0 -0
- bec_widgets/assets/app_icons/ui_loader_tile.png +0 -0
- bec_widgets/cli/__init__.py +0 -1
- bec_widgets/cli/client.py +1779 -2064
- bec_widgets/cli/client_utils.py +346 -174
- bec_widgets/cli/generate_cli.py +143 -37
- bec_widgets/cli/rpc/rpc_base.py +152 -21
- bec_widgets/cli/rpc/rpc_register.py +113 -6
- bec_widgets/cli/rpc/rpc_widget_handler.py +13 -11
- bec_widgets/cli/server.py +125 -239
- bec_widgets/examples/jupyter_console/jupyter_console_window.py +97 -145
- bec_widgets/examples/plugin_example_pyside/tictactoetaskmenu.py +1 -1
- bec_widgets/utils/bec_connector.py +190 -21
- bec_widgets/utils/bec_designer.py +7 -0
- bec_widgets/utils/bec_dispatcher.py +71 -4
- bec_widgets/utils/bec_plugin_helper.py +89 -0
- bec_widgets/utils/bec_signal_proxy.py +1 -1
- bec_widgets/utils/bec_widget.py +26 -10
- bec_widgets/utils/colors.py +1 -1
- bec_widgets/{qt_utils → utils}/compact_popup.py +2 -0
- bec_widgets/utils/container_utils.py +37 -12
- bec_widgets/utils/crosshair.py +25 -8
- bec_widgets/utils/entry_validator.py +3 -1
- bec_widgets/{qt_utils → utils}/error_popups.py +18 -0
- bec_widgets/{qt_utils → utils}/expandable_frame.py +2 -2
- bec_widgets/utils/forms_from_types/forms.py +182 -0
- bec_widgets/{widgets/editors/scan_metadata/_metadata_widgets.py → utils/forms_from_types/items.py} +41 -30
- bec_widgets/utils/generate_designer_plugin.py +40 -36
- bec_widgets/utils/linear_region_selector.py +2 -0
- bec_widgets/utils/name_utils.py +16 -0
- bec_widgets/{qt_utils → utils}/palette_viewer.py +2 -2
- bec_widgets/utils/plot_indicator_items.py +2 -5
- bec_widgets/utils/plugin_utils.py +47 -1
- bec_widgets/{qt_utils → utils}/round_frame.py +14 -14
- bec_widgets/utils/rpc_server.py +277 -0
- bec_widgets/utils/serialization.py +44 -0
- bec_widgets/{qt_utils → utils}/settings_dialog.py +26 -1
- bec_widgets/{qt_utils → utils}/side_panel.py +17 -10
- bec_widgets/{qt_utils → utils}/toolbar.py +69 -25
- bec_widgets/utils/ui_loader.py +8 -8
- bec_widgets/utils/widget_io.py +166 -25
- bec_widgets/widgets/containers/auto_update/auto_updates.py +364 -0
- bec_widgets/widgets/containers/dock/dock.py +157 -49
- bec_widgets/widgets/containers/dock/dock_area.py +188 -138
- bec_widgets/widgets/containers/layout_manager/layout_manager.py +2 -1
- bec_widgets/widgets/containers/main_window/addons/web_links.py +15 -0
- bec_widgets/widgets/containers/main_window/main_window.py +189 -41
- bec_widgets/widgets/control/buttons/button_abort/button_abort.py +3 -4
- bec_widgets/widgets/control/buttons/button_reset/button_reset.py +3 -4
- bec_widgets/widgets/control/buttons/button_resume/button_resume.py +3 -3
- bec_widgets/widgets/control/buttons/stop_button/stop_button.py +18 -7
- bec_widgets/widgets/control/device_control/position_indicator/position_indicator.py +22 -3
- bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +31 -13
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +3 -1
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.ui +27 -4
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +5 -2
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +97 -31
- bec_widgets/widgets/control/device_control/positioner_box/positioner_control_line/positioner_control_line.ui +11 -4
- bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +2 -3
- bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +29 -4
- bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +1 -0
- bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +2 -2
- bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +2 -2
- bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py +1 -2
- bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.py +1 -2
- bec_widgets/widgets/control/scan_control/scan_control.py +7 -5
- bec_widgets/widgets/control/scan_control/scan_group_box.py +28 -5
- bec_widgets/widgets/dap/dap_combo_box/dap_combo_box.py +1 -2
- bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog.py +3 -4
- bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog_vertical.ui +14 -8
- bec_widgets/widgets/editors/console/console.py +1 -1
- bec_widgets/widgets/editors/{scan_metadata/additional_metadata_table.py → dict_backed_table.py} +29 -6
- bec_widgets/widgets/editors/scan_metadata/__init__.py +0 -7
- bec_widgets/widgets/editors/scan_metadata/_util.py +1 -1
- bec_widgets/widgets/{plots/motor_map/register_bec_motor_map_widget.py → editors/scan_metadata/register_scan_metadata.py} +2 -4
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +42 -136
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.pyproject +1 -0
- bec_widgets/widgets/{plots/multi_waveform/bec_multi_waveform_widget_plugin.py → editors/scan_metadata/scan_metadata_plugin.py} +9 -9
- bec_widgets/widgets/editors/text_box/text_box.py +2 -3
- bec_widgets/widgets/editors/website/website.py +2 -2
- bec_widgets/widgets/games/minesweeper.py +3 -2
- bec_widgets/widgets/plots/image/image.py +960 -0
- bec_widgets/widgets/plots/image/image.pyproject +1 -0
- bec_widgets/widgets/plots/image/image_item.py +279 -0
- bec_widgets/widgets/plots/{motor_map/bec_motor_map_widget_plugin.py → image/image_plugin.py} +11 -13
- bec_widgets/widgets/{containers/figure/plots → plots}/image/image_processor.py +31 -64
- bec_widgets/widgets/plots/image/{register_bec_image_widget.py → register_image.py} +2 -2
- bec_widgets/widgets/plots/image/toolbar_bundles/image_selection.py +59 -0
- bec_widgets/widgets/plots/image/toolbar_bundles/processing.py +79 -0
- bec_widgets/widgets/plots/motor_map/motor_map.py +832 -0
- bec_widgets/widgets/plots/motor_map/motor_map.pyproject +1 -0
- bec_widgets/widgets/plots/motor_map/motor_map_plugin.py +54 -0
- bec_widgets/widgets/plots/{multi_waveform/register_bec_multi_waveform_widget.py → motor_map/register_motor_map.py} +2 -4
- bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py +129 -0
- bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui +120 -0
- bec_widgets/widgets/plots/motor_map/toolbar_bundles/motor_selection.py +70 -0
- bec_widgets/widgets/plots/multi_waveform/multi_waveform.py +508 -0
- bec_widgets/widgets/plots/multi_waveform/multi_waveform.pyproject +1 -0
- bec_widgets/widgets/plots/multi_waveform/multi_waveform_plugin.py +54 -0
- bec_widgets/widgets/plots/multi_waveform/register_multi_waveform.py +15 -0
- bec_widgets/widgets/plots/multi_waveform/settings/control_panel.py +144 -0
- bec_widgets/widgets/plots/multi_waveform/settings/multi_waveform_controls.ui +164 -0
- bec_widgets/widgets/plots/multi_waveform/toolbar_bundles/monitor_selection.py +65 -0
- bec_widgets/widgets/{plots_next_gen → plots}/plot_base.py +321 -40
- bec_widgets/widgets/plots/{waveform/register_bec_waveform_widget.py → scatter_waveform/register_scatter_waveform.py} +3 -3
- bec_widgets/widgets/plots/scatter_waveform/scatter_curve.py +197 -0
- bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py +553 -0
- bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.pyproject +1 -0
- bec_widgets/widgets/plots/{image/bec_image_widget_plugin.py → scatter_waveform/scatter_waveform_plugin.py} +9 -13
- bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_setting.py +138 -0
- bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_horizontal.ui +195 -0
- bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_vertical.ui +204 -0
- bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings.py +8 -8
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/mouse_interactions.py +4 -18
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/plot_export.py +14 -3
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/roi_bundle.py +6 -1
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/save_state.py +2 -2
- bec_widgets/widgets/{containers/figure/plots/waveform/waveform_curve.py → plots/waveform/curve.py} +119 -49
- bec_widgets/widgets/plots/waveform/register_waveform.py +15 -0
- bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_setting.py +125 -0
- bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +576 -0
- bec_widgets/widgets/plots/waveform/utils/__init__.py +0 -0
- bec_widgets/widgets/plots/waveform/utils/roi_manager.py +84 -0
- bec_widgets/widgets/plots/waveform/waveform.py +1794 -0
- bec_widgets/widgets/plots/waveform/waveform.pyproject +1 -0
- bec_widgets/widgets/plots/waveform/{bec_waveform_widget_plugin.py → waveform_plugin.py} +9 -13
- bec_widgets/widgets/progress/bec_progressbar/bec_progressbar.py +1 -2
- bec_widgets/widgets/progress/ring_progress_bar/ring.py +11 -10
- bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +24 -14
- bec_widgets/widgets/services/bec_queue/bec_queue.py +13 -11
- bec_widgets/widgets/services/bec_status_box/bec_status_box.py +3 -4
- bec_widgets/widgets/services/device_browser/device_browser.py +5 -2
- bec_widgets/widgets/services/device_browser/device_item/device_item.py +1 -1
- bec_widgets/widgets/utility/logpanel/logpanel.py +36 -17
- bec_widgets/widgets/utility/spinbox/decimal_spinbox.py +3 -3
- bec_widgets/widgets/utility/visual/color_button/color_button.py +1 -1
- bec_widgets/widgets/utility/visual/colormap_widget/colormap_widget.py +4 -6
- bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py +4 -8
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/METADATA +3 -3
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/RECORD +168 -153
- pyproject.toml +3 -3
- bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +0 -198
- bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +0 -615
- bec_widgets/applications/bec_app.py +0 -84
- bec_widgets/cli/auto_updates.py +0 -168
- bec_widgets/widgets/containers/figure/__init__.py +0 -1
- bec_widgets/widgets/containers/figure/figure.py +0 -796
- bec_widgets/widgets/containers/figure/plots/axis_settings.py +0 -91
- bec_widgets/widgets/containers/figure/plots/axis_settings.ui +0 -256
- bec_widgets/widgets/containers/figure/plots/image/image.py +0 -772
- bec_widgets/widgets/containers/figure/plots/image/image_item.py +0 -337
- bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py +0 -525
- bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py +0 -340
- bec_widgets/widgets/containers/figure/plots/plot_base.py +0 -505
- bec_widgets/widgets/containers/figure/plots/waveform/waveform.py +0 -1563
- bec_widgets/widgets/plots/image/bec_image_widget.pyproject +0 -1
- bec_widgets/widgets/plots/image/image_widget.py +0 -515
- bec_widgets/widgets/plots/motor_map/bec_motor_map_widget.pyproject +0 -1
- bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.py +0 -56
- bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.ui +0 -108
- bec_widgets/widgets/plots/motor_map/motor_map_widget.py +0 -234
- bec_widgets/widgets/plots/multi_waveform/bec_multi_waveform_widget.pyproject +0 -1
- bec_widgets/widgets/plots/multi_waveform/multi_waveform_controls.ui +0 -99
- bec_widgets/widgets/plots/multi_waveform/multi_waveform_widget.py +0 -536
- bec_widgets/widgets/plots/waveform/bec_waveform_widget.pyproject +0 -1
- bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.py +0 -336
- bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.ui +0 -372
- bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py +0 -25
- bec_widgets/widgets/plots/waveform/waveform_widget.py +0 -751
- /bec_widgets/{qt_utils → utils}/collapsible_panel_manager.py +0 -0
- /bec_widgets/{applications/alignment → utils/forms_from_types}/__init__.py +0 -0
- /bec_widgets/{qt_utils → utils}/redis_message_waiter.py +0 -0
- /bec_widgets/{applications/alignment/alignment_1d → widgets/containers/auto_update}/__init__.py +0 -0
- /bec_widgets/{qt_utils → widgets/containers/main_window/addons}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots → plots/image/toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/image → plots/motor_map/settings}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/motor_map → plots/motor_map/toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/multi_waveform → plots/multi_waveform/settings}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/waveform → plots/multi_waveform/toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/plots/{motor_map/motor_map_dialog → scatter_waveform}/__init__.py +0 -0
- /bec_widgets/widgets/plots/{waveform/waveform_popups → scatter_waveform/settings}/__init__.py +0 -0
- /bec_widgets/widgets/plots/{waveform/waveform_popups/curve_dialog → setting_menus}/__init__.py +0 -0
- /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_horizontal.ui +0 -0
- /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_vertical.ui +0 -0
- /bec_widgets/widgets/plots/{waveform/waveform_popups/dap_summary_dialog → toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/{plots_next_gen/setting_menus → plots/waveform/settings}/__init__.py +0 -0
- /bec_widgets/widgets/{plots_next_gen/toolbar_bundles → plots/waveform/settings/curve_settings}/__init__.py +0 -0
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/WHEEL +0 -0
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.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
|
bec_widgets/{widgets/editors/scan_metadata/_metadata_widgets.py → utils/forms_from_types/items.py}
RENAMED
@@ -2,11 +2,13 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from abc import abstractmethod
|
4
4
|
from decimal import Decimal
|
5
|
-
from
|
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
|
86
|
-
|
97
|
+
class DynamicFormItem(QWidget):
|
87
98
|
valueChanged = Signal()
|
88
99
|
|
89
|
-
def __init__(self,
|
100
|
+
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
90
101
|
super().__init__(parent)
|
91
|
-
self.
|
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.
|
96
|
-
self._desc = self.
|
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(
|
131
|
-
def __init__(self,
|
132
|
-
super().__init__(
|
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.
|
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(
|
160
|
-
def __init__(self,
|
161
|
-
super().__init__(
|
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.
|
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(
|
189
|
-
def __init__(self,
|
190
|
-
super().__init__(
|
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.
|
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.
|
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(
|
223
|
-
def __init__(self,
|
224
|
-
super().__init__(
|
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.
|
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) ->
|
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 =
|
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.
|
117
|
-
self.
|
118
|
-
self.
|
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__(
|
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
|
-
|
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(
|
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(
|
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
|
-
|
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
|
-
|
37
|
-
|
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
|
56
|
-
|
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(
|
133
|
-
rounded_plot2 = RoundedFrame(
|
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)
|