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
@@ -0,0 +1,277 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import traceback
5
+ import types
6
+ from contextlib import contextmanager
7
+ from typing import TYPE_CHECKING, Callable, TypeVar
8
+
9
+ from bec_lib.client import BECClient
10
+ from bec_lib.endpoints import MessageEndpoints
11
+ from bec_lib.logger import bec_logger
12
+ from bec_lib.utils.import_utils import lazy_import
13
+ from qtpy.QtCore import QTimer
14
+ from qtpy.QtWidgets import QApplication
15
+ from redis.exceptions import RedisError
16
+
17
+ from bec_widgets.cli.rpc.rpc_register import RPCRegister
18
+ from bec_widgets.utils import BECDispatcher
19
+ from bec_widgets.utils.bec_connector import BECConnector
20
+ from bec_widgets.utils.error_popups import ErrorPopupUtility
21
+ from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
22
+
23
+ if TYPE_CHECKING: # pragma: no cover
24
+ from bec_lib import messages
25
+ from qtpy.QtCore import QObject
26
+ else:
27
+ messages = lazy_import("bec_lib.messages")
28
+ logger = bec_logger.logger
29
+
30
+
31
+ T = TypeVar("T")
32
+
33
+
34
+ @contextmanager
35
+ def rpc_exception_hook(err_func):
36
+ """This context replaces the popup message box for error display with a specific hook"""
37
+ # get error popup utility singleton
38
+ popup = ErrorPopupUtility()
39
+ # save current setting
40
+ old_exception_hook = popup.custom_exception_hook
41
+
42
+ # install err_func, if it is a callable
43
+ # IMPORTANT, Keep self here, because this method is overwriting the custom_exception_hook
44
+ # of the ErrorPopupUtility (popup instance) class.
45
+ def custom_exception_hook(self, exc_type, value, tb, **kwargs):
46
+ err_func({"error": popup.get_error_message(exc_type, value, tb)})
47
+
48
+ popup.custom_exception_hook = types.MethodType(custom_exception_hook, popup)
49
+
50
+ try:
51
+ yield popup
52
+ finally:
53
+ # restore state of error popup utility singleton
54
+ popup.custom_exception_hook = old_exception_hook
55
+
56
+
57
+ class RPCServer:
58
+
59
+ client: BECClient
60
+
61
+ def __init__(
62
+ self,
63
+ gui_id: str,
64
+ dispatcher: BECDispatcher | None = None,
65
+ client: BECClient | None = None,
66
+ config=None,
67
+ gui_class_id: str = "bec",
68
+ ) -> None:
69
+ self.status = messages.BECStatus.BUSY
70
+ self.dispatcher = BECDispatcher(config=config) if dispatcher is None else dispatcher
71
+ self.client = self.dispatcher.client if client is None else client
72
+ self.client.start()
73
+ self.gui_id = gui_id
74
+ # register broadcast callback
75
+ self.rpc_register = RPCRegister()
76
+ self.rpc_register.add_callback(self.broadcast_registry_update)
77
+
78
+ self.dispatcher.connect_slot(
79
+ self.on_rpc_update, MessageEndpoints.gui_instructions(self.gui_id)
80
+ )
81
+
82
+ # Setup QTimer for heartbeat
83
+ self._heartbeat_timer = QTimer()
84
+ self._heartbeat_timer.timeout.connect(self.emit_heartbeat)
85
+ self._heartbeat_timer.start(200)
86
+ self._registry_update_callbacks = []
87
+ self._broadcasted_data = {}
88
+
89
+ self.status = messages.BECStatus.RUNNING
90
+ logger.success(f"Server started with gui_id: {self.gui_id}")
91
+
92
+ def on_rpc_update(self, msg: dict, metadata: dict):
93
+ request_id = metadata.get("request_id")
94
+ if request_id is None:
95
+ logger.error("Received RPC instruction without request_id")
96
+ return
97
+ logger.debug(f"Received RPC instruction: {msg}, metadata: {metadata}")
98
+ with rpc_exception_hook(functools.partial(self.send_response, request_id, False)):
99
+ try:
100
+ obj = self.get_object_from_config(msg["parameter"])
101
+ method = msg["action"]
102
+ args = msg["parameter"].get("args", [])
103
+ kwargs = msg["parameter"].get("kwargs", {})
104
+ res = self.run_rpc(obj, method, args, kwargs)
105
+ except Exception:
106
+ content = traceback.format_exc()
107
+ logger.error(f"Error while executing RPC instruction: {content}")
108
+ self.send_response(request_id, False, {"error": content})
109
+ else:
110
+ logger.debug(f"RPC instruction executed successfully: {res}")
111
+ self.send_response(request_id, True, {"result": res})
112
+
113
+ def send_response(self, request_id: str, accepted: bool, msg: dict):
114
+ self.client.connector.set_and_publish(
115
+ MessageEndpoints.gui_instruction_response(request_id),
116
+ messages.RequestResponseMessage(accepted=accepted, message=msg),
117
+ expire=60,
118
+ )
119
+
120
+ def get_object_from_config(self, config: dict):
121
+ gui_id = config.get("gui_id")
122
+ obj = self.rpc_register.get_rpc_by_id(gui_id)
123
+ if obj is None:
124
+ raise ValueError(f"Object with gui_id {gui_id} not found")
125
+ return obj
126
+
127
+ def run_rpc(self, obj, method, args, kwargs):
128
+ # Run with rpc registry broadcast, but only once
129
+ with RPCRegister.delayed_broadcast():
130
+ logger.debug(f"Running RPC instruction: {method} with args: {args}, kwargs: {kwargs}")
131
+ method_obj = getattr(obj, method)
132
+ # check if the method accepts args and kwargs
133
+ if not callable(method_obj):
134
+ if not args:
135
+ res = method_obj
136
+ else:
137
+ setattr(obj, method, args[0])
138
+ res = None
139
+ else:
140
+ res = method_obj(*args, **kwargs)
141
+
142
+ if isinstance(res, list):
143
+ res = [self.serialize_object(obj) for obj in res]
144
+ elif isinstance(res, dict):
145
+ res = {key: self.serialize_object(val) for key, val in res.items()}
146
+ else:
147
+ res = self.serialize_object(res)
148
+ return res
149
+
150
+ def serialize_object(self, obj: T) -> None | dict | T:
151
+ """
152
+ Serialize all BECConnector objects.
153
+
154
+ Args:
155
+ obj: The object to be serialized.
156
+
157
+ Returns:
158
+ None | dict | T: The serialized object or None if the object is not a BECConnector.
159
+ """
160
+ if not isinstance(obj, BECConnector):
161
+ return obj
162
+ # Respect RPC = False
163
+ if getattr(obj, "RPC", True) is False:
164
+ return None
165
+ return self._serialize_bec_connector(obj, wait=True)
166
+
167
+ def emit_heartbeat(self) -> None:
168
+ """
169
+ Emit a heartbeat message to the GUI server.
170
+ This method is called periodically to indicate that the server is still running.
171
+ """
172
+ logger.trace(f"Emitting heartbeat for {self.gui_id}")
173
+ try:
174
+ self.client.connector.set(
175
+ MessageEndpoints.gui_heartbeat(self.gui_id),
176
+ messages.StatusMessage(name=self.gui_id, status=self.status, info={}),
177
+ expire=10,
178
+ )
179
+ except RedisError as exc:
180
+ logger.error(f"Error while emitting heartbeat: {exc}")
181
+
182
+ def broadcast_registry_update(self, connections: dict) -> None:
183
+ """
184
+ Broadcast the registry update to all the callbacks.
185
+ This method is called whenever the registry is updated.
186
+ """
187
+ data = {}
188
+ for key, val in connections.items():
189
+ if not isinstance(val, BECConnector):
190
+ continue
191
+ if not getattr(val, "RPC", True):
192
+ continue
193
+ data[key] = self._serialize_bec_connector(val)
194
+ if self._broadcasted_data == data:
195
+ return
196
+ self._broadcasted_data = data
197
+
198
+ logger.info(f"Broadcasting registry update: {data} for {self.gui_id}")
199
+ self.client.connector.xadd(
200
+ MessageEndpoints.gui_registry_state(self.gui_id),
201
+ msg_dict={"data": messages.GUIRegistryStateMessage(state=data)},
202
+ max_size=1,
203
+ )
204
+
205
+ def _serialize_bec_connector(self, connector: BECConnector, wait=False) -> dict:
206
+ """
207
+ Create the serialization dict for a single BECConnector.
208
+
209
+ Args:
210
+ connector (BECConnector): The BECConnector to serialize.
211
+ wait (bool): If True, wait until the object is registered in the RPC register.
212
+
213
+ Returns:
214
+ dict: The serialized BECConnector object.
215
+ """
216
+
217
+ config_dict = connector.config.model_dump()
218
+ config_dict["parent_id"] = getattr(connector, "parent_id", None)
219
+
220
+ try:
221
+ parent = connector.parent()
222
+ if isinstance(parent, BECMainWindow):
223
+ container_proxy = parent.gui_id
224
+ else:
225
+ container_proxy = None
226
+ except Exception:
227
+ container_proxy = None
228
+
229
+ if wait:
230
+ while not self.rpc_register.object_is_registered(connector):
231
+ QApplication.processEvents()
232
+
233
+ widget_class = getattr(connector, "rpc_widget_class", None)
234
+ if not widget_class:
235
+ widget_class = connector.__class__.__name__
236
+
237
+ return {
238
+ "gui_id": connector.gui_id,
239
+ "object_name": connector.object_name or connector.__class__.__name__,
240
+ "widget_class": widget_class,
241
+ "config": config_dict,
242
+ "container_proxy": container_proxy,
243
+ "__rpc__": True,
244
+ }
245
+
246
+ @staticmethod
247
+ def _get_becwidget_ancestor(widget: QObject) -> BECConnector | None:
248
+ """
249
+ Traverse up the parent chain to find the nearest BECConnector.
250
+ Returns None if none is found.
251
+ """
252
+
253
+ parent = widget.parent()
254
+ while parent is not None:
255
+ if isinstance(parent, BECConnector):
256
+ return parent
257
+ parent = parent.parent()
258
+ return None
259
+
260
+ # Suppose clients register callbacks to receive updates
261
+ def add_registry_update_callback(self, cb: Callable) -> None:
262
+ """
263
+ Add a callback to be called whenever the registry is updated.
264
+ The specified callback is called whenever the registry is updated.
265
+
266
+ Args:
267
+ cb (Callable): The callback to be added. It should accept a dictionary of all the
268
+ registered RPC objects as an argument.
269
+ """
270
+ self._registry_update_callbacks.append(cb)
271
+
272
+ def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
273
+ self.status = messages.BECStatus.IDLE
274
+ self._heartbeat_timer.stop()
275
+ self.emit_heartbeat()
276
+ logger.info("Succeded in shutting down CLI server")
277
+ self.client.shutdown()
@@ -0,0 +1,44 @@
1
+ from bec_lib.serialization import msgpack
2
+ from qtpy.QtCore import QPointF
3
+
4
+
5
+ def register_serializer_extension():
6
+ """
7
+ Register the serializer extension for the BECConnector.
8
+ """
9
+ if not module_is_registered("bec_widgets.utils.serialization"):
10
+ msgpack.register_object_hook(encode_qpointf, decode_qpointf)
11
+
12
+
13
+ def module_is_registered(module_name: str) -> bool:
14
+ """
15
+ Check if the module is registered in the encoder.
16
+
17
+ Args:
18
+ module_name (str): The name of the module to check.
19
+
20
+ Returns:
21
+ bool: True if the module is registered, False otherwise.
22
+ """
23
+ # pylint: disable=protected-access
24
+ for enc in msgpack._encoder:
25
+ if enc[0].__module__ == module_name:
26
+ return True
27
+ return False
28
+
29
+
30
+ def encode_qpointf(obj):
31
+ """
32
+ Encode a QPointF object to a list of floats. As this is mostly used for sending
33
+ data to the client, it is not necessary to convert it back to a QPointF object.
34
+ """
35
+ if isinstance(obj, QPointF):
36
+ return [obj.x(), obj.y()]
37
+ return obj
38
+
39
+
40
+ def decode_qpointf(obj):
41
+ """
42
+ no-op function since QPointF is encoded as a list of floats.
43
+ """
44
+ return obj
@@ -1,6 +1,10 @@
1
+ from bec_lib.logger import bec_logger
2
+ from PySide6.QtGui import QCloseEvent
1
3
  from qtpy.QtWidgets import QDialog, QDialogButtonBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget
