bec-widgets 1.25.1__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. .gitlab-ci.yml +3 -5
  2. CHANGELOG.md +639 -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 +188 -138
  51. bec_widgets/widgets/containers/layout_manager/layout_manager.py +2 -1
  52. bec_widgets/widgets/containers/main_window/addons/web_links.py +15 -0
  53. bec_widgets/widgets/containers/main_window/main_window.py +189 -41
  54. bec_widgets/widgets/control/buttons/button_abort/button_abort.py +3 -4
  55. bec_widgets/widgets/control/buttons/button_reset/button_reset.py +3 -4
  56. bec_widgets/widgets/control/buttons/button_resume/button_resume.py +3 -3
  57. bec_widgets/widgets/control/buttons/stop_button/stop_button.py +18 -7
  58. bec_widgets/widgets/control/device_control/position_indicator/position_indicator.py +22 -3
  59. bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +31 -13
  60. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +3 -1
  61. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.ui +27 -4
  62. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +5 -2
  63. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +97 -31
  64. bec_widgets/widgets/control/device_control/positioner_box/positioner_control_line/positioner_control_line.ui +11 -4
  65. bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +2 -3
  66. bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +29 -4
  67. bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +1 -0
  68. bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +2 -2
  69. bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +2 -2
  70. bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py +1 -2
  71. bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.py +1 -2
  72. bec_widgets/widgets/control/scan_control/scan_control.py +7 -5
  73. bec_widgets/widgets/control/scan_control/scan_group_box.py +28 -5
  74. bec_widgets/widgets/dap/dap_combo_box/dap_combo_box.py +1 -2
  75. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog.py +3 -4
  76. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog_vertical.ui +14 -8
  77. bec_widgets/widgets/editors/console/console.py +1 -1
  78. bec_widgets/widgets/editors/{scan_metadata/additional_metadata_table.py → dict_backed_table.py} +29 -6
  79. bec_widgets/widgets/editors/scan_metadata/__init__.py +0 -7
  80. bec_widgets/widgets/editors/scan_metadata/_util.py +1 -1
  81. bec_widgets/widgets/{plots/motor_map/register_bec_motor_map_widget.py → editors/scan_metadata/register_scan_metadata.py} +2 -4
  82. bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +42 -136
  83. bec_widgets/widgets/editors/scan_metadata/scan_metadata.pyproject +1 -0
  84. bec_widgets/widgets/{plots/multi_waveform/bec_multi_waveform_widget_plugin.py → editors/scan_metadata/scan_metadata_plugin.py} +9 -9
  85. bec_widgets/widgets/editors/text_box/text_box.py +2 -3
  86. bec_widgets/widgets/editors/website/website.py +2 -2
  87. bec_widgets/widgets/games/minesweeper.py +3 -2
  88. bec_widgets/widgets/plots/image/image.py +960 -0
  89. bec_widgets/widgets/plots/image/image.pyproject +1 -0
  90. bec_widgets/widgets/plots/image/image_item.py +279 -0
  91. bec_widgets/widgets/plots/{motor_map/bec_motor_map_widget_plugin.py → image/image_plugin.py} +11 -13
  92. bec_widgets/widgets/{containers/figure/plots → plots}/image/image_processor.py +31 -64
  93. bec_widgets/widgets/plots/image/{register_bec_image_widget.py → register_image.py} +2 -2
  94. bec_widgets/widgets/plots/image/toolbar_bundles/image_selection.py +59 -0
  95. bec_widgets/widgets/plots/image/toolbar_bundles/processing.py +79 -0
  96. bec_widgets/widgets/plots/motor_map/motor_map.py +832 -0
  97. bec_widgets/widgets/plots/motor_map/motor_map.pyproject +1 -0
  98. bec_widgets/widgets/plots/motor_map/motor_map_plugin.py +54 -0
  99. bec_widgets/widgets/plots/{multi_waveform/register_bec_multi_waveform_widget.py → motor_map/register_motor_map.py} +2 -4
  100. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py +129 -0
  101. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui +120 -0
  102. bec_widgets/widgets/plots/motor_map/toolbar_bundles/motor_selection.py +70 -0
  103. bec_widgets/widgets/plots/multi_waveform/multi_waveform.py +508 -0
  104. bec_widgets/widgets/plots/multi_waveform/multi_waveform.pyproject +1 -0
  105. bec_widgets/widgets/plots/multi_waveform/multi_waveform_plugin.py +54 -0
  106. bec_widgets/widgets/plots/multi_waveform/register_multi_waveform.py +15 -0
  107. bec_widgets/widgets/plots/multi_waveform/settings/control_panel.py +144 -0
  108. bec_widgets/widgets/plots/multi_waveform/settings/multi_waveform_controls.ui +164 -0
  109. bec_widgets/widgets/plots/multi_waveform/toolbar_bundles/monitor_selection.py +65 -0
  110. bec_widgets/widgets/{plots_next_gen → plots}/plot_base.py +321 -40
  111. bec_widgets/widgets/plots/{waveform/register_bec_waveform_widget.py → scatter_waveform/register_scatter_waveform.py} +3 -3
  112. bec_widgets/widgets/plots/scatter_waveform/scatter_curve.py +197 -0
  113. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py +553 -0
  114. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.pyproject +1 -0
  115. bec_widgets/widgets/plots/{image/bec_image_widget_plugin.py → scatter_waveform/scatter_waveform_plugin.py} +9 -13
  116. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_setting.py +138 -0
  117. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_horizontal.ui +195 -0
  118. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_vertical.ui +204 -0
  119. bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings.py +8 -8
  120. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/mouse_interactions.py +4 -18
  121. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/plot_export.py +14 -3
  122. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/roi_bundle.py +6 -1
  123. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/save_state.py +2 -2
  124. bec_widgets/widgets/{containers/figure/plots/waveform/waveform_curve.py → plots/waveform/curve.py} +119 -49
  125. bec_widgets/widgets/plots/waveform/register_waveform.py +15 -0
  126. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_setting.py +125 -0
  127. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +576 -0
  128. bec_widgets/widgets/plots/waveform/utils/__init__.py +0 -0
  129. bec_widgets/widgets/plots/waveform/utils/roi_manager.py +84 -0
  130. bec_widgets/widgets/plots/waveform/waveform.py +1794 -0
  131. bec_widgets/widgets/plots/waveform/waveform.pyproject +1 -0
  132. bec_widgets/widgets/plots/waveform/{bec_waveform_widget_plugin.py → waveform_plugin.py} +9 -13
  133. bec_widgets/widgets/progress/bec_progressbar/bec_progressbar.py +1 -2
  134. bec_widgets/widgets/progress/ring_progress_bar/ring.py +11 -10
  135. bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +24 -14
  136. bec_widgets/widgets/services/bec_queue/bec_queue.py +13 -11
  137. bec_widgets/widgets/services/bec_status_box/bec_status_box.py +3 -4
  138. bec_widgets/widgets/services/device_browser/device_browser.py +5 -2
  139. bec_widgets/widgets/services/device_browser/device_item/device_item.py +1 -1
  140. bec_widgets/widgets/utility/logpanel/logpanel.py +36 -17
  141. bec_widgets/widgets/utility/spinbox/decimal_spinbox.py +3 -3
  142. bec_widgets/widgets/utility/visual/color_button/color_button.py +1 -1
  143. bec_widgets/widgets/utility/visual/colormap_widget/colormap_widget.py +4 -6
  144. bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py +4 -8
  145. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/METADATA +3 -3
  146. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/RECORD +168 -153
  147. pyproject.toml +3 -3
  148. bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +0 -198
  149. bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +0 -615
  150. bec_widgets/applications/bec_app.py +0 -84
  151. bec_widgets/cli/auto_updates.py +0 -168
  152. bec_widgets/widgets/containers/figure/__init__.py +0 -1
  153. bec_widgets/widgets/containers/figure/figure.py +0 -796
  154. bec_widgets/widgets/containers/figure/plots/axis_settings.py +0 -91
  155. bec_widgets/widgets/containers/figure/plots/axis_settings.ui +0 -256
  156. bec_widgets/widgets/containers/figure/plots/image/image.py +0 -772
  157. bec_widgets/widgets/containers/figure/plots/image/image_item.py +0 -337
  158. bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py +0 -525
  159. bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py +0 -340
  160. bec_widgets/widgets/containers/figure/plots/plot_base.py +0 -505
  161. bec_widgets/widgets/containers/figure/plots/waveform/waveform.py +0 -1563
  162. bec_widgets/widgets/plots/image/bec_image_widget.pyproject +0 -1
  163. bec_widgets/widgets/plots/image/image_widget.py +0 -515
  164. bec_widgets/widgets/plots/motor_map/bec_motor_map_widget.pyproject +0 -1
  165. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.py +0 -56
  166. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.ui +0 -108
  167. bec_widgets/widgets/plots/motor_map/motor_map_widget.py +0 -234
  168. bec_widgets/widgets/plots/multi_waveform/bec_multi_waveform_widget.pyproject +0 -1
  169. bec_widgets/widgets/plots/multi_waveform/multi_waveform_controls.ui +0 -99
  170. bec_widgets/widgets/plots/multi_waveform/multi_waveform_widget.py +0 -536
  171. bec_widgets/widgets/plots/waveform/bec_waveform_widget.pyproject +0 -1
  172. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.py +0 -336
  173. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.ui +0 -372
  174. bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py +0 -25
  175. bec_widgets/widgets/plots/waveform/waveform_widget.py +0 -751
  176. /bec_widgets/{qt_utils → utils}/collapsible_panel_manager.py +0 -0
  177. /bec_widgets/{applications/alignment → utils/forms_from_types}/__init__.py +0 -0
  178. /bec_widgets/{qt_utils → utils}/redis_message_waiter.py +0 -0
  179. /bec_widgets/{applications/alignment/alignment_1d → widgets/containers/auto_update}/__init__.py +0 -0
  180. /bec_widgets/{qt_utils → widgets/containers/main_window/addons}/__init__.py +0 -0
  181. /bec_widgets/widgets/{containers/figure/plots → plots/image/toolbar_bundles}/__init__.py +0 -0
  182. /bec_widgets/widgets/{containers/figure/plots/image → plots/motor_map/settings}/__init__.py +0 -0
  183. /bec_widgets/widgets/{containers/figure/plots/motor_map → plots/motor_map/toolbar_bundles}/__init__.py +0 -0
  184. /bec_widgets/widgets/{containers/figure/plots/multi_waveform → plots/multi_waveform/settings}/__init__.py +0 -0
  185. /bec_widgets/widgets/{containers/figure/plots/waveform → plots/multi_waveform/toolbar_bundles}/__init__.py +0 -0
  186. /bec_widgets/widgets/plots/{motor_map/motor_map_dialog → scatter_waveform}/__init__.py +0 -0
  187. /bec_widgets/widgets/plots/{waveform/waveform_popups → scatter_waveform/settings}/__init__.py +0 -0
  188. /bec_widgets/widgets/plots/{waveform/waveform_popups/curve_dialog → setting_menus}/__init__.py +0 -0
  189. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_horizontal.ui +0 -0
  190. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_vertical.ui +0 -0
  191. /bec_widgets/widgets/plots/{waveform/waveform_popups/dap_summary_dialog → toolbar_bundles}/__init__.py +0 -0
  192. /bec_widgets/widgets/{plots_next_gen/setting_menus → plots/waveform/settings}/__init__.py +0 -0
  193. /bec_widgets/widgets/{plots_next_gen/toolbar_bundles → plots/waveform/settings/curve_settings}/__init__.py +0 -0
  194. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/WHEEL +0 -0
  195. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/entry_points.txt +0 -0
  196. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,36 @@
