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.
- .gitlab-ci.yml +3 -5
- CHANGELOG.md +631 -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 +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.0.dist-info}/METADATA +3 -3
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.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.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/licenses/LICENSE +0 -0
bec_widgets/cli/client_utils.py
CHANGED
@@ -1,38 +1,43 @@
|
|
1
|
+
"""Client utilities for the BEC GUI."""
|
2
|
+
|
1
3
|
from __future__ import annotations
|
2
4
|
|
3
|
-
import importlib
|
4
|
-
import importlib.metadata as imd
|
5
5
|
import json
|
6
6
|
import os
|
7
7
|
import select
|
8
8
|
import subprocess
|
9
9
|
import threading
|
10
|
+
import time
|
10
11
|
from contextlib import contextmanager
|
11
|
-
from
|
12
|
-
from typing import TYPE_CHECKING
|
12
|
+
from threading import Lock
|
13
|
+
from typing import TYPE_CHECKING, Literal, TypeAlias, cast
|
13
14
|
|
14
15
|
from bec_lib.endpoints import MessageEndpoints
|
15
16
|
from bec_lib.logger import bec_logger
|
16
|
-
from bec_lib.utils.import_utils import
|
17
|
+
from bec_lib.utils.import_utils import lazy_import_from
|
18
|
+
from rich.console import Console
|
19
|
+
from rich.table import Table
|
17
20
|
|
18
21
|
import bec_widgets.cli.client as client
|
19
|
-
from bec_widgets.cli.
|
20
|
-
from bec_widgets.
|
21
|
-
|
22
|
-
if TYPE_CHECKING:
|
23
|
-
from bec_lib import messages
|
24
|
-
from bec_lib.connector import MessageObject
|
25
|
-
from bec_lib.device import DeviceBase
|
22
|
+
from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCReference
|
23
|
+
from bec_widgets.utils.serialization import register_serializer_extension
|
26
24
|
|
27
|
-
|
25
|
+
if TYPE_CHECKING: # pragma: no cover
|
26
|
+
from bec_lib.messages import GUIRegistryStateMessage
|
28
27
|
else:
|
29
|
-
|
30
|
-
# from bec_lib.connector import MessageObject
|
31
|
-
MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",))
|
32
|
-
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
|
28
|
+
GUIRegistryStateMessage = lazy_import_from("bec_lib.messages", "GUIRegistryStateMessage")
|
33
29
|
|
34
30
|
logger = bec_logger.logger
|
35
31
|
|
32
|
+
IGNORE_WIDGETS = ["LaunchWindow"]
|
33
|
+
|
34
|
+
RegistryState: TypeAlias = dict[
|
35
|
+
Literal["gui_id", "name", "widget_class", "config", "__rpc__", "container_proxy"],
|
36
|
+
str | bool | dict,
|
37
|
+
]
|
38
|
+
|
39
|
+
# pylint: disable=redefined-outer-scope
|
40
|
+
|
36
41
|
|
37
42
|
def _filter_output(output: str) -> str:
|
38
43
|
"""
|
@@ -67,7 +72,13 @@ def _get_output(process, logger) -> None:
|
|
67
72
|
logger.error(f"Error reading process output: {str(e)}")
|
68
73
|
|
69
74
|
|
70
|
-
def _start_plot_process(
|
75
|
+
def _start_plot_process(
|
76
|
+
gui_id: str,
|
77
|
+
gui_class_id: str,
|
78
|
+
config: dict | str,
|
79
|
+
gui_class: str = "dock_area",
|
80
|
+
logger=None, # FIXME change gui_class back to "launcher" later
|
81
|
+
) -> tuple[subprocess.Popen[str], threading.Thread | None]:
|
71
82
|
"""
|
72
83
|
Start the plot in a new process.
|
73
84
|
|
@@ -76,7 +87,16 @@ def _start_plot_process(gui_id: str, gui_class: type, config: dict | str, logger
|
|
76
87
|
process will not be captured.
|
77
88
|
"""
|
78
89
|
# pylint: disable=subprocess-run-check
|
79
|
-
command = [
|
90
|
+
command = [
|
91
|
+
"bec-gui-server",
|
92
|
+
"--id",
|
93
|
+
gui_id,
|
94
|
+
"--gui_class",
|
95
|
+
gui_class,
|
96
|
+
"--gui_class_id",
|
97
|
+
gui_class_id,
|
98
|
+
"--hide",
|
99
|
+
]
|
80
100
|
if config:
|
81
101
|
if isinstance(config, dict):
|
82
102
|
config = json.dumps(config)
|
@@ -111,16 +131,20 @@ def _start_plot_process(gui_id: str, gui_class: type, config: dict | str, logger
|
|
111
131
|
|
112
132
|
|
113
133
|
class RepeatTimer(threading.Timer):
|
134
|
+
"""RepeatTimer class."""
|
135
|
+
|
114
136
|
def run(self):
|
115
137
|
while not self.finished.wait(self.interval):
|
116
138
|
self.function(*self.args, **self.kwargs)
|
117
139
|
|
118
140
|
|
141
|
+
# pylint: disable=protected-access
|
119
142
|
@contextmanager
|
120
|
-
def wait_for_server(client):
|
143
|
+
def wait_for_server(client: BECGuiClient):
|
144
|
+
"""Context manager to wait for the server to start."""
|
121
145
|
timeout = client._startup_timeout
|
122
146
|
if not timeout:
|
123
|
-
if client.
|
147
|
+
if client._gui_is_alive():
|
124
148
|
# there is hope, let's wait a bit
|
125
149
|
timeout = 1
|
126
150
|
else:
|
@@ -138,132 +162,223 @@ def wait_for_server(client):
|
|
138
162
|
yield
|
139
163
|
|
140
164
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
try:
|
154
|
-
del BECGuiClient._top_level[self._gui_id]
|
155
|
-
except KeyError:
|
156
|
-
# if a dock area is not at top level
|
157
|
-
pass
|
158
|
-
|
159
|
-
|
160
|
-
client.BECDockArea = BECDockArea
|
161
|
-
### ----------------------------
|
165
|
+
class WidgetNameSpace:
|
166
|
+
def __repr__(self):
|
167
|
+
console = Console()
|
168
|
+
table = Table(title="Available widgets for BEC CLI usage")
|
169
|
+
table.add_column("Widget Name", justify="left", style="magenta")
|
170
|
+
table.add_column("Description", justify="left")
|
171
|
+
for attr, value in self.__dict__.items():
|
172
|
+
docs = value.__doc__
|
173
|
+
docs = docs if docs else "No description available"
|
174
|
+
table.add_row(attr, docs)
|
175
|
+
console.print(table)
|
176
|
+
return ""
|
162
177
|
|
163
178
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
179
|
+
class AvailableWidgetsNamespace:
|
180
|
+
"""Namespace for available widgets in the BEC GUI."""
|
181
|
+
|
182
|
+
def __init__(self):
|
183
|
+
for widget in client.Widgets:
|
184
|
+
name = widget.value
|
185
|
+
if name in IGNORE_WIDGETS:
|
186
|
+
continue
|
187
|
+
setattr(self, name, name)
|
188
|
+
|
189
|
+
def __repr__(self):
|
190
|
+
console = Console()
|
191
|
+
table = Table(title="Available widgets for BEC CLI usage")
|
192
|
+
table.add_column("Widget Name", justify="left", style="magenta")
|
193
|
+
table.add_column("Description", justify="left")
|
194
|
+
for attr_name, _ in self.__dict__.items():
|
195
|
+
docs = getattr(client, attr_name).__doc__
|
196
|
+
docs = docs if docs else "No description available"
|
197
|
+
table.add_row(attr_name, docs if len(docs.strip()) > 0 else "No description available")
|
198
|
+
console.print(table)
|
199
|
+
return ""
|
168
200
|
|
169
201
|
|
170
202
|
class BECGuiClient(RPCBase):
|
171
|
-
|
203
|
+
"""BEC GUI client class. Container for GUI applications within Python."""
|
172
204
|
|
173
205
|
def __init__(self, **kwargs) -> None:
|
174
206
|
super().__init__(**kwargs)
|
175
|
-
self.
|
176
|
-
self.
|
207
|
+
self._lock = Lock()
|
208
|
+
self._anchor_widget = "launcher"
|
209
|
+
self._killed = False
|
210
|
+
self._top_level: dict[str, RPCReference] = {}
|
177
211
|
self._startup_timeout = 0
|
178
212
|
self._gui_started_timer = None
|
179
213
|
self._gui_started_event = threading.Event()
|
180
214
|
self._process = None
|
181
215
|
self._process_output_processing_thread = None
|
216
|
+
self._server_registry: dict[str, RegistryState] = {}
|
217
|
+
self._ipython_registry: dict[str, RPCReference] = {}
|
218
|
+
self.available_widgets = AvailableWidgetsNamespace()
|
219
|
+
register_serializer_extension()
|
182
220
|
|
183
|
-
|
184
|
-
|
185
|
-
|
221
|
+
####################
|
222
|
+
#### Client API ####
|
223
|
+
####################
|
186
224
|
|
187
225
|
@property
|
188
|
-
def
|
189
|
-
|
190
|
-
|
191
|
-
|
226
|
+
def launcher(self) -> RPCBase:
|
227
|
+
"""The launcher object."""
|
228
|
+
return RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self, object_name="launcher")
|
229
|
+
|
230
|
+
def connect_to_gui_server(self, gui_id: str) -> None:
|
231
|
+
"""Connect to a GUI server"""
|
232
|
+
# Unregister the old callback
|
233
|
+
self._client.connector.unregister(
|
234
|
+
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
|
235
|
+
)
|
236
|
+
self._gui_id = gui_id
|
237
|
+
|
238
|
+
# reset the namespace
|
239
|
+
self._update_dynamic_namespace({})
|
240
|
+
self._server_registry = {}
|
241
|
+
self._top_level = {}
|
242
|
+
self._ipython_registry = {}
|
243
|
+
|
244
|
+
# Register the new callback
|
245
|
+
self._client.connector.register(
|
246
|
+
MessageEndpoints.gui_registry_state(self._gui_id),
|
247
|
+
cb=self._handle_registry_update,
|
248
|
+
parent=self,
|
249
|
+
from_start=True,
|
250
|
+
)
|
192
251
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
try:
|
198
|
-
spec = importlib.util.find_spec(ep.module)
|
199
|
-
# if the module is not found, we skip it
|
200
|
-
if spec is None:
|
201
|
-
continue
|
202
|
-
return ep.load()(gui=self._top_level["main"].widget)
|
203
|
-
except Exception as e:
|
204
|
-
logger.error(f"Error loading auto update script from plugin: {str(e)}")
|
205
|
-
return None
|
252
|
+
@property
|
253
|
+
def windows(self) -> dict:
|
254
|
+
"""Dictionary with dock areas in the GUI."""
|
255
|
+
return {widget.object_name: widget for widget in self._top_level.values()}
|
206
256
|
|
207
257
|
@property
|
208
|
-
def
|
258
|
+
def window_list(self) -> list:
|
259
|
+
"""List with dock areas in the GUI."""
|
260
|
+
return list(self._top_level.values())
|
261
|
+
|
262
|
+
def start(self, wait: bool = False) -> None:
|
263
|
+
"""Start the GUI server."""
|
264
|
+
return self._start(wait=wait)
|
265
|
+
|
266
|
+
def show(self):
|
267
|
+
"""Show the GUI window."""
|
268
|
+
if self._check_if_server_is_alive():
|
269
|
+
return self._show_all()
|
270
|
+
return self.start(wait=True)
|
271
|
+
|
272
|
+
def hide(self):
|
273
|
+
"""Hide the GUI window."""
|
274
|
+
return self._hide_all()
|
275
|
+
|
276
|
+
def new(
|
277
|
+
self,
|
278
|
+
name: str | None = None,
|
279
|
+
wait: bool = True,
|
280
|
+
geometry: tuple[int, int, int, int] | None = None,
|
281
|
+
launch_script: str = "dock_area",
|
282
|
+
**kwargs,
|
283
|
+
) -> client.BECDockArea:
|
284
|
+
"""Create a new top-level dock area.
|
285
|
+
|
286
|
+
Args:
|
287
|
+
name(str, optional): The name of the dock area. Defaults to None.
|
288
|
+
wait(bool, optional): Whether to wait for the server to start. Defaults to True.
|
289
|
+
geometry(tuple[int, int, int, int] | None): The geometry of the dock area (pos_x, pos_y, w, h)
|
290
|
+
Returns:
|
291
|
+
client.BECDockArea: The new dock area.
|
209
292
|
"""
|
210
|
-
|
293
|
+
if not self._check_if_server_is_alive():
|
294
|
+
self.start(wait=True)
|
295
|
+
if wait:
|
296
|
+
with wait_for_server(self):
|
297
|
+
widget = self.launcher._run_rpc(
|
298
|
+
"launch", launch_script=launch_script, name=name, geometry=geometry, **kwargs
|
299
|
+
) # pylint: disable=protected-access
|
300
|
+
return widget
|
301
|
+
widget = self.launcher._run_rpc(
|
302
|
+
"launch", launch_script=launch_script, name=name, geometry=geometry, **kwargs
|
303
|
+
) # pylint: disable=protected-access
|
304
|
+
return widget
|
305
|
+
|
306
|
+
def delete(self, name: str) -> None:
|
307
|
+
"""Delete a dock area.
|
308
|
+
|
309
|
+
Args:
|
310
|
+
name(str): The name of the dock area.
|
211
311
|
"""
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
return None
|
217
|
-
|
218
|
-
@selected_device.setter
|
219
|
-
def selected_device(self, device: str | DeviceBase):
|
220
|
-
if isinstance_based_on_class_name(device, "bec_lib.device.DeviceBase"):
|
221
|
-
self._client.connector.set_and_publish(
|
222
|
-
MessageEndpoints.gui_auto_update_config(self._gui_id),
|
223
|
-
messages.GUIAutoUpdateConfigMessage(selected_device=device.name),
|
224
|
-
)
|
225
|
-
elif isinstance(device, str):
|
226
|
-
self._client.connector.set_and_publish(
|
227
|
-
MessageEndpoints.gui_auto_update_config(self._gui_id),
|
228
|
-
messages.GUIAutoUpdateConfigMessage(selected_device=device),
|
229
|
-
)
|
230
|
-
else:
|
231
|
-
raise ValueError("Device must be a string or a device object")
|
312
|
+
widget = self.windows.get(name)
|
313
|
+
if widget is None:
|
314
|
+
raise ValueError(f"Dock area {name} not found.")
|
315
|
+
widget._run_rpc("close") # pylint: disable=protected-access
|
232
316
|
|
233
|
-
def
|
234
|
-
|
317
|
+
def delete_all(self) -> None:
|
318
|
+
"""Delete all dock areas."""
|
319
|
+
for widget_name in self.windows:
|
320
|
+
self.delete(widget_name)
|
235
321
|
|
236
|
-
def
|
237
|
-
|
238
|
-
|
239
|
-
|
322
|
+
def kill_server(self) -> None:
|
323
|
+
"""Kill the GUI server."""
|
324
|
+
# Unregister the registry state
|
325
|
+
self._killed = True
|
240
326
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
return
|
245
|
-
if self._auto_updates_enabled:
|
246
|
-
return self.auto_updates.do_update(msg)
|
327
|
+
if self._gui_started_timer is not None:
|
328
|
+
self._gui_started_timer.cancel()
|
329
|
+
self._gui_started_timer.join()
|
247
330
|
|
248
|
-
|
249
|
-
|
250
|
-
|
331
|
+
if self._process is None:
|
332
|
+
return
|
333
|
+
|
334
|
+
if self._process:
|
335
|
+
logger.success("Stopping GUI...")
|
336
|
+
self._process.terminate()
|
337
|
+
if self._process_output_processing_thread:
|
338
|
+
self._process_output_processing_thread.join()
|
339
|
+
self._process.wait()
|
340
|
+
self._process = None
|
341
|
+
|
342
|
+
# Unregister the registry state
|
343
|
+
self._client.connector.unregister(
|
344
|
+
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
|
251
345
|
)
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
346
|
+
# Remove all reference from top level
|
347
|
+
self._top_level.clear()
|
348
|
+
self._server_registry.clear()
|
349
|
+
|
350
|
+
def close(self):
|
351
|
+
"""Deprecated. Use kill_server() instead."""
|
352
|
+
# FIXME, deprecated in favor of kill, will be removed in the future
|
353
|
+
self.kill_server()
|
354
|
+
|
355
|
+
#########################
|
356
|
+
#### Private methods ####
|
357
|
+
#########################
|
358
|
+
|
359
|
+
def _check_if_server_is_alive(self):
|
360
|
+
"""Checks if the process is alive"""
|
361
|
+
if self._process is None:
|
362
|
+
return False
|
363
|
+
if self._process.poll() is not None:
|
364
|
+
return False
|
365
|
+
return True
|
366
|
+
|
367
|
+
def _gui_post_startup(self):
|
368
|
+
timeout = 60
|
369
|
+
# Wait for 'bec' gui to be registered, this may take some time
|
370
|
+
# After 60s timeout. Should this raise an exception on timeout?
|
371
|
+
while time.time() < time.time() + timeout:
|
372
|
+
if len(list(self._server_registry.keys())) < 2 or not hasattr(
|
373
|
+
self, self._anchor_widget
|
374
|
+
):
|
375
|
+
time.sleep(0.1)
|
376
|
+
else:
|
377
|
+
break
|
378
|
+
|
264
379
|
self._gui_started_event.set()
|
265
380
|
|
266
|
-
def
|
381
|
+
def _start_server(self, wait: bool = False) -> None:
|
267
382
|
"""
|
268
383
|
Start the GUI server, and execute callback when it is launched
|
269
384
|
"""
|
@@ -272,7 +387,10 @@ class BECGuiClient(RPCBase):
|
|
272
387
|
self._startup_timeout = 5
|
273
388
|
self._gui_started_event.clear()
|
274
389
|
self._process, self._process_output_processing_thread = _start_plot_process(
|
275
|
-
self._gui_id,
|
390
|
+
self._gui_id,
|
391
|
+
gui_class_id="bec",
|
392
|
+
config=self._client._service_config.config, # pylint: disable=protected-access
|
393
|
+
logger=logger,
|
276
394
|
)
|
277
395
|
|
278
396
|
def gui_started_callback(callback):
|
@@ -280,80 +398,134 @@ class BECGuiClient(RPCBase):
|
|
280
398
|
if callable(callback):
|
281
399
|
callback()
|
282
400
|
finally:
|
283
|
-
threading.current_thread().cancel()
|
401
|
+
threading.current_thread().cancel() # type: ignore
|
284
402
|
|
285
403
|
self._gui_started_timer = RepeatTimer(
|
286
|
-
0.5, lambda: self.
|
404
|
+
0.5, lambda: self._gui_is_alive() and gui_started_callback(self._gui_post_startup)
|
287
405
|
)
|
288
406
|
self._gui_started_timer.start()
|
289
407
|
|
290
408
|
if wait:
|
291
409
|
self._gui_started_event.wait()
|
292
410
|
|
293
|
-
def
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
411
|
+
def _start(self, wait: bool = False) -> None:
|
412
|
+
self._killed = False
|
413
|
+
self._client.connector.register(
|
414
|
+
MessageEndpoints.gui_registry_state(self._gui_id),
|
415
|
+
cb=self._handle_registry_update,
|
416
|
+
parent=self,
|
417
|
+
)
|
418
|
+
return self._start_server(wait=wait)
|
419
|
+
|
420
|
+
@staticmethod
|
421
|
+
def _handle_registry_update(
|
422
|
+
msg: dict[str, GUIRegistryStateMessage], parent: BECGuiClient
|
423
|
+
) -> None:
|
424
|
+
# This was causing a deadlock during shutdown, not sure why.
|
425
|
+
# with self._lock:
|
426
|
+
self = parent
|
427
|
+
self._server_registry = cast(dict[str, RegistryState], msg["data"].state)
|
428
|
+
self._update_dynamic_namespace(self._server_registry)
|
299
429
|
|
300
430
|
def _do_show_all(self):
|
301
|
-
rpc_client = RPCBase(gui_id=f"{self._gui_id}:
|
302
|
-
rpc_client._run_rpc("show")
|
431
|
+
rpc_client = RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self)
|
432
|
+
rpc_client._run_rpc("show") # pylint: disable=protected-access
|
303
433
|
for window in self._top_level.values():
|
304
|
-
window.
|
434
|
+
window.show()
|
305
435
|
|
306
|
-
def
|
436
|
+
def _show_all(self):
|
307
437
|
with wait_for_server(self):
|
308
438
|
return self._do_show_all()
|
309
439
|
|
310
|
-
def
|
311
|
-
with wait_for_server(self):
|
312
|
-
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
313
|
-
rpc_client._run_rpc("hide")
|
314
|
-
for window in self._top_level.values():
|
315
|
-
window.widget.hide()
|
316
|
-
|
317
|
-
def show(self):
|
318
|
-
if self._process is not None:
|
319
|
-
return self.show_all()
|
320
|
-
# backward compatibility: show() was also starting server
|
321
|
-
return self.start_server(wait=True)
|
322
|
-
|
323
|
-
def hide(self):
|
324
|
-
return self.hide_all()
|
325
|
-
|
326
|
-
@property
|
327
|
-
def main(self):
|
328
|
-
"""Return client to main dock area (in main window)"""
|
440
|
+
def _hide_all(self):
|
329
441
|
with wait_for_server(self):
|
330
|
-
|
442
|
+
rpc_client = RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self)
|
443
|
+
rpc_client._run_rpc("hide") # pylint: disable=protected-access
|
444
|
+
if not self._killed:
|
445
|
+
for window in self._top_level.values():
|
446
|
+
window.hide()
|
331
447
|
|
332
|
-
def
|
333
|
-
"""
|
334
|
-
with
|
335
|
-
|
336
|
-
widget = rpc_client._run_rpc("new_dock_area", title)
|
337
|
-
self._top_level[widget._gui_id] = WidgetDesc(title=title, widget=widget)
|
338
|
-
return widget
|
448
|
+
def _update_dynamic_namespace(self, server_registry: dict):
|
449
|
+
"""
|
450
|
+
Update the dynamic name space with the given server registry.
|
451
|
+
Setting the server registry to an empty dictionary will remove all widgets from the namespace.
|
339
452
|
|
340
|
-
|
453
|
+
Args:
|
454
|
+
server_registry (dict): The server registry
|
341
455
|
"""
|
342
|
-
|
456
|
+
top_level_widgets: dict[str, RPCReference] = {}
|
457
|
+
for gui_id, state in server_registry.items():
|
458
|
+
widget = self._add_widget(state, self)
|
459
|
+
if widget is None:
|
460
|
+
# ignore widgets that are not supported
|
461
|
+
continue
|
462
|
+
# get all top-level widgets. These are widgets that have no parent
|
463
|
+
if not state["config"].get("parent_id"):
|
464
|
+
top_level_widgets[gui_id] = widget
|
465
|
+
|
466
|
+
remove_from_registry = []
|
467
|
+
for gui_id, widget in self._ipython_registry.items():
|
468
|
+
if gui_id not in server_registry:
|
469
|
+
remove_from_registry.append(gui_id)
|
470
|
+
for gui_id in remove_from_registry:
|
471
|
+
self._ipython_registry.pop(gui_id)
|
472
|
+
|
473
|
+
removed_widgets = [
|
474
|
+
widget.object_name for widget in self._top_level.values() if widget._is_deleted()
|
475
|
+
]
|
476
|
+
|
477
|
+
for widget_name in removed_widgets:
|
478
|
+
# the check is not strictly necessary, but better safe
|
479
|
+
# than sorry; who knows what the user has done
|
480
|
+
if hasattr(self, widget_name):
|
481
|
+
delattr(self, widget_name)
|
482
|
+
|
483
|
+
for gui_id, widget_ref in top_level_widgets.items():
|
484
|
+
setattr(self, widget_ref.object_name, widget_ref)
|
485
|
+
|
486
|
+
self._top_level = top_level_widgets
|
487
|
+
|
488
|
+
for widget in self._ipython_registry.values():
|
489
|
+
widget._refresh_references()
|
490
|
+
|
491
|
+
def _add_widget(self, state: dict, parent: object) -> RPCReference | None:
|
492
|
+
"""Add a widget to the namespace
|
493
|
+
|
494
|
+
Args:
|
495
|
+
state (dict): The state of the widget from the _server_registry.
|
496
|
+
parent (object): The parent object.
|
343
497
|
"""
|
344
|
-
|
498
|
+
object_name = state["object_name"]
|
499
|
+
gui_id = state["gui_id"]
|
500
|
+
if state["widget_class"] in IGNORE_WIDGETS:
|
501
|
+
return
|
502
|
+
widget_class = getattr(client, state["widget_class"], None)
|
503
|
+
if widget_class is None:
|
504
|
+
return
|
505
|
+
obj = self._ipython_registry.get(gui_id)
|
506
|
+
if obj is None:
|
507
|
+
widget = widget_class(gui_id=gui_id, object_name=object_name, parent=parent)
|
508
|
+
self._ipython_registry[gui_id] = widget
|
509
|
+
else:
|
510
|
+
widget = obj
|
511
|
+
obj = RPCReference(registry=self._ipython_registry, gui_id=gui_id)
|
512
|
+
return obj
|
345
513
|
|
346
|
-
if self._gui_started_timer is not None:
|
347
|
-
self._gui_started_timer.cancel()
|
348
|
-
self._gui_started_timer.join()
|
349
514
|
|
350
|
-
|
351
|
-
|
515
|
+
if __name__ == "__main__": # pragma: no cover
|
516
|
+
from bec_lib.client import BECClient
|
517
|
+
from bec_lib.service_config import ServiceConfig
|
352
518
|
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
519
|
+
try:
|
520
|
+
config = ServiceConfig()
|
521
|
+
bec_client = BECClient(config)
|
522
|
+
bec_client.start()
|
523
|
+
|
524
|
+
# Test the client_utils.py module
|
525
|
+
gui = BECGuiClient()
|
526
|
+
|
527
|
+
gui.start(wait=True)
|
528
|
+
gui.new().new(widget="Waveform")
|
529
|
+
time.sleep(10)
|
530
|
+
finally:
|
531
|
+
gui.kill_server()
|