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,16 +1,17 @@
1
- import os
2
-
3
- from qtpy import PYQT6, PYSIDE6, QT_VERSION
1
+ from bec_lib.logger import bec_logger
2
+ from qtpy import PYQT6, PYSIDE6
4
3
  from qtpy.QtCore import QFile, QIODevice
5
4
 
6
5
  from bec_widgets.utils.generate_designer_plugin import DesignerPluginInfo
7
6
  from bec_widgets.utils.plugin_utils import get_custom_classes
8
7
 
8
+ logger = bec_logger.logger
9
+
9
10
  if PYSIDE6:
10
11
  from PySide6.QtUiTools import QUiLoader
11
12
 
12
13
  class CustomUiLoader(QUiLoader):
13
- def __init__(self, baseinstance, custom_widgets: dict = None):
14
+ def __init__(self, baseinstance, custom_widgets: dict | None = None):
14
15
  super().__init__(baseinstance)
15
16
  self.custom_widgets = custom_widgets or {}
16
17
 
@@ -18,10 +19,9 @@ if PYSIDE6:
18
19
 
19
20
  def createWidget(self, class_name, parent=None, name=""):
20
21
  if class_name in self.custom_widgets:
21
- widget = self.custom_widgets[class_name](parent)
22
- widget.setObjectName(name)
22
+ widget = self.custom_widgets[class_name](self.baseinstance)
23
23
  return widget
24
- return super().createWidget(class_name, parent, name)
24
+ return super().createWidget(class_name, self.baseinstance, name)
25
25
 
26
26
 
27
27
  class UILoader:
@@ -51,7 +51,7 @@ class UILoader:
51
51
  Returns:
52
52
  QWidget: The loaded widget.
