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.
Files changed (197) hide show
  1. .gitlab-ci.yml +11 -6
  2. CHANGELOG.md +650 -0
  3. PKG-INFO +3 -3
  4. bec_widgets/__init__.py +4 -0
  5. bec_widgets/applications/bw_launch.py +23 -0
  6. bec_widgets/applications/launch_window.py +430 -0
  7. bec_widgets/assets/app_icons/auto_update.png +0 -0
  8. bec_widgets/assets/app_icons/ui_loader_tile.png +0 -0
  9. bec_widgets/cli/__init__.py +0 -1
  10. bec_widgets/cli/client.py +1779 -2064
  11. bec_widgets/cli/client_utils.py +346 -174
  12. bec_widgets/cli/generate_cli.py +143 -37
  13. bec_widgets/cli/rpc/rpc_base.py +152 -21
  14. bec_widgets/cli/rpc/rpc_register.py +113 -6
  15. bec_widgets/cli/rpc/rpc_widget_handler.py +13 -11
  16. bec_widgets/cli/server.py +125 -239
  17. bec_widgets/examples/jupyter_console/jupyter_console_window.py +97 -145
  18. bec_widgets/examples/plugin_example_pyside/tictactoetaskmenu.py +1 -1
  19. bec_widgets/utils/bec_connector.py +190 -21
  20. bec_widgets/utils/bec_designer.py +7 -0
  21. bec_widgets/utils/bec_dispatcher.py +71 -4
  22. bec_widgets/utils/bec_plugin_helper.py +89 -0
  23. bec_widgets/utils/bec_signal_proxy.py +1 -1
  24. bec_widgets/utils/bec_widget.py +26 -10
  25. bec_widgets/utils/colors.py +1 -1
  26. bec_widgets/{qt_utils → utils}/compact_popup.py +2 -0
  27. bec_widgets/utils/container_utils.py +37 -12
  28. bec_widgets/utils/crosshair.py +25 -8
  29. bec_widgets/utils/entry_validator.py +3 -1
  30. bec_widgets/{qt_utils → utils}/error_popups.py +18 -0
  31. bec_widgets/{qt_utils → utils}/expandable_frame.py +2 -2
  32. bec_widgets/utils/forms_from_types/forms.py +182 -0
  33. bec_widgets/{widgets/editors/scan_metadata/_metadata_widgets.py → utils/forms_from_types/items.py} +41 -30
  34. bec_widgets/utils/generate_designer_plugin.py +40 -36
  35. bec_widgets/utils/linear_region_selector.py +2 -0
  36. bec_widgets/utils/name_utils.py +16 -0
  37. bec_widgets/{qt_utils → utils}/palette_viewer.py +2 -2
  38. bec_widgets/utils/plot_indicator_items.py +2 -5
  39. bec_widgets/utils/plugin_utils.py +47 -1
  40. bec_widgets/{qt_utils → utils}/round_frame.py +14 -14
  41. bec_widgets/utils/rpc_server.py +277 -0
  42. bec_widgets/utils/serialization.py +44 -0
  43. bec_widgets/{qt_utils → utils}/settings_dialog.py +26 -1
  44. bec_widgets/{qt_utils → utils}/side_panel.py +17 -10
  45. bec_widgets/{qt_utils → utils}/toolbar.py +69 -25
  46. bec_widgets/utils/ui_loader.py +8 -8
  47. bec_widgets/utils/widget_io.py +166 -25
  48. bec_widgets/widgets/containers/auto_update/auto_updates.py +364 -0
  49. bec_widgets/widgets/containers/dock/dock.py +157 -49
  50. bec_widgets/widgets/containers/dock/dock_area.py +186 -138
  51. bec_widgets/widgets/containers/layout_manager/layout_manager.py +2 -1
  52. bec_widgets/widgets/containers/main_window/addons/web_links.py +15 -0
  53. bec_widgets/widgets/containers/main_window/main_window.py +189 -41
  54. bec_widgets/widgets/control/buttons/button_abort/button_abort.py +3 -4
  55. bec_widgets/widgets/control/buttons/button_reset/button_reset.py +3 -4
  56. bec_widgets/widgets/control/buttons/button_resume/button_resume.py +3 -3
  57. bec_widgets/widgets/control/buttons/stop_button/stop_button.py +18 -7
  58. bec_widgets/widgets/control/device_control/position_indicator/position_indicator.py +22 -3
  59. bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +37 -18
  60. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +28 -4
  61. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.ui +27 -4
  62. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +5 -2
  63. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +97 -31
  64. bec_widgets/widgets/control/device_control/positioner_box/positioner_control_line/positioner_control_line.ui +11 -4
  65. bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +2 -3
  66. bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +29 -4
  67. bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +1 -0
  68. bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +2 -2
  69. bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +2 -2
  70. bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py +1 -2
  71. bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.py +1 -2
  72. bec_widgets/widgets/control/scan_control/scan_control.py +7 -5
  73. bec_widgets/widgets/control/scan_control/scan_group_box.py +28 -5
  74. bec_widgets/widgets/dap/dap_combo_box/dap_combo_box.py +1 -2
  75. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog.py +3 -4
  76. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog_vertical.ui +14 -8
  77. bec_widgets/widgets/editors/console/console.py +1 -1
  78. bec_widgets/widgets/editors/{scan_metadata/additional_metadata_table.py → dict_backed_table.py} +29 -6
  79. bec_widgets/widgets/editors/scan_metadata/__init__.py +0 -7
  80. bec_widgets/widgets/editors/scan_metadata/_util.py +1 -1
  81. bec_widgets/widgets/{plots/motor_map/register_bec_motor_map_widget.py → editors/scan_metadata/register_scan_metadata.py} +2 -4
  82. bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +42 -136
  83. bec_widgets/widgets/editors/scan_metadata/scan_metadata.pyproject +1 -0
  84. bec_widgets/widgets/{plots/multi_waveform/bec_multi_waveform_widget_plugin.py → editors/scan_metadata/scan_metadata_plugin.py} +9 -9
  85. bec_widgets/widgets/editors/text_box/text_box.py +2 -3
  86. bec_widgets/widgets/editors/website/website.py +2 -2
  87. bec_widgets/widgets/games/minesweeper.py +3 -2
  88. bec_widgets/widgets/plots/image/image.py +960 -0
  89. bec_widgets/widgets/plots/image/image.pyproject +1 -0
  90. bec_widgets/widgets/plots/image/image_item.py +279 -0
  91. bec_widgets/widgets/plots/{motor_map/bec_motor_map_widget_plugin.py → image/image_plugin.py} +11 -13
  92. bec_widgets/widgets/{containers/figure/plots → plots}/image/image_processor.py +31 -64
  93. bec_widgets/widgets/plots/image/{register_bec_image_widget.py → register_image.py} +2 -2
  94. bec_widgets/widgets/plots/image/toolbar_bundles/image_selection.py +59 -0
  95. bec_widgets/widgets/plots/image/toolbar_bundles/processing.py +79 -0
  96. bec_widgets/widgets/plots/motor_map/motor_map.py +832 -0
  97. bec_widgets/widgets/plots/motor_map/motor_map.pyproject +1 -0
  98. bec_widgets/widgets/plots/motor_map/motor_map_plugin.py +54 -0
  99. bec_widgets/widgets/plots/{multi_waveform/register_bec_multi_waveform_widget.py → motor_map/register_motor_map.py} +2 -4
  100. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py +129 -0
  101. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui +120 -0
  102. bec_widgets/widgets/plots/motor_map/toolbar_bundles/motor_selection.py +70 -0
  103. bec_widgets/widgets/plots/multi_waveform/multi_waveform.py +508 -0
  104. bec_widgets/widgets/plots/multi_waveform/multi_waveform.pyproject +1 -0
  105. bec_widgets/widgets/plots/multi_waveform/multi_waveform_plugin.py +54 -0
  106. bec_widgets/widgets/plots/multi_waveform/register_multi_waveform.py +15 -0
  107. bec_widgets/widgets/plots/multi_waveform/settings/control_panel.py +144 -0
  108. bec_widgets/widgets/plots/multi_waveform/settings/multi_waveform_controls.ui +164 -0
  109. bec_widgets/widgets/plots/multi_waveform/toolbar_bundles/monitor_selection.py +65 -0
  110. bec_widgets/widgets/{plots_next_gen → plots}/plot_base.py +321 -40
  111. bec_widgets/widgets/plots/{waveform/register_bec_waveform_widget.py → scatter_waveform/register_scatter_waveform.py} +3 -3
  112. bec_widgets/widgets/plots/scatter_waveform/scatter_curve.py +197 -0
  113. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py +553 -0
  114. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.pyproject +1 -0
  115. bec_widgets/widgets/plots/{image/bec_image_widget_plugin.py → scatter_waveform/scatter_waveform_plugin.py} +9 -13
  116. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_setting.py +138 -0
  117. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_horizontal.ui +195 -0
  118. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_vertical.ui +204 -0
  119. bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings.py +8 -8
  120. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/mouse_interactions.py +4 -18
  121. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/plot_export.py +14 -3
  122. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/roi_bundle.py +6 -1
  123. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/save_state.py +2 -2
  124. bec_widgets/widgets/{containers/figure/plots/waveform/waveform_curve.py → plots/waveform/curve.py} +119 -49
  125. bec_widgets/widgets/plots/waveform/register_waveform.py +15 -0
  126. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_setting.py +125 -0
  127. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +576 -0
  128. bec_widgets/widgets/plots/waveform/utils/__init__.py +0 -0
  129. bec_widgets/widgets/plots/waveform/utils/roi_manager.py +84 -0
  130. bec_widgets/widgets/plots/waveform/waveform.py +1794 -0
  131. bec_widgets/widgets/plots/waveform/waveform.pyproject +1 -0
  132. bec_widgets/widgets/plots/waveform/{bec_waveform_widget_plugin.py → waveform_plugin.py} +9 -13
  133. bec_widgets/widgets/progress/bec_progressbar/bec_progressbar.py +1 -2
  134. bec_widgets/widgets/progress/ring_progress_bar/ring.py +11 -10
  135. bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +24 -14
  136. bec_widgets/widgets/services/bec_queue/bec_queue.py +13 -11
  137. bec_widgets/widgets/services/bec_status_box/bec_status_box.py +3 -4
  138. bec_widgets/widgets/services/device_browser/device_browser.py +5 -2
  139. bec_widgets/widgets/services/device_browser/device_item/device_item.py +1 -1
  140. bec_widgets/widgets/utility/logpanel/logpanel.py +36 -17
  141. bec_widgets/widgets/utility/spinbox/decimal_spinbox.py +3 -3
  142. bec_widgets/widgets/utility/spinner/spinner.py +2 -2
  143. bec_widgets/widgets/utility/visual/color_button/color_button.py +1 -1
  144. bec_widgets/widgets/utility/visual/colormap_widget/colormap_widget.py +4 -6
  145. bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py +4 -8
  146. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/METADATA +3 -3
  147. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/RECORD +169 -154
  148. pyproject.toml +3 -3
  149. bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +0 -198
  150. bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +0 -615
  151. bec_widgets/applications/bec_app.py +0 -84
  152. bec_widgets/cli/auto_updates.py +0 -168
  153. bec_widgets/widgets/containers/figure/__init__.py +0 -1
  154. bec_widgets/widgets/containers/figure/figure.py +0 -796
  155. bec_widgets/widgets/containers/figure/plots/axis_settings.py +0 -91
  156. bec_widgets/widgets/containers/figure/plots/axis_settings.ui +0 -256
  157. bec_widgets/widgets/containers/figure/plots/image/image.py +0 -772
  158. bec_widgets/widgets/containers/figure/plots/image/image_item.py +0 -337
  159. bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py +0 -525
  160. bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py +0 -340
  161. bec_widgets/widgets/containers/figure/plots/plot_base.py +0 -505
  162. bec_widgets/widgets/containers/figure/plots/waveform/waveform.py +0 -1563
  163. bec_widgets/widgets/plots/image/bec_image_widget.pyproject +0 -1
  164. bec_widgets/widgets/plots/image/image_widget.py +0 -515
  165. bec_widgets/widgets/plots/motor_map/bec_motor_map_widget.pyproject +0 -1
  166. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.py +0 -56
  167. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.ui +0 -108
  168. bec_widgets/widgets/plots/motor_map/motor_map_widget.py +0 -234
  169. bec_widgets/widgets/plots/multi_waveform/bec_multi_waveform_widget.pyproject +0 -1
  170. bec_widgets/widgets/plots/multi_waveform/multi_waveform_controls.ui +0 -99
  171. bec_widgets/widgets/plots/multi_waveform/multi_waveform_widget.py +0 -536
  172. bec_widgets/widgets/plots/waveform/bec_waveform_widget.pyproject +0 -1
  173. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.py +0 -336
  174. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.ui +0 -372
  175. bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py +0 -25
  176. bec_widgets/widgets/plots/waveform/waveform_widget.py +0 -751
  177. /bec_widgets/{qt_utils → utils}/collapsible_panel_manager.py +0 -0
  178. /bec_widgets/{applications/alignment → utils/forms_from_types}/__init__.py +0 -0
  179. /bec_widgets/{qt_utils → utils}/redis_message_waiter.py +0 -0
  180. /bec_widgets/{applications/alignment/alignment_1d → widgets/containers/auto_update}/__init__.py +0 -0
  181. /bec_widgets/{qt_utils → widgets/containers/main_window/addons}/__init__.py +0 -0
  182. /bec_widgets/widgets/{containers/figure/plots → plots/image/toolbar_bundles}/__init__.py +0 -0
  183. /bec_widgets/widgets/{containers/figure/plots/image → plots/motor_map/settings}/__init__.py +0 -0
  184. /bec_widgets/widgets/{containers/figure/plots/motor_map → plots/motor_map/toolbar_bundles}/__init__.py +0 -0
  185. /bec_widgets/widgets/{containers/figure/plots/multi_waveform → plots/multi_waveform/settings}/__init__.py +0 -0
  186. /bec_widgets/widgets/{containers/figure/plots/waveform → plots/multi_waveform/toolbar_bundles}/__init__.py +0 -0
  187. /bec_widgets/widgets/plots/{motor_map/motor_map_dialog → scatter_waveform}/__init__.py +0 -0
  188. /bec_widgets/widgets/plots/{waveform/waveform_popups → scatter_waveform/settings}/__init__.py +0 -0
  189. /bec_widgets/widgets/plots/{waveform/waveform_popups/curve_dialog → setting_menus}/__init__.py +0 -0
  190. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_horizontal.ui +0 -0
  191. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_vertical.ui +0 -0
  192. /bec_widgets/widgets/plots/{waveform/waveform_popups/dap_summary_dialog → toolbar_bundles}/__init__.py +0 -0
  193. /bec_widgets/widgets/{plots_next_gen/setting_menus → plots/waveform/settings}/__init__.py +0 -0
  194. /bec_widgets/widgets/{plots_next_gen/toolbar_bundles → plots/waveform/settings/curve_settings}/__init__.py +0 -0
  195. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/WHEEL +0 -0
  196. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/entry_points.txt +0 -0
  197. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -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 dataclasses import dataclass
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 isinstance_based_on_class_name, lazy_import, lazy_import_from
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.auto_updates import AutoUpdates
20
- from bec_widgets.cli.rpc.rpc_base import RPCBase
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
- from bec_widgets.utils.bec_dispatcher import BECDispatcher
25
+ if TYPE_CHECKING: # pragma: no cover
26
+ from bec_lib.messages import GUIRegistryStateMessage
28
27
  else:
29
- messages = lazy_import("bec_lib.messages")
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(gui_id: str, gui_class: type, config: dict | str, logger=None) -> None:
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 = ["bec-gui-server", "--id", gui_id, "--gui_class", gui_class.__name__, "--hide"]
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.gui_is_alive():
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
- ### NOTE
143
- ### it is far easier to extend the 'delete' method on the client side,
144
- ### to know when the client is deleted, rather than listening to server
145
- ### to get notified. However, 'generate_cli.py' cannot add extra stuff
146
- ### in the generated client module. So, here a class with the same name
147
- ### is created, and client module is patched.
148
- class BECDockArea(client.BECDockArea):
149
- def delete(self):
150
- if self is BECGuiClient._top_level["main"].widget:
151
- raise RuntimeError("Cannot delete main window")
152
- super().delete()
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
- @dataclass
165
- class WidgetDesc:
166
- title: str
167
- widget: BECDockArea
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
- _top_level = {}
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._auto_updates_enabled = True
176
- self._auto_updates = None
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
- @property
184
- def windows(self):
185
- return self._top_level
221
+ ####################
222
+ #### Client API ####
223
+ ####################
186
224
 
187
225
  @property
188
- def auto_updates(self):
189
- if self._auto_updates_enabled:
190
- with wait_for_server(self):
191
- return self._auto_updates
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
- def _get_update_script(self) -> AutoUpdates | None:
194
- eps = imd.entry_points(group="bec.widgets.auto_updates")
195
- for ep in eps:
196
- if ep.name == "plugin_widgets_update":
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 selected_device(self):
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
- Selected device for the plot.
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
- auto_update_config_ep = MessageEndpoints.gui_auto_update_config(self._gui_id)
213
- auto_update_config = self._client.connector.get(auto_update_config_ep)
214
- if auto_update_config:
215
- return auto_update_config.selected_device
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 _start_update_script(self) -> None:
234
- self._client.connector.register(MessageEndpoints.scan_status(), cb=self._handle_msg_update)
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 _handle_msg_update(self, msg: MessageObject) -> None:
237
- if self.auto_updates is not None:
238
- # pylint: disable=protected-access
239
- return self._update_script_msg_parser(msg.value)
322
+ def kill_server(self) -> None:
323
+ """Kill the GUI server."""
324
+ # Unregister the registry state
325
+ self._killed = True
240
326
 