2
4
 
3
- from bec_widgets.qt_utils.error_popups import SafeSlot
5
+ from bec_widgets.utils.error_popups import SafeSlot
6
+
7
+ logger = bec_logger.logger
4
8
 
5
9
 
6
10
  class SettingWidget(QWidget):
@@ -37,6 +41,15 @@ class SettingWidget(QWidget):
37
41
  """
38
42
  pass
39
43
 
44
+ def cleanup(self):
45
+ """
46
+ Cleanup the settings widget.
47
+ """
48
+
49
+ def closeEvent(self, event: QCloseEvent) -> None:
50
+ self.cleanup()
51
+ return super().closeEvent(event)
52
+
40
53
 
41
54
  class SettingsDialog(QDialog):
42
55
  """
@@ -99,8 +112,17 @@ class SettingsDialog(QDialog):
99
112
  Accept the changes made in the settings widget and close the dialog.
100
113
  """
101
114
  self.widget.accept_changes()
115
+ self.cleanup()
102
116
  super().accept()
103
117
 
118
+ @SafeSlot()
119
+ def reject(self):
120
+ """
121
+ Reject the changes made in the settings widget and close the dialog.
122
+ """
123
+ self.cleanup()
124
+ super().reject()
125
+
104
126
  @SafeSlot()
105
127
  def apply_changes(self):
106
128
  """