53
53
  """
54
-
54
+ parent = parent or self.parent
55
55
  loader = CustomUiLoader(parent, self.custom_widgets)
56
56
  file = QFile(ui_file)
57
57
  if not file.open(QIODevice.ReadOnly):
@@ -1,6 +1,9 @@
1
1
  # pylint: disable=no-name-in-module
2
+ from __future__ import annotations
3
+
2
4
  from abc import ABC, abstractmethod
3
5
 
6
+ import shiboken6 as shb
4
7
  from qtpy.QtWidgets import (
5
8
  QApplication,
6
9
  QCheckBox,
@@ -8,6 +11,7 @@ from qtpy.QtWidgets import (
8
11
  QDoubleSpinBox,
9
12
  QLabel,
10
13
  QLineEdit,
14
+ QSlider,
11
15
  QSpinBox,
12
16
  QTableWidget,
13
17
  QTableWidgetItem,
@@ -104,10 +108,10 @@ class TableWidgetHandler(WidgetHandler):
104
108
  class SpinBoxHandler(WidgetHandler):
105
109
  """Handler for QSpinBox and QDoubleSpinBox widgets."""
106
110
 
107
- def get_value(self, widget, **kwargs):
111
+ def get_value(self, widget: QSpinBox | QDoubleSpinBox, **kwargs):
108
112
  return widget.value()
109
113
 
110
- def set_value(self, widget, value):
114
+ def set_value(self, widget: QSpinBox | QDoubleSpinBox, value):
111
115
  widget.setValue(value)
112
116
 
113
117
  def connect_change_signal(self, widget: QSpinBox | QDoubleSpinBox, slot):
@@ -117,23 +121,36 @@ class SpinBoxHandler(WidgetHandler):
117
121
  class CheckBoxHandler(WidgetHandler):
118
122
  """Handler for QCheckBox widgets."""
119
123
 
120
- def get_value(self, widget, **kwargs):
124
+ def get_value(self, widget: QCheckBox, **kwargs):
121
125
  return widget.isChecked()
122
126
 
123
- def set_value(self, widget, value):
127
+ def set_value(self, widget: QCheckBox, value):
124
128
  widget.setChecked(value)
125
129
 
126
130
  def connect_change_signal(self, widget: QCheckBox, slot):
127
131
  widget.toggled.connect(lambda val, w=widget: slot(w, val))
128
132
 
129
133
 
134
+ class SlideHandler(WidgetHandler):
135
+ """Handler for QCheckBox widgets."""
136
+
137
+ def get_value(self, widget: QSlider, **kwargs):
138
+ return widget.value()
139
+
140
+ def set_value(self, widget: QSlider, value):
141
+ widget.setValue(value)
142
+
143
+ def connect_change_signal(self, widget: QSlider, slot):
144
+ widget.valueChanged.connect(lambda val, w=widget: slot(w, val))
145
+
146
+
130
147
  class ToggleSwitchHandler(WidgetHandler):
131
148
  """Handler for ToggleSwitch widgets."""
132
149
 
133
- def get_value(self, widget, **kwargs):
150
+ def get_value(self, widget: ToggleSwitch, **kwargs):
134
151
  return widget.checked
135
152
 
136
- def set_value(self, widget, value):
153
+ def set_value(self, widget: ToggleSwitch, value):
137
154
  widget.checked = value
138
155
 
139
156
  def connect_change_signal(self, widget: ToggleSwitch, slot):
@@ -143,7 +160,7 @@ class ToggleSwitchHandler(WidgetHandler):
143
160
  class LabelHandler(WidgetHandler):
144
161
  """Handler for QLabel widgets."""
145
162
 
146
- def get_value(self, widget, **kwargs):
163
+ def get_value(self, widget: QLabel, **kwargs):
147
164
  return widget.text()
148
165
 
149
166
  def set_value(self, widget: QLabel, value):
@@ -165,6 +182,7 @@ class WidgetIO:
165
182
  QCheckBox: CheckBoxHandler,
166
183
  QLabel: LabelHandler,
167
184
  ToggleSwitch: ToggleSwitchHandler,
185
+ QSlider: SlideHandler,
168
186
  }
169
187
 
170
188
  @staticmethod
@@ -258,39 +276,162 @@ class WidgetHierarchy:
258
276
  grab_values: bool = False,
259
277
  prefix: str = "",
260
278
  exclude_internal_widgets: bool = True,
279
+ only_bec_widgets: bool = False,
280
+ show_parent: bool = True,
261
281
  ) -> None:
262
282
  """
263
283
  Print the widget hierarchy to the console.
264
284
 
265
285
  Args:
266
- widget: Widget to print the hierarchy of
286
+ widget: Widget to print the hierarchy of.
267
287
  indent(int, optional): Level of indentation.
268
288
  grab_values(bool,optional): Whether to grab the values of the widgets.
269
- prefix(stc,optional): Custom string prefix for indentation.
289
+ prefix(str,optional): Custom string prefix for indentation.
270
290
  exclude_internal_widgets(bool,optional): Whether to exclude internal widgets (e.g. QComboBox in PyQt6).