241
- def _update_script_msg_parser(self, msg: messages.BECMessage) -> None:
242
- if isinstance(msg, messages.ScanStatusMessage):
243
- if not self.gui_is_alive():
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
- def _gui_post_startup(self):
249
- self._top_level["main"] = WidgetDesc(
250
- title="BEC Widgets", widget=BECDockArea(gui_id=self._gui_id)
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
- if self._auto_updates_enabled:
253
- if self._auto_updates is None:
254
- auto_updates = self._get_update_script()
255
- if auto_updates is None:
256
- AutoUpdates.create_default_dock = True
257
- AutoUpdates.enabled = True
258
- auto_updates = AutoUpdates(self._top_level["main"].widget)
259
- if auto_updates.create_default_dock:
260
- auto_updates.start_default_dock()
261
- self._start_update_script()
262
- self._auto_updates = auto_updates
263
- self._do_show_all()
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 start_server(self, wait=False) -> None:
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, self.__class__, self._client._service_config.config, logger=logger
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.gui_is_alive() and gui_started_callback(self._gui_post_startup)
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 _dump(self):
294
- rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
295
- return rpc_client._run_rpc("_dump")
296
-
297
- def start(self):
298
- return self.start_server()
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}:window", parent=self)
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.widget.show()
434
+ window.show()
305
435
 
306
- def show_all(self):
436
+ def _show_all(self):
307
437
  with wait_for_server(self):
308
438
  return self._do_show_all()
309
439
 
310
- def hide_all(self):
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
- return self._top_level["main"].widget
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 new(self, title):
333
- """Ask main window to create a new top-level dock area"""
334
- with wait_for_server(self):
335
- rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
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
- def close(self) -> None:
453
+ Args:
454
+ server_registry (dict): The server registry
341
455
  """
342
- Close the gui window.
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
- self._top_level.clear()
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
- if self._process is None:
351
- return
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
- if self._process:
354
- logger.success("Stopping GUI...")
355
- self._process.terminate()
356
- if self._process_output_processing_thread:
357
- self._process_output_processing_thread.join()
358
- self._process.wait()
359
- self._process = None
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()