@@ -114,7 +136,10 @@ class SettingsDialog(QDialog):
114
136
  """
115
137
  self.button_box.close()
116
138
  self.button_box.deleteLater()
139
+ self.widget.close()
140
+ self.widget.deleteLater()
117
141
 
118
142
  def closeEvent(self, event):
143
+ logger.info("Closing settings dialog")
119
144
  self.cleanup()
120
145
  super().closeEvent(event)
@@ -16,7 +16,7 @@ from qtpy.QtWidgets import (
16
16
  QWidget,
17
17
  )
18
18
 
19
- from bec_widgets.qt_utils.toolbar import MaterialIconAction, ModularToolBar
19
+ from bec_widgets.utils.toolbar import MaterialIconAction, ModularToolBar
20
20
 
21
21
 
22
22
  class SidePanel(QWidget):
@@ -35,7 +35,6 @@ class SidePanel(QWidget):
35
35
  super().__init__(parent=parent)
36
36
 
37
37
  self.setProperty("skip_settings", True)
38
- self.setObjectName("SidePanel")
39
38
 
40
39
  self._orientation = orientation
41
40
  self._panel_max_width = panel_max_width
@@ -60,7 +59,7 @@ class SidePanel(QWidget):
60
59
  self.main_layout.setContentsMargins(0, 0, 0, 0)
61
60
  self.main_layout.setSpacing(0)
62
61
 
63
- self.toolbar = ModularToolBar(target_widget=self, orientation="vertical")
62
+ self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="vertical")
64
63
 
65
64
  self.container = QWidget()
66
65
  self.container.layout = QVBoxLayout(self.container)
@@ -90,7 +89,7 @@ class SidePanel(QWidget):
90
89
  self.main_layout.setContentsMargins(0, 0, 0, 0)
91
90
  self.main_layout.setSpacing(0)
92
91
 
93
- self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
92
+ self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
94
93
 
95
94
  self.container = QWidget()
96
95
  self.container.layout = QVBoxLayout(self.container)
@@ -232,7 +231,14 @@ class SidePanel(QWidget):
232
231
  self.stack_widget.setCurrentIndex(idx)
233
232
  self.current_index = idx
234
233
 
235
- def add_menu(self, action_id: str, icon_name: str, tooltip: str, widget: QWidget, title: str):
234
+ def add_menu(
235
+ self,
236
+ action_id: str,
237
+ icon_name: str,
238
+ tooltip: str,
239
+ widget: QWidget,
240
+ title: str | None = None,
241
+ ):
236
242
  """