291
+ only_bec_widgets(bool, optional): Whether to print only widgets that are instances of BECWidget.
292
+ show_parent(bool, optional): Whether to display which BECWidget is the parent of each discovered BECWidget.
271
293
  """
272
- widget_info = f"{widget.__class__.__name__} ({widget.objectName()})"
273
- if grab_values:
274
- value = WidgetIO.get_value(widget, ignore_errors=True)
275
- value_str = f" [value: {value}]" if value is not None else ""
276
- widget_info += value_str
277
-
294
+ from bec_widgets.utils import BECConnector
295
+ from bec_widgets.widgets.plots.waveform.waveform import Waveform
296
+
297
+ # 1) Filter out widgets that are not BECConnectors (if 'only_bec_widgets' is True)
298
+ is_bec = isinstance(widget, BECConnector)
299
+ if only_bec_widgets and not is_bec:
300
+ return
301
+
302
+ # 2) Determine and print the parent's info (closest BECConnector)
303
+ parent_info = ""
304
+ if show_parent and is_bec:
305
+ ancestor = WidgetHierarchy._get_becwidget_ancestor(widget)
306
+ if ancestor:
307
+ parent_label = ancestor.objectName() or ancestor.__class__.__name__
308
+ parent_info = f" parent={parent_label}"
309
+ else:
310
+ parent_info = " parent=None"
311
+
312
+ widget_info = f"{widget.__class__.__name__} ({widget.objectName()}){parent_info}"
278
313
  print(prefix + widget_info)
279
314
 
280
- children = widget.children()
281
- for child in children:
282
- if (
283
- exclude_internal_widgets
284
- and isinstance(widget, QComboBox)
285
- and child.__class__.__name__ in ["QFrame", "QBoxLayout", "QListView"]
286
- ):
315
+ # 3) If it's a Waveform, explicitly print the curves
316
+ if isinstance(widget, Waveform):
317
+ for curve in widget.curves:
318
+ curve_prefix = prefix + " └─ "
319
+ print(
320
+ f"{curve_prefix}{curve.__class__.__name__} ({curve.objectName()}) "
321
+ f"parent={widget.objectName()}"
322
+ )
323
+
324
+ # 4) Recursively handle each child if:
325
+ # - It's a QWidget
326
+ # - It is a BECConnector (or we don't care about filtering)
327
+ # - Its closest BECConnector parent is the current widget
328
+ for child in widget.findChildren(QWidget):
329
+ if only_bec_widgets and not isinstance(child, BECConnector):
287
330
  continue
288
- child_prefix = prefix + " "
289
- arrow = "├─ " if child != children[-1] else "└─ "
331
+
332
+ # if WidgetHierarchy._get_becwidget_ancestor(child) == widget:
333
+ child_prefix = prefix + " └─ "
290
334
  WidgetHierarchy.print_widget_hierarchy(
291
- child, indent + 1, grab_values, prefix=child_prefix + arrow
335
+ child,
336
+ indent=indent + 1,
337
+ grab_values=grab_values,
338
+ prefix=child_prefix,
339
+ exclude_internal_widgets=exclude_internal_widgets,
340
+ only_bec_widgets=only_bec_widgets,
341
+ show_parent=show_parent,
292
342
  )
293
343
 
344
+ @staticmethod
345
+ def print_becconnector_hierarchy_from_app():
346
+ """
347
+ Enumerate ALL BECConnector objects in the QApplication.
348
+ Also detect if a widget is a PlotBase, and add any data items
349
+ (PlotDataItem-like) that are also BECConnector objects.
350
+
351
+ Build a parent->children graph where each child's 'parent'
352
+ is its closest BECConnector ancestor. Print the entire hierarchy
353
+ from the root(s).
354
+
355
+ The result is a single, consolidated tree for your entire
356
+ running GUI, including PlotBase data items that are BECConnector.
357
+ """
358
+ import sys
359
+ from collections import defaultdict
360
+
361
+ from qtpy.QtWidgets import QApplication
362
+
363
+ from bec_widgets.utils import BECConnector
364
+ from bec_widgets.widgets.plots.plot_base import PlotBase
365
+
366
+ # 1) Gather ALL QWidget-based BECConnector objects
367
+ all_qwidgets = QApplication.allWidgets()
368
+ bec_widgets = set(w for w in all_qwidgets if isinstance(w, BECConnector))
369
+
370
+ # 2) Also gather any BECConnector-based data items from PlotBase widgets
371
+ for w in all_qwidgets:
372
+ if isinstance(w, PlotBase) and hasattr(w, "plot_item"):
373
+ plot_item = w.plot_item
374
+ if hasattr(plot_item, "listDataItems"):
375
+ for data_item in plot_item.listDataItems():
376
+ if isinstance(data_item, BECConnector):
377
+ bec_widgets.add(data_item)
378
+
379
+ # 3) Build a map of (closest BECConnector parent) -> list of children
380
+ parent_map = defaultdict(list)
381
+ for w in bec_widgets:
382
+ parent_bec = WidgetHierarchy._get_becwidget_ancestor(w)
383
+ parent_map[parent_bec].append(w)
384
+
385
+ # 4) Define a recursive printer to show each object's children
386
+ def print_tree(parent, prefix=""):
387
+ children = parent_map[parent]
388
+ for i, child in enumerate(children):
389
+ connector_class = child.__class__.__name__
390
+ connector_name = child.objectName() or connector_class
391
+
392
+ if parent is None:
393
+ parent_label = "None"
394
+ else:
395
+ parent_label = parent.objectName() or parent.__class__.__name__
396
+
397
+ line = f"{connector_class} ({connector_name}) parent={parent_label}"
398
+ # Determine tree-branch symbols
399
+ is_last = i == len(children) - 1
400
+ branch_str = "└─ " if is_last else "├─ "
401
+ print(prefix + branch_str + line)
402
+
403
+ # Recurse deeper
404
+ next_prefix = prefix + (" " if is_last else "│ ")
405
+ print_tree(child, prefix=next_prefix)
406
+
407
+ # 5) Print top-level items (roots) whose BECConnector parent is None
408
+ roots = parent_map[None]
409
+ for r_i, root in enumerate(roots):
410
+ root_class = root.__class__.__name__
411
+ root_name = root.objectName() or root_class
412
+ line = f"{root_class} ({root_name}) parent=None"
413
+ is_last_root = r_i == len(roots) - 1
414
+ print(line)
415
+ # Recurse into its children
416
+ print_tree(root, prefix=" ")
417
+
418
+ @staticmethod
419
+ def _get_becwidget_ancestor(widget):
420
+ """
421
+ Traverse up the parent chain to find the nearest BECConnector.
422
+ Returns None if none is found.
423
+ """
424
+ from bec_widgets.utils import BECConnector
425
+
426
+ if not shb.isValid(widget):
427
+ return None
428
+ parent = widget.parent()
429
+ while parent is not None:
430
+ if isinstance(parent, BECConnector):
431
+ return parent
432
+ parent = parent.parent()
433
+ return None
434
+
294
435
  @staticmethod
295
436
  def export_config_to_dict(
296
437
  widget: QWidget,
@@ -0,0 +1,364 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Literal, overload
4
+
5
+ from bec_lib.endpoints import MessageEndpoints
6
+ from bec_lib.logger import bec_logger
7
+ from bec_lib.messages import ScanStatusMessage
8
+
9
+ from bec_widgets.utils.error_popups import SafeSlot
10
+ from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
11
+ from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
12
+
13
+ if TYPE_CHECKING: # pragma: no cover
14
+ from bec_widgets.utils.bec_widget import BECWidget
15
+ from bec_widgets.widgets.containers.dock.dock import BECDock
16
+ from bec_widgets.widgets.plots.image.image import Image
17
+ from bec_widgets.widgets.plots.motor_map.motor_map import MotorMap
18
+ from bec_widgets.widgets.plots.multi_waveform.multi_waveform import MultiWaveform
19
+ from bec_widgets.widgets.plots.scatter_waveform.scatter_waveform import ScatterWaveform
20
+ from bec_widgets.widgets.plots.waveform.waveform import Waveform
21
+
22
+
23
+ logger = bec_logger.logger
24
+
25
+
26
+ class AutoUpdates(BECMainWindow):
27
+ _default_dock: BECDock
28
+ USER_ACCESS = ["enabled", "enabled.setter", "selected_device", "selected_device.setter"]
29
+ RPC = True
30
+
31
+ # enforce that subclasses have the same rpc widget class
32
+ rpc_widget_class = "AutoUpdates"
33
+
34
+ def __init__(
35
+ self, parent=None, gui_id: str = None, window_title="Auto Update", *args, **kwargs
36
+ ):
37
+ super().__init__(parent=parent, gui_id=gui_id, window_title=window_title, **kwargs)
38
+
39
+ self.dock_area = BECDockArea(parent=self, object_name="dock_area")
40
+ self.setCentralWidget(self.dock_area)
41
+ self._auto_update_selected_device: str | None = None
42
+
43
+ self._default_dock = None # type:ignore
44
+ self.current_widget: BECWidget | None = None
45
+ self.dock_name = None
46
+ self._enabled = True
47
+ self.start_auto_update()
48
+
49
+ def start_auto_update(self):
50
+ """
51
+ Establish all connections for the auto updates.
52
+ """
53
+ self.bec_dispatcher.connect_slot(self._on_scan_status, MessageEndpoints.scan_status())
54
+
55
+ def stop_auto_update(self):
56
+ """
57
+ Disconnect all connections for the auto updates.
58
+ """
59
+ self.bec_dispatcher.disconnect_slot(
60
+ self._on_scan_status, MessageEndpoints.scan_status() # type:ignore
61
+ )
62
+
63
+ @property
64
+ def selected_device(self) -> str | None:
65
+ """
66
+ Get the selected device from the auto update config.
67
+
68
+ Returns:
69
+ str: The selected device. If no device is selected, None is returned.
70
+ """
71
+ return self._auto_update_selected_device
72
+
73
+ @selected_device.setter
74
+ def selected_device(self, value: str | None) -> None:
75
+ """
76
+ Set the selected device in the auto update config.
77
+
78
+ Args:
79
+ value(str): The selected device.
80
+ """
81
+ self._auto_update_selected_device = value
82
+
83
+ @SafeSlot()
84
+ def _on_scan_status(self, content: dict, metadata: dict) -> None:
85
+ """
86
+ Callback for scan status messages.
87
+ """
88
+ msg = ScanStatusMessage(**content, metadata=metadata)
89
+ if not self.enabled:
90
+ return
91
+
92
+ self.enable_gui_highlights(True)
93
+
94
+ match msg.status:
95
+ case "open":
96
+ self.on_scan_open(msg)
97
+ case "closed":
98
+ self.on_scan_closed(msg)
99
+ case ["aborted", "halted"]:
100
+ self.on_scan_abort(msg)
101
+ case _:
102
+ pass
103
+
104
+ def start_default_dock(self):
105
+ """
106
+ Create a default dock for the auto updates.
107
+ """
108
+ self.dock_name = "update_dock"
109
+ self._default_dock = self.dock_area.new(self.dock_name)
110
+ self.current_widget = self._default_dock.new("Waveform")
111
+
112
+ @overload
113
+ def set_dock_to_widget(self, widget: Literal["Waveform"]) -> Waveform: ...
114
+
115
+ @overload
116
+ def set_dock_to_widget(self, widget: Literal["Image"]) -> Image: ...
117
+
118
+ @overload
119
+ def set_dock_to_widget(self, widget: Literal["ScatterWaveform"]) -> ScatterWaveform: ...
120
+
121
+ @overload
122
+ def set_dock_to_widget(self, widget: Literal["MotorMap"]) -> MotorMap: ...
123
+
124
+ @overload
125
+ def set_dock_to_widget(self, widget: Literal["MultiWaveform"]) -> MultiWaveform: ...
126
+
127
+ def set_dock_to_widget(
128
+ self,
129
+ widget: Literal["Waveform", "Image", "ScatterWaveform", "MotorMap", "MultiWaveForm"] | str,
130
+ ) -> BECWidget:
131
+ """
132
+ Set the dock to the widget.
133
+
134
+ Args:
135
+ widget (str): The widget to set the dock to. Must be the name of a valid widget class.
136
+
137
+ Returns:
138
+ BECWidget: The widget that was set.
139
+ """
140
+ if self._default_dock is None or self.current_widget is None:
141
+ logger.warning(
142
+ f"Auto Updates: No default dock found. Creating a new one with name {self.dock_name}"
143
+ )
144
+ self.start_default_dock()
145
+ assert self.current_widget is not None
146
+
147
+ if not self.current_widget.__class__.__name__ == widget:
148
+ self._default_dock.delete(self.current_widget.object_name)
149
+ self.current_widget = self._default_dock.new(widget)
150
+ return self.current_widget
151
+
152
+ def get_selected_device(
153
+ self, monitored_devices, selected_device: str | None = None
154
+ ) -> str | None:
155
+ """
156
+ Get the selected device for the plot. If no device is selected, the first
157
+ device in the monitored devices list is selected.
158
+ """
159
+
160
+ if selected_device is None:
161
+ selected_device = self.selected_device
162
+ if selected_device:
163
+ return selected_device
164
+ if len(monitored_devices) > 0:
165
+ sel_device = monitored_devices[0]
166
+ return sel_device
167
+ return None
168
+
169
+ def enable_gui_highlights(self, enable: bool) -> None:
170
+ """
171
+ Enable or disable GUI highlights.
172
+
173
+ Args:
174
+ enable (bool): Whether to enable or disable the highlights.
175
+ """
176
+ if enable:
177
+ title = self.dock_area.window().windowTitle()
178
+ if " [Auto Updates]" in title:
179
+ return
180
+ self.dock_area.window().setWindowTitle(f"{title} [Auto Updates]")
181
+ else:
182
+ title = self.dock_area.window().windowTitle()
183
+ self.dock_area.window().setWindowTitle(title.replace(" [Auto Updates]", ""))
184
+
185
+ @property
186
+ def enabled(self) -> bool:
187
+ """
188
+ Get the enabled status of the auto updates.
189
+ """
190
+ return self._enabled
191
+
192
+ @enabled.setter
193
+ def enabled(self, value: bool) -> None:
194
+ """
195
+ Set the enabled status of the auto updates.
196
+ """
197
+ if self._enabled == value:
198
+ return
199
+ self._enabled = value
200
+
201
+ if value:
202
+ self.start_auto_update()
203
+ self.enable_gui_highlights(True)
204
+ self.on_start()
205
+ else:
206
+ self.stop_auto_update()
207
+ self.enable_gui_highlights(False)
208
+ self.on_stop()
209
+
210
+ def cleanup(self) -> None:
211
+ """
212
+ Cleanup procedure to run when the auto updates are disabled.
213
+ """
214
+ self.enabled = False
215
+ self.stop_auto_update()
216
+ self.dock_area.close()
217
+ self.dock_area.deleteLater()
218
+ self.dock_area = None
219
+ super().cleanup()
220
+
221
+ ########################################################################
222
+ ################# Update Functions #####################################
223
+ ########################################################################
224
+
225
+ def simple_line_scan(self, info: ScanStatusMessage) -> None:
226
+ """
227
+ Simple line scan.
228
+
229
+ Args:
230
+ info (ScanStatusMessage): The scan status message.
231
+ """
232
+
233
+ # Set the dock to the waveform widget
234
+ wf = self.set_dock_to_widget("Waveform")
235
+
236
+ # Get the scan report devices reported by the scan
237
+ dev_x = info.scan_report_devices[0] # type:ignore
238
+
239
+ # For the y axis, get the selected device
240
+ dev_y = self.get_selected_device(info.readout_priority["monitored"]) # type:ignore
241
+ if not dev_y:
242
+ return
243
+
244
+ # Clear the waveform widget and plot the data
245
+ # with the scan number and device names
246
+ # as the label and title
247
+ wf.clear_all()
248
+ wf.plot(
249
+ x_name=dev_x,
250
+ y_name=dev_y,
251
+ label=f"Scan {info.scan_number} - {dev_y}",
252
+ title=f"Scan {info.scan_number}",
253
+ x_label=dev_x,
254
+ y_label=dev_y,
255
+ )
256
+
257
+ logger.info(
258
+ f"Auto Update [simple_line_scan]: Started plot with: x_name={dev_x}, y_name={dev_y}"
259
+ )
260
+
261
+ def simple_grid_scan(self, info: ScanStatusMessage) -> None:
262
+ """
263
+ Simple grid scan.
264
+
265
+ Args:
266
+ info (ScanStatusMessage): The scan status message.
267
+ """
268
+ # Set the dock to the scatter waveform widget
269
+ scatter = self.set_dock_to_widget("ScatterWaveform")
270
+
271
+ # Get the scan report devices reported by the scan
272
+ dev_x, dev_y = info.scan_report_devices[0], info.scan_report_devices[1] # type:ignore
273
+ dev_z = self.get_selected_device(info.readout_priority["monitored"]) # type:ignore
274
+
275
+ if None in (dev_x, dev_y, dev_z):
276
+ return
277
+
278
+ # Clear the scatter waveform widget and plot the data
279
+ scatter.clear_all()
280
+ scatter.plot(
281
+ x_name=dev_x, y_name=dev_y, z_name=dev_z, label=f"Scan {info.scan_number} - {dev_z}"
282
+ )
283
+
284
+ logger.info(
285
+ f"Auto Update [simple_grid_scan]: Started plot with: x_name={dev_x}, y_name={dev_y}, z_name={dev_z}"
286
+ )
287
+
288
+ def best_effort(self, info: ScanStatusMessage) -> None:
289
+ """
290
+ Best effort scan.
291
+
292
+ Args:
293
+ info (ScanStatusMessage): The scan status message.
294
+ """
295
+
296
+ # If the scan report devices are empty, there is nothing we can do
297
+ if not info.scan_report_devices:
298
+ return
299
+ dev_x = info.scan_report_devices[0] # type:ignore
300
+ dev_y = self.get_selected_device(info.readout_priority["monitored"]) # type:ignore
301
+ if not dev_y:
302
+ return
303
+
304
+ # Set the dock to the waveform widget
305
+ wf = self.set_dock_to_widget("Waveform")
306
+
307
+ # Clear the waveform widget and plot the data
308
+ wf.clear_all()
309
+ wf.plot(
310
+ x_name=dev_x,
311
+ y_name=dev_y,
312
+ label=f"Scan {info.scan_number} - {dev_y}",
313
+ title=f"Scan {info.scan_number}",
314
+ x_label=dev_x,
315
+ y_label=dev_y,
316
+ )
317
+
318
+ logger.info(f"Auto Update [best_effort]: Started plot with: x_name={dev_x}, y_name={dev_y}")
319
+
320
+ #######################################################################
321
+ ################# GUI Callbacks #######################################
322
+ #######################################################################
323
+
324
+ def on_start(self) -> None:
325
+ """
326
+ Procedure to run when the auto updates are enabled.
327
+ """
328
+ self.start_default_dock()
329
+
330
+ def on_stop(self) -> None:
331
+ """
332
+ Procedure to run when the auto updates are disabled.
333
+ """
334
+
335
+ def on_scan_open(self, msg: ScanStatusMessage) -> None:
336
+ """
337
+ Procedure to run when a scan starts.
338
+
339
+ Args:
340
+ msg (ScanStatusMessage): The scan status message.
341
+ """
342
+ if msg.scan_name == "line_scan" and msg.scan_report_devices:
343
+ return self.simple_line_scan(msg)
344
+ if msg.scan_name == "grid_scan" and msg.scan_report_devices:
345
+ return self.simple_grid_scan(msg)
346
+ if msg.scan_report_devices:
347
+ return self.best_effort(msg)
348
+ return None
349
+
350
+ def on_scan_closed(self, msg: ScanStatusMessage) -> None:
351
+ """
352
+ Procedure to run when a scan ends.
353
+
354
+ Args:
355
+ msg (ScanStatusMessage): The scan status message.
356
+ """
357
+
358
+ def on_scan_abort(self, msg: ScanStatusMessage) -> None:
359
+ """
360
+ Procedure to run when a scan is aborted.
361
+
362
+ Args:
363
+ msg (ScanStatusMessage): The scan status message.
364
+ """