bec-widgets 1.25.0__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.
- .gitlab-ci.yml +11 -6
- CHANGELOG.md +650 -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 +186 -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 +37 -18
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +28 -4
- 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/spinner/spinner.py +2 -2
- 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.0.dist-info → bec_widgets-2.0.0.dist-info}/METADATA +3 -3
- {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/RECORD +169 -154
- 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.0.dist-info → bec_widgets-2.0.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,36 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from
|
3
|
+
from functools import wraps
|
4
|
+
from threading import RLock
|
5
|
+
from typing import TYPE_CHECKING, Callable
|
4
6
|
from weakref import WeakValueDictionary
|
5
7
|
|
8
|
+
from bec_lib.logger import bec_logger
|
6
9
|
from qtpy.QtCore import QObject
|
7
10
|
|
11
|
+
if TYPE_CHECKING: # pragma: no cover
|
12
|
+
from bec_widgets.utils.bec_connector import BECConnector
|
13
|
+
from bec_widgets.utils.bec_widget import BECWidget
|
14
|
+
from bec_widgets.widgets.containers.dock.dock import BECDock
|
15
|
+
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
16
|
+
|
17
|
+
logger = bec_logger.logger
|
18
|
+
|
19
|
+
|
20
|
+
def broadcast_update(func):
|
21
|
+
"""
|
22
|
+
Decorator to broadcast updates to the RPCRegister whenever a new RPC object is added or removed.
|
23
|
+
If class attribute _skip_broadcast is set to True, the broadcast will be skipped
|
24
|
+
"""
|
25
|
+
|
26
|
+
@wraps(func)
|
27
|
+
def wrapper(self, *args, **kwargs):
|
28
|
+
result = func(self, *args, **kwargs)
|
29
|
+
self.broadcast()
|
30
|
+
return result
|
31
|
+
|
32
|
+
return wrapper
|
33
|
+
|
8
34
|
|
9
35
|
class RPCRegister:
|
10
36
|
"""
|
@@ -13,7 +39,6 @@ class RPCRegister:
|
|
13
39
|
|
14
40
|
_instance = None
|
15
41
|
_initialized = False
|
16
|
-
_lock = Lock()
|
17
42
|
|
18
43
|
def __new__(cls, *args, **kwargs):
|
19
44
|
if cls._instance is None:
|
@@ -25,9 +50,22 @@ class RPCRegister:
|
|
25
50
|
if self._initialized:
|
26
51
|
return
|
27
52
|
self._rpc_register = WeakValueDictionary()
|
53
|
+
self._broadcast_on_hold = RPCRegisterBroadcast(self)
|
54
|
+
self._lock = RLock()
|
55
|
+
self._skip_broadcast = False
|
28
56
|
self._initialized = True
|
57
|
+
self.callbacks = []
|
58
|
+
|
59
|
+
@classmethod
|
60
|
+
def delayed_broadcast(cls):
|
61
|
+
"""
|
62
|
+
Delay the broadcast of the update to all the callbacks.
|
63
|
+
"""
|
64
|
+
register = cls()
|
65
|
+
return register._broadcast_on_hold
|
29
66
|
|
30
|
-
|
67
|
+
@broadcast_update
|
68
|
+
def add_rpc(self, rpc: BECConnector):
|
31
69
|
"""
|
32
70
|
Add an RPC object to the register.
|
33
71
|
|
@@ -38,7 +76,8 @@ class RPCRegister:
|
|
38
76
|
raise ValueError("RPC object must have a 'gui_id' attribute.")
|
39
77
|
self._rpc_register[rpc.gui_id] = rpc
|
40
78
|
|
41
|
-
|
79
|
+
@broadcast_update
|
80
|
+
def remove_rpc(self, rpc: BECConnector):
|
42
81
|
"""
|
43
82
|
Remove an RPC object from the register.
|
44
83
|
|
@@ -49,7 +88,7 @@ class RPCRegister:
|
|
49
88
|
raise ValueError(f"RPC object {rpc} must have a 'gui_id' attribute.")
|
50
89
|
self._rpc_register.pop(rpc.gui_id, None)
|
51
90
|
|
52
|
-
def get_rpc_by_id(self, gui_id: str) -> QObject:
|
91
|
+
def get_rpc_by_id(self, gui_id: str) -> QObject | None:
|
53
92
|
"""
|
54
93
|
Get an RPC object by its ID.
|
55
94
|
|
@@ -57,7 +96,7 @@ class RPCRegister:
|
|
57
96
|
gui_id(str): The ID of the RPC object to be retrieved.
|
58
97
|
|
59
98
|
Returns:
|
60
|
-
QObject: The RPC object with the given ID
|
99
|
+
QObject | None: The RPC object with the given ID or None
|
61
100
|
"""
|
62
101
|
rpc_object = self._rpc_register.get(gui_id, None)
|
63
102
|
return rpc_object
|
@@ -73,6 +112,52 @@ class RPCRegister:
|
|
73
112
|
connections = dict(self._rpc_register)
|
74
113
|
return connections
|
75
114
|
|
115
|
+
def get_names_of_rpc_by_class_type(
|
116
|
+
self, cls: type[BECWidget] | type[BECConnector] | type[BECDock] | type[BECDockArea]
|
117
|
+
) -> list[str]:
|
118
|
+
"""Get all the names of the widgets.
|
119
|
+
|
120
|
+
Args:
|
121
|
+
cls(BECWidget | BECConnector): The class of the RPC object to be retrieved.
|
122
|
+
"""
|
123
|
+
# This retrieves any rpc objects that are subclass of BECWidget,
|
124
|
+
# i.e. curve and image items are excluded
|
125
|
+
widgets = [rpc for rpc in self._rpc_register.values() if isinstance(rpc, cls)]
|
126
|
+
return [widget.object_name for widget in widgets]
|
127
|
+
|
128
|
+
def broadcast(self):
|
129
|
+
"""
|
130
|
+
Broadcast the update to all the callbacks.
|
131
|
+
"""
|
132
|
+
|
133
|
+
if self._skip_broadcast:
|
134
|
+
return
|
135
|
+
connections = self.list_all_connections()
|
136
|
+
for callback in self.callbacks:
|
137
|
+
callback(connections)
|
138
|
+
|
139
|
+
def object_is_registered(self, obj: BECConnector) -> bool:
|
140
|
+
"""
|
141
|
+
Check if an object is registered in the RPC register.
|
142
|
+
|
143
|
+
Args:
|
144
|
+
obj(QObject): The object to check.
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
bool: True if the object is registered, False otherwise.
|
148
|
+
"""
|
149
|
+
return obj.gui_id in self._rpc_register
|
150
|
+
|
151
|
+
def add_callback(self, callback: Callable[[dict], None]):
|
152
|
+
"""
|
153
|
+
Add a callback that will be called whenever the registry is updated.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
callback(Callable[[dict], None]): The callback to be added. It should accept a dictionary of all the
|
157
|
+
registered RPC objects as an argument.
|
158
|
+
"""
|
159
|
+
self.callbacks.append(callback)
|
160
|
+
|
76
161
|
@classmethod
|
77
162
|
def reset_singleton(cls):
|
78
163
|
"""
|
@@ -80,3 +165,25 @@ class RPCRegister:
|
|
80
165
|
"""
|
81
166
|
cls._instance = None
|
82
167
|
cls._initialized = False
|
168
|
+
|
169
|
+
|
170
|
+
class RPCRegisterBroadcast:
|
171
|
+
"""Context manager for RPCRegister broadcast."""
|
172
|
+
|
173
|
+
def __init__(self, rpc_register: RPCRegister) -> None:
|
174
|
+
self.rpc_register = rpc_register
|
175
|
+
self._call_depth = 0
|
176
|
+
|
177
|
+
def __enter__(self):
|
178
|
+
"""Enter the context manager"""
|
179
|
+
self._call_depth += 1 # Needed for nested calls
|
180
|
+
self.rpc_register._skip_broadcast = True
|
181
|
+
return self.rpc_register
|
182
|
+
|
183
|
+
def __exit__(self, *exc):
|
184
|
+
"""Exit the context manager"""
|
185
|
+
|
186
|
+
self._call_depth -= 1 # Remove nested calls
|
187
|
+
if self._call_depth == 0: # The Last one to exit is responsible for broadcasting
|
188
|
+
self.rpc_register._skip_broadcast = False
|
189
|
+
self.rpc_register.broadcast()
|
@@ -1,6 +1,9 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from bec_widgets.
|
3
|
+
from bec_widgets.cli.client_utils import IGNORE_WIDGETS
|
4
|
+
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets
|
5
|
+
from bec_widgets.utils.bec_widget import BECWidget
|
6
|
+
from bec_widgets.utils.plugin_utils import get_custom_classes
|
4
7
|
|
5
8
|
|
6
9
|
class RPCWidgetHandler:
|
@@ -10,7 +13,7 @@ class RPCWidgetHandler:
|
|
10
13
|
self._widget_classes = None
|
11
14
|
|
12
15
|
@property
|
13
|
-
def widget_classes(self):
|
16
|
+
def widget_classes(self) -> dict[str, type[BECWidget]]:
|
14
17
|
"""
|
15
18
|
Get the available widget classes.
|
16
19
|
|
@@ -19,7 +22,7 @@ class RPCWidgetHandler:
|
|
19
22
|
"""
|
20
23
|
if self._widget_classes is None:
|
21
24
|
self.update_available_widgets()
|
22
|
-
return self._widget_classes
|
25
|
+
return self._widget_classes # type: ignore
|
23
26
|
|
24
27
|
def update_available_widgets(self):
|
25
28
|
"""
|
@@ -28,25 +31,24 @@ class RPCWidgetHandler:
|
|
28
31
|
Returns:
|
29
32
|
None
|
30
33
|
"""
|
31
|
-
from bec_widgets.utils.plugin_utils import get_custom_classes
|
32
|
-
|
33
34
|
clss = get_custom_classes("bec_widgets")
|
34
|
-
self._widget_classes =
|
35
|
+
self._widget_classes = get_all_plugin_widgets() | {
|
36
|
+
cls.__name__: cls for cls in clss.widgets if cls.__name__ not in IGNORE_WIDGETS
|
37
|
+
}
|
35
38
|
|
36
|
-
def create_widget(self, widget_type, **kwargs) ->
|
39
|
+
def create_widget(self, widget_type, **kwargs) -> BECWidget:
|
37
40
|
"""
|
38
41
|
Create a widget from an RPC message.
|
39
42
|
|
40
43
|
Args:
|
41
44
|
widget_type(str): The type of the widget.
|
45
|
+
name (str): The name of the widget.
|
42
46
|
**kwargs: The keyword arguments for the widget.
|
43
47
|
|
44
48
|
Returns:
|
45
|
-
widget(
|
49
|
+
widget(BECWidget): The created widget.
|
46
50
|
"""
|
47
|
-
|
48
|
-
self.update_available_widgets()
|
49
|
-
widget_class = self._widget_classes.get(widget_type)
|
51
|
+
widget_class = self.widget_classes.get(widget_type) # type: ignore
|
50
52
|
if widget_class:
|
51
53
|
return widget_class(**kwargs)
|
52
54
|
raise ValueError(f"Unknown widget type: {widget_type}")
|
bec_widgets/cli/server.py
CHANGED
@@ -1,164 +1,27 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import
|
3
|
+
import argparse
|
4
4
|
import json
|
5
|
+
import os
|
5
6
|
import signal
|
6
7
|
import sys
|
7
|
-
import
|
8
|
-
from
|
9
|
-
from typing import Union
|
8
|
+
from contextlib import redirect_stderr, redirect_stdout
|
9
|
+
from typing import cast
|
10
10
|
|
11
|
-
from bec_lib.endpoints import MessageEndpoints
|
12
11
|
from bec_lib.logger import bec_logger
|
13
12
|
from bec_lib.service_config import ServiceConfig
|
14
|
-
from
|
15
|
-
from qtpy.
|
16
|
-
from
|
13
|
+
from qtpy.QtCore import QSize, Qt
|
14
|
+
from qtpy.QtGui import QIcon
|
15
|
+
from qtpy.QtWidgets import QApplication
|
17
16
|
|
17
|
+
import bec_widgets
|
18
|
+
from bec_widgets.applications.launch_window import LaunchWindow
|
18
19
|
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
19
|
-
from bec_widgets.
|
20
|
-
from bec_widgets.utils import BECDispatcher
|
21
|
-
from bec_widgets.utils.bec_connector import BECConnector
|
22
|
-
from bec_widgets.widgets.containers.dock import BECDockArea
|
23
|
-
from bec_widgets.widgets.containers.figure import BECFigure
|
24
|
-
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
25
|
-
|
26
|
-
messages = lazy_import("bec_lib.messages")
|
27
|
-
logger = bec_logger.logger
|
28
|
-
|
29
|
-
|
30
|
-
@contextmanager
|
31
|
-
def rpc_exception_hook(err_func):
|
32
|
-
"""This context replaces the popup message box for error display with a specific hook"""
|
33
|
-
# get error popup utility singleton
|
34
|
-
popup = ErrorPopupUtility()
|
35
|
-
# save current setting
|
36
|
-
old_exception_hook = popup.custom_exception_hook
|
37
|
-
|
38
|
-
# install err_func, if it is a callable
|
39
|
-
def custom_exception_hook(self, exc_type, value, tb, **kwargs):
|
40
|
-
err_func({"error": popup.get_error_message(exc_type, value, tb)})
|
41
|
-
|
42
|
-
popup.custom_exception_hook = types.MethodType(custom_exception_hook, popup)
|
43
|
-
|
44
|
-
try:
|
45
|
-
yield popup
|
46
|
-
finally:
|
47
|
-
# restore state of error popup utility singleton
|
48
|
-
popup.custom_exception_hook = old_exception_hook
|
49
|
-
|
50
|
-
|
51
|
-
class BECWidgetsCLIServer:
|
52
|
-
|
53
|
-
def __init__(
|
54
|
-
self,
|
55
|
-
gui_id: str,
|
56
|
-
dispatcher: BECDispatcher = None,
|
57
|
-
client=None,
|
58
|
-
config=None,
|
59
|
-
gui_class: Union[BECFigure, BECDockArea] = BECFigure,
|
60
|
-
) -> None:
|
61
|
-
self.status = messages.BECStatus.BUSY
|
62
|
-
self.dispatcher = BECDispatcher(config=config) if dispatcher is None else dispatcher
|
63
|
-
self.client = self.dispatcher.client if client is None else client
|
64
|
-
self.client.start()
|
65
|
-
self.gui_id = gui_id
|
66
|
-
self.gui = gui_class(gui_id=self.gui_id)
|
67
|
-
self.rpc_register = RPCRegister()
|
68
|
-
self.rpc_register.add_rpc(self.gui)
|
69
|
-
|
70
|
-
self.dispatcher.connect_slot(
|
71
|
-
self.on_rpc_update, MessageEndpoints.gui_instructions(self.gui_id)
|
72
|
-
)
|
73
|
-
|
74
|
-
# Setup QTimer for heartbeat
|
75
|
-
self._heartbeat_timer = QTimer()
|
76
|
-
self._heartbeat_timer.timeout.connect(self.emit_heartbeat)
|
77
|
-
self._heartbeat_timer.start(200)
|
78
|
-
|
79
|
-
self.status = messages.BECStatus.RUNNING
|
80
|
-
logger.success(f"Server started with gui_id: {self.gui_id}")
|
20
|
+
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
81
21
|
|
82
|
-
|
83
|
-
request_id = metadata.get("request_id")
|
84
|
-
logger.debug(f"Received RPC instruction: {msg}, metadata: {metadata}")
|
85
|
-
with rpc_exception_hook(functools.partial(self.send_response, request_id, False)):
|
86
|
-
try:
|
87
|
-
obj = self.get_object_from_config(msg["parameter"])
|
88
|
-
method = msg["action"]
|
89
|
-
args = msg["parameter"].get("args", [])
|
90
|
-
kwargs = msg["parameter"].get("kwargs", {})
|
91
|
-
res = self.run_rpc(obj, method, args, kwargs)
|
92
|
-
except Exception as e:
|
93
|
-
logger.error(f"Error while executing RPC instruction: {e}")
|
94
|
-
self.send_response(request_id, False, {"error": str(e)})
|
95
|
-
else:
|
96
|
-
logger.debug(f"RPC instruction executed successfully: {res}")
|
97
|
-
self.send_response(request_id, True, {"result": res})
|
98
|
-
|
99
|
-
def send_response(self, request_id: str, accepted: bool, msg: dict):
|
100
|
-
self.client.connector.set_and_publish(
|
101
|
-
MessageEndpoints.gui_instruction_response(request_id),
|
102
|
-
messages.RequestResponseMessage(accepted=accepted, message=msg),
|
103
|
-
expire=60,
|
104
|
-
)
|
105
|
-
|
106
|
-
def get_object_from_config(self, config: dict):
|
107
|
-
gui_id = config.get("gui_id")
|
108
|
-
obj = self.rpc_register.get_rpc_by_id(gui_id)
|
109
|
-
if obj is None:
|
110
|
-
raise ValueError(f"Object with gui_id {gui_id} not found")
|
111
|
-
return obj
|
112
|
-
|
113
|
-
def run_rpc(self, obj, method, args, kwargs):
|
114
|
-
logger.debug(f"Running RPC instruction: {method} with args: {args}, kwargs: {kwargs}")
|
115
|
-
method_obj = getattr(obj, method)
|
116
|
-
# check if the method accepts args and kwargs
|
117
|
-
if not callable(method_obj):
|
118
|
-
if not args:
|
119
|
-
res = method_obj
|
120
|
-
else:
|
121
|
-
setattr(obj, method, args[0])
|
122
|
-
res = None
|
123
|
-
else:
|
124
|
-
res = method_obj(*args, **kwargs)
|
22
|
+
logger = bec_logger.logger
|
125
23
|
|
126
|
-
|
127
|
-
res = [self.serialize_object(obj) for obj in res]
|
128
|
-
elif isinstance(res, dict):
|
129
|
-
res = {key: self.serialize_object(val) for key, val in res.items()}
|
130
|
-
else:
|
131
|
-
res = self.serialize_object(res)
|
132
|
-
return res
|
133
|
-
|
134
|
-
def serialize_object(self, obj):
|
135
|
-
if isinstance(obj, BECConnector):
|
136
|
-
return {
|
137
|
-
"gui_id": obj.gui_id,
|
138
|
-
"widget_class": obj.__class__.__name__,
|
139
|
-
"config": obj.config.model_dump(),
|
140
|
-
"__rpc__": True,
|
141
|
-
}
|
142
|
-
return obj
|
143
|
-
|
144
|
-
def emit_heartbeat(self):
|
145
|
-
logger.trace(f"Emitting heartbeat for {self.gui_id}")
|
146
|
-
try:
|
147
|
-
self.client.connector.set(
|
148
|
-
MessageEndpoints.gui_heartbeat(self.gui_id),
|
149
|
-
messages.StatusMessage(name=self.gui_id, status=self.status, info={}),
|
150
|
-
expire=10,
|
151
|
-
)
|
152
|
-
except RedisError as exc:
|
153
|
-
logger.error(f"Error while emitting heartbeat: {exc}")
|
154
|
-
|
155
|
-
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
|
156
|
-
logger.info(f"Shutting down server with gui_id: {self.gui_id}")
|
157
|
-
self.status = messages.BECStatus.IDLE
|
158
|
-
self._heartbeat_timer.stop()
|
159
|
-
self.emit_heartbeat()
|
160
|
-
self.gui.close()
|
161
|
-
self.client.shutdown()
|
24
|
+
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
162
25
|
|
163
26
|
|
164
27
|
class SimpleFileLikeFromLogOutputFunc:
|
@@ -179,36 +42,112 @@ class SimpleFileLikeFromLogOutputFunc:
|
|
179
42
|
return
|
180
43
|
|
181
44
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
45
|
+
class GUIServer:
|
46
|
+
"""
|
47
|
+
This class is used to start the BEC GUI and is the main entry point for launching BEC Widgets in a subprocess.
|
48
|
+
"""
|
49
|
+
|
50
|
+
def __init__(self, args):
|
51
|
+
self.config = args.config
|
52
|
+
self.gui_id = args.id
|
53
|
+
self.gui_class = args.gui_class
|
54
|
+
self.gui_class_id = args.gui_class_id
|
55
|
+
self.hide = args.hide
|
56
|
+
self.app: QApplication | None = None
|
57
|
+
self.launcher_window: LaunchWindow | None = None
|
58
|
+
self.dispatcher: BECDispatcher | None = None
|
59
|
+
|
60
|
+
def start(self):
|
61
|
+
"""
|
62
|
+
Start the GUI server.
|
63
|
+
"""
|
64
|
+
bec_logger.level = bec_logger.LOGLEVEL.INFO
|
65
|
+
if self.hide:
|
66
|
+
# pylint: disable=protected-access
|
67
|
+
bec_logger._stderr_log_level = bec_logger.LOGLEVEL.ERROR
|
68
|
+
bec_logger._update_sinks()
|
69
|
+
|
70
|
+
with redirect_stdout(SimpleFileLikeFromLogOutputFunc(logger.info)): # type: ignore
|
71
|
+
with redirect_stderr(SimpleFileLikeFromLogOutputFunc(logger.error)): # type: ignore
|
72
|
+
self._run()
|
73
|
+
|
74
|
+
def _get_service_config(self) -> ServiceConfig:
|
75
|
+
if self.config:
|
76
|
+
try:
|
77
|
+
config = json.loads(self.config)
|
78
|
+
service_config = ServiceConfig(config=config)
|
79
|
+
except (json.JSONDecodeError, TypeError):
|
80
|
+
service_config = ServiceConfig(config_path=config)
|
81
|
+
else:
|
82
|
+
# if no config is provided, use the default config
|
83
|
+
service_config = ServiceConfig()
|
84
|
+
return service_config
|
85
|
+
|
86
|
+
def _run(self):
|
87
|
+
"""
|
88
|
+
Run the GUI server.
|
89
|
+
"""
|
90
|
+
self.app = QApplication(sys.argv)
|
91
|
+
self.app.setApplicationName("BEC")
|
92
|
+
self.app.gui_id = self.gui_id # type: ignore
|
93
|
+
self.setup_bec_icon()
|
94
|
+
|
95
|
+
service_config = self._get_service_config()
|
96
|
+
self.dispatcher = BECDispatcher(config=service_config, gui_id=self.gui_id)
|
97
|
+
# self.dispatcher.start_cli_server(gui_id=self.gui_id)
|
98
|
+
|
99
|
+
self.launcher_window = LaunchWindow(gui_id=f"{self.gui_id}:launcher")
|
100
|
+
self.launcher_window.setAttribute(Qt.WA_ShowWithoutActivating) # type: ignore
|
101
|
+
|
102
|
+
self.app.aboutToQuit.connect(self.shutdown)
|
103
|
+
self.app.setQuitOnLastWindowClosed(False)
|
104
|
+
|
105
|
+
if self.gui_class:
|
106
|
+
# If the server is started with a specific gui class, we launch it.
|
107
|
+
# This will automatically hide the launcher.
|
108
|
+
self.launcher_window.launch(self.gui_class, name=self.gui_class_id)
|
109
|
+
|
110
|
+
def sigint_handler(*args):
|
111
|
+
# display message, for people to let it terminate gracefully
|
112
|
+
print("Caught SIGINT, exiting")
|
113
|
+
# Widgets should be all closed.
|
114
|
+
with RPCRegister.delayed_broadcast():
|
115
|
+
for widget in QApplication.instance().topLevelWidgets(): # type: ignore
|
116
|
+
widget.close()
|
117
|
+
if self.app:
|
118
|
+
self.app.quit()
|
119
|
+
|
120
|
+
signal.signal(signal.SIGINT, sigint_handler)
|
121
|
+
signal.signal(signal.SIGTERM, sigint_handler)
|
122
|
+
|
123
|
+
sys.exit(self.app.exec())
|
124
|
+
|
125
|
+
def setup_bec_icon(self):
|
126
|
+
"""
|
127
|
+
Set the BEC icon for the application
|
128
|
+
"""
|
129
|
+
if self.app is None:
|
130
|
+
return
|
131
|
+
icon = QIcon()
|
132
|
+
icon.addFile(
|
133
|
+
os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
|
134
|
+
size=QSize(48, 48),
|
135
|
+
)
|
136
|
+
self.app.setWindowIcon(icon)
|
192
137
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
return server
|
138
|
+
def shutdown(self):
|
139
|
+
"""
|
140
|
+
Shutdown the GUI server.
|
141
|
+
"""
|
142
|
+
if self.dispatcher:
|
143
|
+
self.dispatcher.stop_cli_server()
|
144
|
+
self.dispatcher.disconnect_all()
|
201
145
|
|
202
146
|
|
203
147
|
def main():
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
from qtpy.QtCore import QSize
|
208
|
-
from qtpy.QtGui import QIcon
|
209
|
-
from qtpy.QtWidgets import QApplication
|
210
|
-
|
211
|
-
import bec_widgets
|
148
|
+
"""
|
149
|
+
Main entry point for subprocesses that start a GUI server.
|
150
|
+
"""
|
212
151
|
|
213
152
|
parser = argparse.ArgumentParser(description="BEC Widgets CLI Server")
|
214
153
|
parser.add_argument("--id", type=str, default="test", help="The id of the server")
|
@@ -217,76 +156,23 @@ def main():
|
|
217
156
|
type=str,
|
218
157
|
help="Name of the gui class to be rendered. Possible values: \n- BECFigure\n- BECDockArea",
|
219
158
|
)
|
159
|
+
parser.add_argument(
|
160
|
+
"--gui_class_id",
|
161
|
+
type=str,
|
162
|
+
default="bec",
|
163
|
+
help="The id of the gui class that is added to the QApplication",
|
164
|
+
)
|
220
165
|
parser.add_argument("--config", type=str, help="Config file or config string.")
|
221
166
|
parser.add_argument("--hide", action="store_true", help="Hide on startup")
|
222
167
|
|
223
168
|
args = parser.parse_args()
|
224
169
|
|
225
|
-
|
226
|
-
|
227
|
-
# pylint: disable=protected-access
|
228
|
-
bec_logger._stderr_log_level = bec_logger.LOGLEVEL.ERROR
|
229
|
-
bec_logger._update_sinks()
|
230
|
-
|
231
|
-
if args.gui_class == "BECDockArea":
|
232
|
-
gui_class = BECDockArea
|
233
|
-
elif args.gui_class == "BECFigure":
|
234
|
-
gui_class = BECFigure
|
235
|
-
else:
|
236
|
-
print(
|
237
|
-
"Please specify a valid gui_class to run. Use -h for help."
|
238
|
-
"\n Starting with default gui_class BECFigure."
|
239
|
-
)
|
240
|
-
gui_class = BECDockArea
|
241
|
-
|
242
|
-
with redirect_stdout(SimpleFileLikeFromLogOutputFunc(logger.info)):
|
243
|
-
with redirect_stderr(SimpleFileLikeFromLogOutputFunc(logger.error)):
|
244
|
-
app = QApplication(sys.argv)
|
245
|
-
# set close on last window, only if not under control of client ;
|
246
|
-
# indeed, Qt considers a hidden window a closed window, so if all windows
|
247
|
-
# are hidden by default it exits
|
248
|
-
app.setQuitOnLastWindowClosed(not args.hide)
|
249
|
-
module_path = os.path.dirname(bec_widgets.__file__)
|
250
|
-
icon = QIcon()
|
251
|
-
icon.addFile(
|
252
|
-
os.path.join(module_path, "assets", "app_icons", "bec_widgets_icon.png"),
|
253
|
-
size=QSize(48, 48),
|
254
|
-
)
|
255
|
-
app.setWindowIcon(icon)
|
256
|
-
# store gui id within QApplication object, to make it available to all widgets
|
257
|
-
app.gui_id = args.id
|
258
|
-
|
259
|
-
server = _start_server(args.id, gui_class, args.config)
|
260
|
-
|
261
|
-
win = BECMainWindow(gui_id=f"{server.gui_id}:window")
|
262
|
-
win.setAttribute(Qt.WA_ShowWithoutActivating)
|
263
|
-
win.setWindowTitle("BEC Widgets")
|
264
|
-
|
265
|
-
RPCRegister().add_rpc(win)
|
266
|
-
|
267
|
-
gui = server.gui
|
268
|
-
win.setCentralWidget(gui)
|
269
|
-
if not args.hide:
|
270
|
-
win.show()
|
271
|
-
|
272
|
-
app.aboutToQuit.connect(server.shutdown)
|
273
|
-
|
274
|
-
def sigint_handler(*args):
|
275
|
-
# display message, for people to let it terminate gracefully
|
276
|
-
print("Caught SIGINT, exiting")
|
277
|
-
# first hide all top level windows
|
278
|
-
# this is to discriminate the cases between "user clicks on [X]"
|
279
|
-
# (which should be filtered, to not close -see BECDockArea-)
|
280
|
-
# or "app is asked to close"
|
281
|
-
for window in app.topLevelWidgets():
|
282
|
-
window.hide() # so, we know we can exit because it is hidden
|
283
|
-
app.quit()
|
284
|
-
|
285
|
-
signal.signal(signal.SIGINT, sigint_handler)
|
286
|
-
signal.signal(signal.SIGTERM, sigint_handler)
|
287
|
-
|
288
|
-
sys.exit(app.exec())
|
170
|
+
server = GUIServer(args)
|
171
|
+
server.start()
|
289
172
|
|
290
173
|
|
291
174
|
if __name__ == "__main__":
|
175
|
+
# import sys
|
176
|
+
|
177
|
+
# sys.argv = ["bec_widgets", "--gui_class", "MainWindow"]
|
292
178
|
main()
|