237
243
  Add a menu to the side panel.
238
244
 
@@ -249,9 +255,10 @@ class SidePanel(QWidget):
249
255
  container_layout.setContentsMargins(0, 0, 0, 0)
250
256
  container_layout.setSpacing(5)
251
257
 
252
- title_label = QLabel(f"<b>{title}</b>")
253
- title_label.setStyleSheet("font-size: 16px;")
254
- container_layout.addWidget(title_label)
258
+ if title is not None:
259
+ title_label = QLabel(f"<b>{title}</b>")
260
+ title_label.setStyleSheet("font-size: 16px;")
261
+ container_layout.addWidget(title_label)
255
262
 
256
263
  # Create a QScrollArea for the actual widget to ensure scrolling if the widget inside is too large
257
264
  scroll_area = QScrollArea()
@@ -317,9 +324,9 @@ class ExampleApp(QMainWindow): # pragma: no cover
317
324
  self.side_panel = SidePanel(self, orientation="left", panel_max_width=250)
318
325
  self.layout.addWidget(self.side_panel)
319
326
 
320
- from bec_widgets.widgets.plots.waveform.waveform_widget import BECWaveformWidget
327
+ from bec_widgets.widgets.plots.waveform.waveform import Waveform
321
328
 
322
- self.plot = BECWaveformWidget()
329
+ self.plot = Waveform()
323
330
  self.layout.addWidget(self.plot)
