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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. .gitlab-ci.yml +3 -5
  2. CHANGELOG.md +631 -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 +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.0.dist-info}/METADATA +3 -3
  146. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.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.0.dist-info}/WHEEL +0 -0
  195. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/entry_points.txt +0 -0
  196. {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import collections
4
+ import random
5
+ import string
4
6
  from collections.abc import Callable
5
7
  from typing import TYPE_CHECKING, Union
6
8
 
@@ -12,11 +14,15 @@ from bec_lib.service_config import ServiceConfig
12
14
  from qtpy.QtCore import QObject
13
15
  from qtpy.QtCore import Signal as pyqtSignal
14
16
 
17
+ from bec_widgets.utils.serialization import register_serializer_extension
18
+
15
19
  logger = bec_logger.logger
16
20
 
17
- if TYPE_CHECKING:
21
+ if TYPE_CHECKING: # pragma: no cover
18
22
  from bec_lib.endpoints import EndpointInfo
19
23
 
24
+ from bec_widgets.utils.rpc_server import RPCServer
25
+
20
26
 
21
27
  class QtThreadSafeCallback(QObject):
22
28
  cb_signal = pyqtSignal(dict, dict)
@@ -73,14 +79,23 @@ class BECDispatcher:
73
79
 
74
80
  _instance = None
75
81
  _initialized = False
76
-
77
- def __new__(cls, client=None, config: str = None, *args, **kwargs):
82
+ client: BECClient
83
+ cli_server: RPCServer | None = None
84
+
85
+ def __new__(
86
+ cls,
87
+ client=None,
88
+ config: str | ServiceConfig | None = None,
89
+ gui_id: str = None,
90
+ *args,
91
+ **kwargs,
92
+ ):
78
93
  if cls._instance is None:
79
94
  cls._instance = super(BECDispatcher, cls).__new__(cls)
80
95
  cls._initialized = False
81
96
  return cls._instance
82
97
 
83
- def __init__(self, client=None, config: str | ServiceConfig = None):
98
+ def __init__(self, client=None, config: str | ServiceConfig | None = None, gui_id: str = None):
84
99
  if self._initialized:
85
100
  return
86
101
 
@@ -107,11 +122,18 @@ class BECDispatcher:
107
122
  except redis.exceptions.ConnectionError:
108
123
  logger.warning("Could not connect to Redis, skipping start of BECClient.")
109
124
 
125
+ register_serializer_extension()
126
+
110
127
  logger.success("Initialized BECDispatcher")
128
+
129
+ self.start_cli_server(gui_id=gui_id)
111
130
  self._initialized = True
112
131
 
113
132
  @classmethod
114
133
  def reset_singleton(cls):
134
+ """
135
+ Reset the singleton instance of the BECDispatcher.
136
+ """
115
137
  cls._instance = None
116
138
  cls._initialized = False
117
139
 
@@ -178,4 +200,49 @@ class BECDispatcher:
178
200
  *args: Arbitrary positional arguments
179
201
  **kwargs: Arbitrary keyword arguments
180
202
  """
203
+ # pylint: disable=protected-access
181
204
  self.disconnect_topics(self.client.connector._topics_cb)
205
+
206
+ def start_cli_server(self, gui_id: str | None = None):
207
+ """
208
+ Start the CLI server.
209
+
210
+ Args:
211
+ gui_id(str, optional): The GUI ID. Defaults to None. If None, a unique identifier will be generated.
212
+ """
213
+ # pylint: disable=import-outside-toplevel
214
+ from bec_widgets.utils.rpc_server import RPCServer
215
+
216
+ if gui_id is None:
217
+ gui_id = self.generate_unique_identifier()
218
+
219
+ if not self.client.started:
220
+ logger.error("Cannot start CLI server without a running client")
221
+ return
222
+ self.cli_server = RPCServer(gui_id, dispatcher=self, client=self.client)
223
+ logger.success(f"Started CLI server with gui_id: {gui_id}")
224
+
225
+ def stop_cli_server(self):
226
+ """
227
+ Stop the CLI server.
228
+ """
229
+ if self.cli_server is None:
230
+ logger.error("Cannot stop CLI server without starting it first")
231
+ return
232
+ self.cli_server.shutdown()
233
+ self.cli_server = None
234
+ logger.success("Stopped CLI server")
235
+
236
+ @staticmethod
237
+ def generate_unique_identifier(length: int = 4) -> str:
238
+ """
239
+ Generate a unique identifier for the application.
240
+
241
+ Args:
242
+ length: The length of the identifier. Defaults to 4.
243
+
244
+ Returns:
245
+ str: The unique identifier.
246
+ """
247
+ allowed_chars = string.ascii_lowercase + string.digits
248
+ return "".join(random.choices(allowed_chars, k=length))
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.metadata
4
+ import inspect
5
+ import pkgutil
6
+ from importlib import util as importlib_util
7
+ from importlib.machinery import FileFinder, ModuleSpec, SourceFileLoader
8
+ from types import ModuleType
9
+ from typing import Generator
10
+
11
+ from bec_widgets.utils.bec_widget import BECWidget
12
+
13
+
14
+ def _submodule_specs(module: ModuleType) -> tuple[ModuleSpec | None, ...]:
15
+ """Return specs for all submodules of the given module."""
16
+ return tuple(
17
+ module_info.module_finder.find_spec(module_info.name)
18
+ for module_info in pkgutil.iter_modules(module.__path__)
19
+ if isinstance(module_info.module_finder, FileFinder)
20
+ )
21
+
22
+
23
+ def _loaded_submodules_from_specs(
24
+ submodule_specs: tuple[ModuleSpec | None, ...]
25
+ ) -> Generator[ModuleType, None, None]:
26
+ """Load all submodules from the given specs."""
27
+ for submodule in (
28
+ importlib_util.module_from_spec(spec) for spec in submodule_specs if spec is not None
29
+ ):
30
+ assert isinstance(
31
+ submodule.__loader__, SourceFileLoader
32
+ ), "Module found from FileFinder should have SourceFileLoader!"
33
+ submodule.__loader__.exec_module(submodule)
34
+ yield submodule
35
+
36
+
37
+ def _submodule_by_name(module: ModuleType, name: str):
38
+ for submod in _loaded_submodules_from_specs(_submodule_specs(module)):
39
+ if submod.__name__ == name:
40
+ return submod
41
+ return None
42
+
43
+
44
+ def _get_widgets_from_module(module: ModuleType) -> dict[str, "type[BECWidget]"]:
45
+ """Find any BECWidget subclasses in the given module and return them with their names."""
46
+ from bec_widgets.utils.bec_widget import BECWidget # avoid circular import
47
+
48
+ return dict(
49
+ inspect.getmembers(
50
+ module,
51
+ predicate=lambda item: inspect.isclass(item)
52
+ and issubclass(item, BECWidget)
53
+ and item is not BECWidget,
54
+ )
55
+ )
56
+
57
+
58
+ def _all_widgets_from_all_submods(module):
59
+ """Recursively load submodules, find any BECWidgets, and return them all as a flat dict."""
60
+ widgets = _get_widgets_from_module(module)
61
+ if not hasattr(module, "__path__"):
62
+ return widgets
63
+ for submod in _loaded_submodules_from_specs(_submodule_specs(module)):
64
+ widgets.update(_all_widgets_from_all_submods(submod))
65
+ return widgets
66
+
67
+
68
+ def user_widget_plugin() -> ModuleType | None:
69
+ plugins = importlib.metadata.entry_points(group="bec.widgets.user_widgets") # type: ignore
70
+ return None if len(plugins) == 0 else tuple(plugins)[0].load()
71
+
72
+
73
+ def get_plugin_client_module() -> ModuleType | None:
74
+ """If there is a plugin repository installed, return the client module."""
75
+ return _submodule_by_name(plugin, "client") if (plugin := user_widget_plugin()) else None
76
+
77
+
78
+ def get_all_plugin_widgets() -> dict[str, "type[BECWidget]"]:
79
+ """If there is a plugin repository installed, load all widgets from it."""
80
+ if plugin := user_widget_plugin():
81
+ return _all_widgets_from_all_submods(plugin)
82
+ else:
83
+ return {}
84
+
85
+
86
+ if __name__ == "__main__": # pragma: no cover
87
+ # print(get_all_plugin_widgets())
88
+ client = get_plugin_client_module()
89
+ ...
@@ -7,7 +7,7 @@ will allow you to decide by yourself when to unblock and execute the callback ag
7
7
  from pyqtgraph import SignalProxy
8
8
  from qtpy.QtCore import QTimer, Signal
9
9
 
10
- from bec_widgets.qt_utils.error_popups import SafeSlot
10
+ from bec_widgets.utils.error_popups import SafeSlot
11
11
 
12
12
 
13
13
  class BECSignalProxy(SignalProxy):
@@ -1,13 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  import darkdetect
4
6
  from bec_lib.logger import bec_logger
5
- from qtpy.QtCore import Slot
6
- from qtpy.QtWidgets import QApplication, QWidget
7
+ from qtpy.QtCore import QObject, Slot
8
+ from qtpy.QtWidgets import QApplication
7
9
 
10
+ from bec_widgets.cli.rpc.rpc_register import RPCRegister
8
11
  from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
9
12
  from bec_widgets.utils.colors import set_theme
10
13
 
14
+ if TYPE_CHECKING: # pragma: no cover
15
+ from bec_widgets.widgets.containers.dock import BECDock
16
+
11
17
  logger = bec_logger.logger
12
18
 
13
19
 
@@ -17,13 +23,16 @@ class BECWidget(BECConnector):
17
23
  # The icon name is the name of the icon in the icon theme, typically a name taken
18
24
  # from fonts.google.com/icons. Override this in subclasses to set the icon name.
19
25
  ICON_NAME = "widgets"
26
+ USER_ACCESS = ["remove"]
20
27
 
28
+ # pylint: disable=too-many-arguments
21
29
  def __init__(
22
30
  self,
23
31
  client=None,
24
32
  config: ConnectionConfig = None,
25
- gui_id: str = None,
33
+ gui_id: str | None = None,
26
34
  theme_update: bool = False,
35
+ parent_dock: BECDock | None = None, # TODO should go away -> issue created #473
27
36
  **kwargs,
28
37
  ):
29
38
  """
@@ -43,11 +52,12 @@ class BECWidget(BECConnector):
43
52
  theme_update(bool, optional): Whether to subscribe to theme updates. Defaults to False. When set to True, the
44
53
  widget's apply_theme method will be called when the theme changes.
45
54
  """
46
- if not isinstance(self, QWidget):
47
- raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
48
- super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
49
55
 
50
- # Set the theme to auto if it is not set yet
56
+ super().__init__(
57
+ client=client, config=config, gui_id=gui_id, parent_dock=parent_dock, **kwargs
58
+ )
59
+ if not isinstance(self, QObject):
60
+ raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
51
61
  app = QApplication.instance()
52
62
  if not hasattr(app, "theme"):
53
63
  # DO NOT SET THE THEME TO AUTO! Otherwise, the qwebengineview will segfault
@@ -88,10 +98,16 @@ class BECWidget(BECConnector):
88
98
 
89
99
  def cleanup(self):
90
100
  """Cleanup the widget."""
101
+ with RPCRegister.delayed_broadcast():
102
+ # All widgets need to call super().cleanup() in their cleanup method
103
+ logger.info(f"Registry cleanup for widget {self.__class__.__name__}")
104
+ self.rpc_register.remove_rpc(self)
91
105
 
92
106
  def closeEvent(self, event):
93
- self.rpc_register.remove_rpc(self)
107
+ """Wrap the close even to ensure the rpc_register is cleaned up."""
94
108
  try:
95
- self.cleanup()
109
+ if not self._destroyed:
110
+ self.cleanup()
111
+ self._destroyed = True
96
112
  finally:
97
- super().closeEvent(event)
113
+ super().closeEvent(event) # pylint: disable=no-member
@@ -11,7 +11,7 @@ from pydantic_core import PydanticCustomError
11
11
  from qtpy.QtGui import QColor
12
12
  from qtpy.QtWidgets import QApplication
13
13
 
14
- if TYPE_CHECKING:
14
+ if TYPE_CHECKING: # pragma: no cover
15
15
  from bec_qthemes._main import AccentColors
16
16
 
17
17
 
@@ -266,3 +266,5 @@ class CompactPopupWidget(QWidget):
266
266
  # to ensure proper resources cleanup
267
267
  for child in self.container.findChildren(QWidget, options=Qt.FindDirectChildrenOnly):
268
268
  child.close()
269
+
270
+ super().closeEvent(event)
@@ -1,30 +1,55 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import itertools
4
- from typing import Type
4
+ from typing import Literal, Type
5
5
 
6
6
  from qtpy.QtWidgets import QWidget
7
7
 
8
+ from bec_widgets.cli.rpc.rpc_register import RPCRegister
9
+
8
10
 
9
11
  class WidgetContainerUtils:
10
12
 
13
+ # We need one handler that checks if a WIDGET of a given name is already created for that DOCKAREA
14
+ # 1. If the name exists, then it depends whether the name was auto-generated -> add _1 to the name
15
+ # or alternatively raise an error that it can't be added again ( just raise an error)
16
+ # 2. Dock names in between docks should also be unique
17
+
11
18
  @staticmethod
12
- def generate_unique_widget_id(container: dict, prefix: str = "widget") -> str:
13
- """
14
- Generate a unique widget ID.
19
+ def has_name_valid_chars(name: str) -> bool:
20
+ """Check if the name is valid.
15
21
 
16
22
  Args:
17
- container(dict): The container of widgets.
18
- prefix(str): The prefix of the widget ID.
23
+ name(str): The name to be checked.
24
+
25
+ Returns:
26
+ bool: True if the name is valid, False otherwise.
27
+ """
28
+ if not name or len(name) > 256:
29
+ return False # Don't accept empty names or names longer than 256 characters
30
+ check_value = name.replace("_", "").replace("-", "")
31
+ if not check_value.isalnum() or not check_value.isascii():
32
+ return False
33
+ return True
19
34
 
35
+ @staticmethod
36
+ def generate_unique_name(name: str, list_of_names: list[str] | None = None) -> str:
37
+ """Generate a unique ID.
38
+
39
+ Args:
40
+ name(str): The name of the widget.
20
41
  Returns:
21
- widget_id(str): The unique widget ID.
42
+ tuple (str): The unique name
22
43
  """
23
- existing_ids = set(container.keys())
24
- for i in itertools.count(1):
25
- widget_id = f"{prefix}_{i}"
26
- if widget_id not in existing_ids:
27
- return widget_id
44
+ if list_of_names is None:
45
+ list_of_names = []
46
+ ii = 0
47
+ while ii < 1000: # 1000 is arbritrary!
48
+ name_candidate = f"{name}_{ii}"
49
+ if name_candidate not in list_of_names:
50
+ return name_candidate
51
+ ii += 1
52
+ raise ValueError("Could not generate a unique name after within 1000 attempts.")
28
53
 
29
54
  @staticmethod
30
55
  def find_first_widget_by_class(
@@ -1,4 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  from collections import defaultdict
4
+ from typing import Any
2
5
 
3
6
  import numpy as np
4
7
  import pyqtgraph as pg
@@ -197,15 +200,18 @@ class Crosshair(QObject):
197
200
  self.marker_2d = pg.ROI(
198
201
  [0, 0], size=[1, 1], pen=pg.mkPen("r", width=2), movable=False
199
202
  )
203
+ self.marker_2d.skip_auto_range = True
200
204
  self.plot_item.addItem(self.marker_2d)
201
205
 
202
- def snap_to_data(self, x, y) -> tuple[defaultdict[list], defaultdict[list]]:
206
+ def snap_to_data(
207
+ self, x: float, y: float
208
+ ) -> tuple[None, None] | tuple[defaultdict[Any, list], defaultdict[Any, list]]:
203
209
  """
204
210
  Finds the nearest data points to the given x and y coordinates.
205
211
 
206
212
  Args:
207
- x: The x-coordinate of the mouse cursor
208
- y: The y-coordinate of the mouse cursor
213
+ x(float): The x-coordinate of the mouse cursor
214
+ y(float): The y-coordinate of the mouse cursor
209
215
 
210
216
  Returns:
211
217
  tuple: x and y values snapped to the nearest data
@@ -235,7 +241,7 @@ class Crosshair(QObject):
235
241
  y_values[name] = closest_y
236
242
  x_values[name] = closest_x
237
243
  elif isinstance(item, pg.ImageItem): # 2D plot
238
- name = item.config.monitor
244
+ name = item.config.monitor or str(id(item))
239
245
  image_2d = item.image
240
246
  # Clip the x and y values to the image dimensions to avoid out of bounds errors
241
247
  y_values[name] = int(np.clip(y, 0, image_2d.shape[1] - 1))
@@ -320,7 +326,7 @@ class Crosshair(QObject):
320
326
  )
321
327
  self.coordinatesChanged1D.emit(coordinate_to_emit)
322
328
  elif isinstance(item, pg.ImageItem):
323
- name = item.config.monitor
329
+ name = item.config.monitor or str(id(item))
324
330
  x, y = x_snap_values[name], y_snap_values[name]
325
331
  if x is None or y is None:
326
332
  continue
@@ -374,7 +380,7 @@ class Crosshair(QObject):
374
380
  )
375
381
  self.coordinatesClicked1D.emit(coordinate_to_emit)
376
382
  elif isinstance(item, pg.ImageItem):
377
- name = item.config.monitor
383
+ name = item.config.monitor or str(id(item))
378
384
  x, y = x_snap_values[name], y_snap_values[name]
379
385
  if x is None or y is None:
380
386
  continue
@@ -418,9 +424,17 @@ class Crosshair(QObject):
418
424
  """
419
425
  x, y = pos
420
426
  x_scaled, y_scaled = self.scale_emitted_coordinates(x, y)
421
-
427
+ text = f"({x_scaled:.{self.precision}g}, {y_scaled:.{self.precision}g})"
428
+ for item in self.items:
429
+ if isinstance(item, pg.ImageItem):
430
+ image = item.image
431
+ ix = int(np.clip(x, 0, image.shape[0] - 1))
432
+ iy = int(np.clip(y, 0, image.shape[1] - 1))
433
+ intensity = image[ix, iy]
434
+ text += f"\nIntensity: {intensity:.{self.precision}g}"
435
+ break
422
436
  # Update coordinate label
423
- self.coord_label.setText(f"({x_scaled:.{self.precision}g}, {y_scaled:.{self.precision}g})")
437
+ self.coord_label.setText(text)
424
438
  self.coord_label.setPos(x, y)
425
439
  self.coord_label.setVisible(True)
426
440
 
@@ -436,6 +450,9 @@ class Crosshair(QObject):
436
450
  self.clear_markers()
437
451
 
438
452
  def cleanup(self):
453
+ if self.marker_2d is not None:
454
+ self.plot_item.removeItem(self.marker_2d)
455
+ self.marker_2d = None
439
456
  self.plot_item.removeItem(self.v_line)
440
457
  self.plot_item.removeItem(self.h_line)
441
458
  self.plot_item.removeItem(self.coord_label)
@@ -22,7 +22,9 @@ class EntryValidator:
22
22
  if entry is None or entry == "":
23
23
  entry = next(iter(device._hints), name) if hasattr(device, "_hints") else name
24
24
  if entry not in description:
25
- raise ValueError(f"Entry '{entry}' not found in device '{name}' signals")
25
+ raise ValueError(
26
+ f"Entry '{entry}' not found in device '{name}' signals. Available signals: {description.keys()}"
27
+ )
26
28
 
27
29
  return entry
28
30
 
@@ -96,15 +96,33 @@ def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name
96
96
 
97
97
  'popup_error' keyword argument can be passed with boolean value if a dialog should pop up,
98
98
  otherwise error display is left to the original exception hook
99
+ 'verify_sender' keyword argument can be passed with boolean value if the sender should be verified
100
+ before executing the slot. If True, the slot will only execute if the sender is a QObject. This is
101
+ useful to prevent function calls from already deleted objects.
99
102
  """
100
103
  popup_error = bool(slot_kwargs.pop("popup_error", False))
104
+ verify_sender = bool(slot_kwargs.pop("verify_sender", False))
101
105
 
102
106
  def error_managed(method):
103
107
  @Slot(*slot_args, **slot_kwargs)
104
108
  @functools.wraps(method)
105
109
  def wrapper(*args, **kwargs):
106
110
  try:
111
+ if not verify_sender or len(args) == 0:
112
+ return method(*args, **kwargs)
113
+
114
+ _instance = args[0]
115
+ if not isinstance(_instance, QObject):
116
+ return method(*args, **kwargs)
117
+ sender = _instance.sender()
118
+ if sender is None:
119
+ logger.info(
120
+ f"Sender is None for {method.__module__}.{method.__qualname__}, "
121
+ "skipping method call."
122
+ )
123
+ return
107
124
  return method(*args, **kwargs)
125
+
108
126
  except Exception:
109
127
  slot_name = f"{method.__module__}.{method.__qualname__}"
110
128
  error_msg = traceback.format_exc()
@@ -12,7 +12,7 @@ from qtpy.QtWidgets import (
12
12
  QWidget,
13
13
  )
14
14
 
15
- from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
15
+ from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
16
16
 
17
17
 
18
18
  class ExpandableGroupFrame(QFrame):
@@ -37,7 +37,7 @@ class ExpandableGroupFrame(QFrame):
37
37
  self._title_layout.addWidget(self._expansion_button)
38
38
  self._title_layout.addWidget(self._title)
39
39
 
40
- self._contents = QWidget()
40
+ self._contents = QWidget(self)
41
41
  self._layout.addWidget(self._contents)
42
42
 
43
43
  self._expansion_button.clicked.connect(self.switch_expanded_state)