1
1
  from __future__ import annotations
2
2
 
3
- from threading import Lock
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
- def add_rpc(self, rpc: QObject):
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
- def remove_rpc(self, rpc: str):
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.utils import BECConnector
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 = {cls.__name__: cls for cls in clss.widgets}
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) -> BECConnector:
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(BECConnector): The created widget.
49
+ widget(BECWidget): The created widget.
46
50
  """
47
- if self._widget_classes is None:
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 functools
3
+ import argparse
4
4
  import json
5
+ import os
5
6
  import signal
6
7
  import sys
7
- import types
8
- from contextlib import contextmanager, redirect_stderr, redirect_stdout
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 bec_lib.utils.import_utils import lazy_import
15
- from qtpy.QtCore import Qt, QTimer
16
- from redis.exceptions import RedisError
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.qt_utils.error_popups import ErrorPopupUtility
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
- def on_rpc_update(self, msg: dict, metadata: dict):
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
- if isinstance(res, list):
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
- def _start_server(gui_id: str, gui_class: Union[BECFigure, BECDockArea], config: str | None = None):
183
- if config:
184
- try:
185
- config = json.loads(config)
186
- service_config = ServiceConfig(config=config)
187
- except (json.JSONDecodeError, TypeError):
188
- service_config = ServiceConfig(config_path=config)
189
- else:
190
- # if no config is provided, use the default config
191
- service_config = ServiceConfig()
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
- # bec_logger.configure(
194
- # service_config.redis,
195
- # QtRedisConnector,
196
- # service_name="BECWidgetsCLIServer",
197
- # service_config=service_config.service_config,
198
- # )
199
- server = BECWidgetsCLIServer(gui_id=gui_id, config=service_config, gui_class=gui_class)
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
- import argparse
205
- import os
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
- bec_logger.level = bec_logger.LOGLEVEL.INFO
226
- if args.hide:
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()