324
331
 
325
332
  self.add_side_menus()
@@ -118,7 +118,7 @@ class IconAction(ToolBarAction):
118
118
  def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
119
119
  icon = QIcon()
120
120
  icon.addFile(self.icon_path, size=QSize(20, 20))
121
- self.action = QAction(icon, self.tooltip, target)
121
+ self.action = QAction(icon=icon, text=self.tooltip, parent=target)
122
122
  self.action.setCheckable(self.checkable)
123
123
  toolbar.addAction(self.action)
124
124
 
@@ -128,7 +128,7 @@ class QtIconAction(ToolBarAction):
128
128
  super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable)
129
129
  self.standard_icon = standard_icon
130
130
  self.icon = QApplication.style().standardIcon(standard_icon)
131
- self.action = QAction(self.icon, self.tooltip, parent)
131
+ self.action = QAction(icon=self.icon, text=self.tooltip, parent=parent)
132
132
  self.action.setCheckable(self.checkable)
133
133
 
134
134
  def add_to_toolbar(self, toolbar, target):
@@ -173,7 +173,7 @@ class MaterialIconAction(ToolBarAction):
173
173
  filled=self.filled,
174
174
  color=self.color,
175
175
  )
176
- self.action = QAction(self.icon, self.tooltip, parent=parent)
176
+ self.action = QAction(icon=self.icon, text=self.tooltip, parent=parent)
177
177
  self.action.setCheckable(self.checkable)
178
178
 
179
179
  def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
@@ -212,12 +212,12 @@ class DeviceSelectionAction(ToolBarAction):
212
212
  self.device_combobox.currentIndexChanged.connect(lambda: self.set_combobox_style("#ffa700"))
213
213
 
214
214
  def add_to_toolbar(self, toolbar, target):
215
- widget = QWidget()
215
+ widget = QWidget(parent=target)
216
216
  layout = QHBoxLayout(widget)
217
217
  layout.setContentsMargins(0, 0, 0, 0)
218
218
  layout.setSpacing(0)
219
219
  if self.label is not None:
220
- label = QLabel(f"{self.label}")
220
+ label = QLabel(text=f"{self.label}", parent=target)
221
221
  layout.addWidget(label)
222
222
  if self.device_combobox is not None:
223
223
  layout.addWidget(self.device_combobox)
@@ -279,31 +279,63 @@ class SwitchableToolBarAction(ToolBarAction):
279
279
  self.main_button.setToolTip(default_action.tooltip)
280
280
  self.main_button.clicked.connect(self._trigger_current_action)
281
281
  menu = QMenu(self.main_button)
282
- self.menu_actions = {}
283
282
  for key, action_obj in self.actions.items():
284
- menu_action = QAction(action_obj.get_icon(), action_obj.tooltip, self.main_button)
283
+ menu_action = QAction(
284
+ icon=action_obj.get_icon(), text=action_obj.tooltip, parent=self.main_button
285
+ )
285
286
  menu_action.setIconVisibleInMenu(True)
286
287
  menu_action.setCheckable(self.checkable)
287
288
  menu_action.setChecked(key == self.current_key)
288
289
  menu_action.triggered.connect(lambda checked, k=key: self.set_default_action(k))
289
290
  menu.addAction(menu_action)
290
- self.menu_actions[key] = menu_action
291
291
  self.main_button.setMenu(menu)
292
292
  toolbar.addWidget(self.main_button)
293
293
 
294
294
  def _trigger_current_action(self):
295
+ """
296
+ Triggers the current action associated with the main button.
297
+ """
295
298
  action_obj = self.actions[self.current_key]
296
299
  action_obj.action.trigger()
297
300
 
298
301
  def set_default_action(self, key: str):
302
+ """
303
+ Sets the default action for the split action.
304
+
305
+ Args:
306
+ key(str): The key of the action to set as default.
307
+ """
299
308
  self.current_key = key
300
309
  new_action = self.actions[self.current_key]
301
310
  self.main_button.setIcon(new_action.get_icon())
302
311
  self.main_button.setToolTip(new_action.tooltip)
303
312
  # Update check state of menu items
304
- for k, menu_act in self.menu_actions.items():
305
- menu_act.setChecked(k == key)
313
+ for k, menu_act in self.actions.items():
314
+ menu_act.action.setChecked(False)
306
315
  new_action.action.trigger()
316
+ # Active action chosen from menu is always checked, uncheck through main button
317
+ if self.checkable:
318
+ new_action.action.setChecked(True)
319
+ self.main_button.setChecked(True)
320
+
321
+ def block_all_signals(self, block: bool = True):
322
+ """
323
+ Blocks or unblocks all signals for the actions in the toolbar.
324
+
325
+ Args:
326
+ block (bool): Whether to block signals. Defaults to True.
327
+ """
328
+ self.main_button.blockSignals(block)
329
+ for action in self.actions.values():
330
+ action.action.blockSignals(block)
331
+
332
+ def set_state_all(self, state: bool):
333
+ """
334
+ Uncheck all actions in the toolbar.
335
+ """
336
+ for action in self.actions.values():
337
+ action.action.setChecked(state)
338
+ self.main_button.setChecked(state)
307
339
 
308
340
  def get_icon(self) -> QIcon:
309
341
  return self.actions[self.current_key].get_icon()
@@ -318,11 +350,18 @@ class WidgetAction(ToolBarAction):
318
350
  widget (QWidget): The widget to be added to the toolbar.
319
351
  """
320
352
 
321
- def __init__(self, label: str | None = None, widget: QWidget = None, parent=None):
353
+ def __init__(
354
+ self,
355
+ label: str | None = None,
356
+ widget: QWidget = None,
357
+ adjust_size: bool = True,
358
+ parent=None,
359
+ ):
322
360
  super().__init__(icon_path=None, tooltip=label, checkable=False)
323
361
  self.label = label
324
362
  self.widget = widget
325
363
  self.container = None
364
+ self.adjust_size = adjust_size
326
365
 
327
366
  def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
328
367
  """
@@ -332,18 +371,18 @@ class WidgetAction(ToolBarAction):
332
371
  toolbar (QToolBar): The toolbar to add the widget to.
333
372
  target (QWidget): The target widget for the action.
334
373
  """
335
- self.container = QWidget()
374
+ self.container = QWidget(parent=target)
336
375
  layout = QHBoxLayout(self.container)
337
376
  layout.setContentsMargins(0, 0, 0, 0)
338
377
  layout.setSpacing(0)
339
378
 
340
379
  if self.label is not None:
341
- label_widget = QLabel(f"{self.label}")
380
+ label_widget = QLabel(text=f"{self.label}", parent=target)
342
381
  label_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
343
382
  label_widget.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
344
383
  layout.addWidget(label_widget)
345
384
 
346
- if isinstance(self.widget, QComboBox):
385
+ if isinstance(self.widget, QComboBox) and self.adjust_size:
347
386
  self.widget.setSizeAdjustPolicy(QComboBox.AdjustToContents)
348
387
 
349
388
  size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
@@ -400,7 +439,7 @@ class ExpandableMenuAction(ToolBarAction):
400
439
  )
401
440
  menu = QMenu(button)
402
441
  for action_id, action in self.actions.items():
403
- sub_action = QAction(action.tooltip, target)
442
+ sub_action = QAction(text=action.tooltip, parent=target)
404
443
  sub_action.setIconVisibleInMenu(True)
405
444
  if action.icon_path:
406
445
  icon = QIcon()
@@ -484,7 +523,7 @@ class ModularToolBar(QToolBar):
484
523
  orientation: Literal["horizontal", "vertical"] = "horizontal",
485
524
  background_color: str = "rgba(0, 0, 0, 0)",
486
525
  ):
487
- super().__init__(parent)
526
+ super().__init__(parent=parent)
488
527
 
489
528
  self.widgets = defaultdict(dict)
490
529
  self.background_color = background_color
@@ -821,13 +860,13 @@ class MainWindow(QMainWindow): # pragma: no cover
821
860
 
822
861
  # For theme testing
823
862
 
824
- self.dark_button = DarkModeButton(toolbar=True)
863
+ self.dark_button = DarkModeButton(parent=self, toolbar=True)
825
864
  dark_mode_action = WidgetAction(label=None, widget=self.dark_button)
826
865
  self.toolbar.add_action("dark_mode", dark_mode_action, self)
827
866
 
828
867
  def add_bundles(self):
829
868
  home_action = MaterialIconAction(
830
- icon_name="home", tooltip="Home", checkable=True, parent=self
869
+ icon_name="home", tooltip="Home", checkable=False, parent=self
831
870
  )
832
871
  settings_action = MaterialIconAction(
833
872
  icon_name="settings", tooltip="Settings", checkable=True, parent=self
@@ -844,6 +883,7 @@ class MainWindow(QMainWindow): # pragma: no cover
844
883
  ],
845
884
  )
846
885
  self.toolbar.add_bundle(main_actions_bundle, target_widget=self)
886
+ home_action.action.triggered.connect(lambda: self.switchable_action.set_state_all(False))
847
887
 
848
888
  search_action = MaterialIconAction(
849
889
  icon_name="search", tooltip="Search", checkable=False, parent=self
@@ -897,20 +937,20 @@ class MainWindow(QMainWindow): # pragma: no cover
897
937
 
898
938
  def add_switchable_button_checkable(self):
899
939
  action1 = MaterialIconAction(
900
- icon_name="counter_1", tooltip="Action 1", checkable=True, parent=self
940
+ icon_name="hdr_auto", tooltip="Action 1", checkable=True, parent=self
901
941
  )
902
942
  action2 = MaterialIconAction(
903
- icon_name="counter_2", tooltip="Action 2", checkable=True, parent=self
943
+ icon_name="hdr_auto", tooltip="Action 2", checkable=True, filled=True, parent=self
904
944
  )
905
945
 
906
- switchable_action = SwitchableToolBarAction(
946
+ self.switchable_action = SwitchableToolBarAction(
907
947
  actions={"action1": action1, "action2": action2},
908
948
  initial_action="action1",
909
949
  tooltip="Switchable Action",
910
950
  checkable=True,
911
951
  parent=self,
912
952
  )
913
- self.toolbar.add_action("switchable_action", switchable_action, self)
953
+ self.toolbar.add_action("switchable_action", self.switchable_action, self)
914
954
 
915
955
  action1.action.toggled.connect(
916
956
  lambda checked: self.test_label.setText(f"Action 1 triggered, checked = {checked}")
@@ -931,16 +971,20 @@ class MainWindow(QMainWindow): # pragma: no cover
931
971
  actions={"action1": action1, "action2": action2},
932
972
  initial_action="action1",
933
973
  tooltip="Switchable Action",
934
- checkable=True,
974
+ checkable=False,
935
975
  parent=self,
936
976
  )
937
977
  self.toolbar.add_action("switchable_action_no_toggle", switchable_action, self)
938
978
 
939
979
  action1.action.triggered.connect(
940
- lambda checked: self.test_label.setText(f"Action 1 triggered, checked = {checked}")
980
+ lambda checked: self.test_label.setText(
981
+ f"Action 1 (non-checkable) triggered, checked = {checked}"
982
+ )
941
983
  )
942
984
  action2.action.triggered.connect(
943
- lambda checked: self.test_label.setText(f"Action 2 triggered, checked = {checked}")
985
+ lambda checked: self.test_label.setText(
986
+ f"Action 2 (non-checkable) triggered, checked = {checked}"
987
+ )
944
988
  )
945
989
  switchable_action.actions["action1"].action.setChecked(True)
946
990