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
@@ -0,0 +1,553 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ import pyqtgraph as pg
6
+ from bec_lib import bec_logger
7
+ from bec_lib.endpoints import MessageEndpoints
8
+ from pydantic import Field, ValidationError, field_validator
9
+ from qtpy.QtCore import QTimer, Signal
10
+ from qtpy.QtWidgets import QHBoxLayout, QMainWindow, QWidget
11
+
12
+ from bec_widgets.utils import Colors, ConnectionConfig
13
+ from bec_widgets.utils.colors import set_theme
14
+ from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
15
+ from bec_widgets.utils.settings_dialog import SettingsDialog
16
+ from bec_widgets.utils.toolbar import MaterialIconAction
17
+ from bec_widgets.widgets.plots.plot_base import PlotBase, UIMode
18
+ from bec_widgets.widgets.plots.scatter_waveform.scatter_curve import (
19
+ ScatterCurve,
20
+ ScatterCurveConfig,
21
+ ScatterDeviceSignal,
22
+ )
23
+ from bec_widgets.widgets.plots.scatter_waveform.settings.scatter_curve_setting import (
24
+ ScatterCurveSettings,
25
+ )
26
+
27
+ logger = bec_logger.logger
28
+
29
+
30
+ # noinspection PyDataclass
31
+ class ScatterWaveformConfig(ConnectionConfig):
32
+ color_map: str | None = Field(
33
+ "plasma",
34
+ description="The color map of the z scaling of scatter waveform.",
35
+ validate_default=True,
36
+ )
37
+
38
+ model_config: dict = {"validate_assignment": True}
39
+ _validate_color_palette = field_validator("color_map")(Colors.validate_color_map)
40
+
41
+
42
+ class ScatterWaveform(PlotBase):
43
+ PLUGIN = True
44
+ RPC = True
45
+ ICON_NAME = "scatter_plot"
46
+ USER_ACCESS = [
47
+ # General PlotBase Settings
48
+ "enable_toolbar",
49
+ "enable_toolbar.setter",
50
+ "enable_side_panel",
51
+ "enable_side_panel.setter",
52
+ "enable_fps_monitor",
53
+ "enable_fps_monitor.setter",
54
+ "set",
55
+ "title",
56
+ "title.setter",
57
+ "x_label",
58
+ "x_label.setter",
59
+ "y_label",
60
+ "y_label.setter",
61
+ "x_limits",
62
+ "x_limits.setter",
63
+ "y_limits",
64
+ "y_limits.setter",
65
+ "x_grid",
66
+ "x_grid.setter",
67
+ "y_grid",
68
+ "y_grid.setter",
69
+ "inner_axes",
70
+ "inner_axes.setter",
71
+ "outer_axes",
72
+ "outer_axes.setter",
73
+ "lock_aspect_ratio",
74
+ "lock_aspect_ratio.setter",
75
+ "auto_range_x",
76
+ "auto_range_x.setter",
77
+ "auto_range_y",
78
+ "auto_range_y.setter",
79
+ "x_log",
80
+ "x_log.setter",
81
+ "y_log",
82
+ "y_log.setter",
83
+ "legend_label_size",
84
+ "legend_label_size.setter",
85
+ # Scatter Waveform Specific RPC Access
86
+ "main_curve",
87
+ "color_map",
88
+ "color_map.setter",
89
+ "plot",
90
+ "update_with_scan_history",
91
+ "clear_all",
92
+ ]
93
+
94
+ sync_signal_update = Signal()
95
+ new_scan = Signal()
96
+ new_scan_id = Signal(str)
97
+ scatter_waveform_property_changed = Signal()
98
+
99
+ def __init__(
100
+ self,
101
+ parent: QWidget | None = None,
102
+ config: ScatterWaveformConfig | None = None,
103
+ client=None,
104
+ gui_id: str | None = None,
105
+ popups: bool = True,
106
+ **kwargs,
107
+ ):
108
+ if config is None:
109
+ config = ScatterWaveformConfig(widget_class=self.__class__.__name__)
110
+ # Specific GUI elements
111
+ self.scatter_dialog = None
112
+ self.scatter_curve_settings = None
113
+
114
+ super().__init__(
115
+ parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
116
+ )
117
+ self._main_curve = ScatterCurve(parent_item=self)
118
+
119
+ # Scan Data
120
+ self.old_scan_id = None
121
+ self.scan_id = None
122
+ self.scan_item = None
123
+
124
+ # Scan status update loop
125
+ self.bec_dispatcher.connect_slot(self.on_scan_status, MessageEndpoints.scan_status())
126
+ self.bec_dispatcher.connect_slot(self.on_scan_progress, MessageEndpoints.scan_progress())
127
+
128
+ # Curve update loop
129
+ self.proxy_update_sync = pg.SignalProxy(
130
+ self.sync_signal_update, rateLimit=25, slot=self.update_sync_curves
131
+ )
132
+ if self.ui_mode == UIMode.SIDE:
133
+ self._init_scatter_curve_settings()
134
+ self.update_with_scan_history(-1)
135
+
136
+ ################################################################################
137
+ # Widget Specific GUI interactions
138
+ ################################################################################
139
+
140
+ def _init_scatter_curve_settings(self):
141
+ """
142
+ Initialize the scatter curve settings menu.
143
+ """
144
+
145
+ self.scatter_curve_settings = ScatterCurveSettings(
146
+ parent=self, target_widget=self, popup=False
147
+ )
148
+ self.side_panel.add_menu(
149
+ action_id="scatter_curve",
150
+ icon_name="scatter_plot",
151
+ tooltip="Show Scatter Curve Settings",
152
+ widget=self.scatter_curve_settings,
153
+ title="Scatter Curve Settings",
154
+ )
155
+
156
+ def add_popups(self):
157
+ """
158
+ Add popups to the ScatterWaveform widget.
159
+ """
160
+ super().add_popups()
161
+ scatter_curve_setting_action = MaterialIconAction(
162
+ icon_name="scatter_plot",
163
+ tooltip="Show Scatter Curve Settings",
164
+ checkable=True,
165
+ parent=self,
166
+ )
167
+ self.toolbar.add_action_to_bundle(
168
+ bundle_id="popup_bundle",
169
+ action_id="scatter_waveform_settings",
170
+ action=scatter_curve_setting_action,
171
+ target_widget=self,
172
+ )
173
+ self.toolbar.widgets["scatter_waveform_settings"].action.triggered.connect(
174
+ self.show_scatter_curve_settings
175
+ )
176
+
177
+ def show_scatter_curve_settings(self):
178
+ """
179
+ Show the scatter curve settings dialog.
180
+ """
181
+ scatter_settings_action = self.toolbar.widgets["scatter_waveform_settings"].action
182
+ if self.scatter_dialog is None or not self.scatter_dialog.isVisible():
183
+ scatter_settings = ScatterCurveSettings(parent=self, target_widget=self, popup=True)
184
+ self.scatter_dialog = SettingsDialog(
185
+ self,
186
+ settings_widget=scatter_settings,
187
+ window_title="Scatter Curve Settings",
188
+ modal=False,
189
+ )
190
+ self.scatter_dialog.resize(620, 200)
191
+ # When the dialog is closed, update the toolbar icon and clear the reference
192
+ self.scatter_dialog.finished.connect(self._scatter_dialog_closed)
193
+ self.scatter_dialog.show()
194
+ scatter_settings_action.setChecked(True)
195
+ else:
196
+ # If already open, bring it to the front
197
+ self.scatter_dialog.raise_()
198
+ self.scatter_dialog.activateWindow()
199
+ scatter_settings_action.setChecked(True) # keep it toggled
200
+
201
+ def _scatter_dialog_closed(self):
202
+ """
203
+ Slot for when the scatter curve settings dialog is closed.
204
+ """
205
+ self.scatter_dialog = None
206
+ self.toolbar.widgets["scatter_waveform_settings"].action.setChecked(False)
207
+
208
+ ################################################################################
209
+ # Widget Specific Properties
210
+ ################################################################################
211
+ @property
212
+ def main_curve(self) -> ScatterCurve:
213
+ """The main scatter curve item."""
214
+ return self._main_curve
215
+
216
+ @SafeProperty(str)
217
+ def color_map(self) -> str:
218
+ """The color map of the scatter waveform."""
219
+ return self.config.color_map
220
+
221
+ @color_map.setter
222
+ def color_map(self, value: str):
223
+ """
224
+ Set the color map of the scatter waveform.
225
+
226
+ Args:
227
+ value(str): The color map to set.
228
+ """
229
+ try:
230
+ self.config.color_map = value
231
+ self.main_curve.color_map = value
232
+ self.scatter_waveform_property_changed.emit()
233
+ except ValidationError:
234
+ return
235
+
236
+ @SafeProperty(str, designable=False, popup_error=True)
237
+ def curve_json(self) -> str:
238
+ """
239
+ Get the curve configuration as a JSON string.
240
+ """
241
+ return json.dumps(self.main_curve.config.model_dump(), indent=2)
242
+
243
+ @curve_json.setter
244
+ def curve_json(self, value: str):
245
+ """
246
+ Set the curve configuration from a JSON string.
247
+
248
+ Args:
249
+ value(str): The JSON string to set the curve configuration from.
250
+ """
251
+ try:
252
+ config = ScatterCurveConfig(**json.loads(value))
253
+ self._add_main_scatter_curve(config)
254
+ except json.JSONDecodeError as e:
255
+ logger.error(f"Failed to decode JSON: {e}")
256
+
257
+ ################################################################################
258
+ # High Level methods for API
259
+ ################################################################################
260
+ @SafeSlot(popup_error=True)
261
+ def plot(
262
+ self,
263
+ x_name: str,
264
+ y_name: str,
265
+ z_name: str,
266
+ x_entry: None | str = None,
267
+ y_entry: None | str = None,
268
+ z_entry: None | str = None,
269
+ color_map: str | None = "plasma",
270
+ label: str | None = None,
271
+ validate_bec: bool = True,
272
+ ) -> ScatterCurve:
273
+ """
274
+ Plot the data from the device signals.
275
+
276
+ Args:
277
+ x_name (str): The name of the x device signal.
278
+ y_name (str): The name of the y device signal.
279
+ z_name (str): The name of the z device signal.
280
+ x_entry (None | str): The x entry of the device signal.
281
+ y_entry (None | str): The y entry of the device signal.
282
+ z_entry (None | str): The z entry of the device signal.
283
+ color_map (str | None): The color map of the scatter waveform.
284
+ label (str | None): The label of the curve.
285
+ validate_bec (bool): Whether to validate the device signals with current BEC instance.
286
+
287
+ Returns:
288
+ ScatterCurve: The scatter curve object.
289
+ """
290
+
291
+ if validate_bec:
292
+ x_entry = self.entry_validator.validate_signal(x_name, x_entry)
293
+ y_entry = self.entry_validator.validate_signal(y_name, y_entry)
294
+ z_entry = self.entry_validator.validate_signal(z_name, z_entry)
295
+
296
+ if color_map is not None:
297
+ try:
298
+ self.config.color_map = color_map
299
+ except ValidationError:
300
+ raise ValueError(
301
+ f"Invalid color map '{color_map}'. Using previously defined color map '{self.config.color_map}'."
302
+ )
303
+
304
+ if label is None:
305
+ label = f"{z_name}-{z_entry}"
306
+
307
+ config = ScatterCurveConfig(
308
+ parent_id=self.gui_id,
309
+ label=label,
310
+ color_map=color_map,
311
+ x_device=ScatterDeviceSignal(name=x_name, entry=x_entry),
312
+ y_device=ScatterDeviceSignal(name=y_name, entry=y_entry),
313
+ z_device=ScatterDeviceSignal(name=z_name, entry=z_entry),
314
+ )
315
+
316
+ # Add Curve
317
+ self._add_main_scatter_curve(config)
318
+
319
+ self.scatter_waveform_property_changed.emit()
320
+
321
+ return self._main_curve
322
+
323
+ def _add_main_scatter_curve(self, config: ScatterCurveConfig):
324
+ """
325
+ Add the main scatter curve to the plot.
326
+
327
+ Args:
328
+ config(ScatterCurveConfig): The configuration of the scatter curve.
329
+ """
330
+ # Apply suffix for axes
331
+ self.set_x_label_suffix(f"[{config.x_device.name}-{config.x_device.name}]")
332
+ self.set_y_label_suffix(f"[{config.y_device.name}-{config.y_device.name}]")
333
+
334
+ # To have only one main curve
335
+ if self._main_curve is not None:
336
+ self.rpc_register.remove_rpc(self._main_curve)
337
+ self.rpc_register.broadcast()
338
+ self.plot_item.removeItem(self._main_curve)
339
+ self._main_curve.deleteLater()
340
+ self._main_curve = None
341
+
342
+ self._main_curve = ScatterCurve(parent_item=self, config=config, name=config.label)
343
+ self.plot_item.addItem(self._main_curve)
344
+
345
+ self.sync_signal_update.emit()
346
+
347
+ ################################################################################
348
+ # BEC Update Methods
349
+ ################################################################################
350
+ @SafeSlot(dict, dict)
351
+ def on_scan_status(self, msg: dict, meta: dict):
352
+ """
353
+ Initial scan status message handler, which is triggered at the begging and end of scan.
354
+ Used for triggering the update of the sync and async curves.
355
+
356
+ Args:
357
+ msg(dict): The message content.
358
+ meta(dict): The message metadata.
359
+ """
360
+ current_scan_id = msg.get("scan_id", None)
361
+ if current_scan_id is None:
362
+ return
363
+ if current_scan_id != self.scan_id:
364
+ self.reset()
365
+ self.new_scan.emit()
366
+ self.new_scan_id.emit(current_scan_id)
367
+ self.auto_range_x = True
368
+ self.auto_range_y = True
369
+ self.old_scan_id = self.scan_id
370
+ self.scan_id = current_scan_id
371
+ self.scan_item = self.queue.scan_storage.find_scan_by_ID(self.scan_id)
372
+
373
+ # First trigger to update the scan curves
374
+ self.sync_signal_update.emit()
375
+
376
+ @SafeSlot(dict, dict)
377
+ def on_scan_progress(self, msg: dict, meta: dict):
378
+ """
379
+ Slot for handling scan progress messages. Used for triggering the update of the sync curves.
380
+
381
+ Args:
382
+ msg(dict): The message content.
383
+ meta(dict): The message metadata.
384
+ """
385
+ self.sync_signal_update.emit()
386
+ status = msg.get("done")
387
+ if status:
388
+ QTimer.singleShot(100, self.update_sync_curves)
389
+ QTimer.singleShot(300, self.update_sync_curves)
390
+
391
+ @SafeSlot()
392
+ def update_sync_curves(self, _=None):
393
+ """
394
+ Update the scan curves with the data from the scan segment.
395
+ """
396
+ if self.scan_item is None:
397
+ logger.info("No scan executed so far; skipping device curves categorisation.")
398
+ return "none"
399
+ data, access_key = self._fetch_scan_data_and_access()
400
+
401
+ if data == "none":
402
+ logger.info("No scan executed so far; skipping device curves categorisation.")
403
+ return "none"
404
+
405
+ try:
406
+ x_name = self._main_curve.config.x_device.name
407
+ x_entry = self._main_curve.config.x_device.entry
408
+ y_name = self._main_curve.config.y_device.name
409
+ y_entry = self._main_curve.config.y_device.entry
410
+ z_name = self._main_curve.config.z_device.name
411
+ z_entry = self._main_curve.config.z_device.entry
412
+ except AttributeError:
413
+ return
414
+
415
+ if access_key == "val":
416
+ x_data = data.get(x_name, {}).get(x_entry, {}).get(access_key, None)
417
+ y_data = data.get(y_name, {}).get(y_entry, {}).get(access_key, None)
418
+ z_data = data.get(z_name, {}).get(z_entry, {}).get(access_key, None)
419
+ else:
420
+ x_data = data.get(x_name, {}).get(x_entry, {}).read().get("value", None)
421
+ y_data = data.get(y_name, {}).get(y_entry, {}).read().get("value", None)
422
+ z_data = data.get(z_name, {}).get(z_entry, {}).read().get("value", None)
423
+
424
+ self._main_curve.set_data(x=x_data, y=y_data, z=z_data)
425
+
426
+ def _fetch_scan_data_and_access(self):
427
+ """
428
+ Decide whether the widget is in live or historical mode
429
+ and return the appropriate data dict and access key.
430
+
431
+ Returns:
432
+ data_dict (dict): The data structure for the current scan.
433
+ access_key (str): Either 'val' (live) or 'value' (history).
434
+ """
435
+ if self.scan_item is None:
436
+ # Optionally fetch the latest from history if nothing is set
437
+ self.update_with_scan_history(-1)
438
+ if self.scan_item is None:
439
+ logger.info("No scan executed so far; skipping device curves categorisation.")
440
+ return "none", "none"
441
+
442
+ if hasattr(self.scan_item, "live_data"):
443
+ # Live scan
444
+ return self.scan_item.live_data, "val"
445
+ else:
446
+ # Historical
447
+ scan_devices = self.scan_item.devices
448
+ return scan_devices, "value"
449
+
450
+ @SafeSlot(int)
451
+ @SafeSlot(str)
452
+ @SafeSlot()
453
+ def update_with_scan_history(self, scan_index: int = None, scan_id: str = None):
454
+ """
455
+ Update the scan curves with the data from the scan storage.
456
+ Provide only one of scan_id or scan_index.
457
+
458
+ Args:
459
+ scan_id(str, optional): ScanID of the scan to be updated. Defaults to None.
460
+ scan_index(int, optional): Index of the scan to be updated. Defaults to None.
461
+ """
462
+ if scan_index is not None and scan_id is not None:
463
+ raise ValueError("Only one of scan_id or scan_index can be provided.")
464
+
465
+ if scan_index is None and scan_id is None:
466
+ logger.warning(f"Neither scan_id or scan_number was provided, fetching the latest scan")
467
+ scan_index = -1
468
+
469
+ if scan_index is None:
470
+ self.scan_id = scan_id
471
+ self.scan_item = self.client.history.get_by_scan_id(scan_id)
472
+ self.sync_signal_update.emit()
473
+ return
474
+
475
+ if scan_index == -1:
476
+ scan_item = self.client.queue.scan_storage.current_scan
477
+ if scan_item is not None:
478
+ if scan_item.status_message is None:
479
+ logger.warning(f"Scan item with {scan_item.scan_id} has no status message.")
480
+ return
481
+ self.scan_item = scan_item
482
+ self.scan_id = scan_item.scan_id
483
+ self.sync_signal_update.emit()
484
+ return
485
+
486
+ if len(self.client.history) == 0:
487
+ logger.info("No scans executed so far. Skipping scan history update.")
488
+ return
489
+
490
+ self.scan_item = self.client.history[scan_index]
491
+ metadata = self.scan_item.metadata
492
+ self.scan_id = metadata["bec"]["scan_id"]
493
+
494
+ self.sync_signal_update.emit()
495
+
496
+ ################################################################################
497
+ # Cleanup
498
+ ################################################################################
499
+ @SafeSlot()
500
+ def clear_all(self):
501
+ """
502
+ Clear all the curves from the plot.
503
+ """
504
+ if self.crosshair is not None:
505
+ self.crosshair.clear_markers()
506
+ self._main_curve.clear()
507
+
508
+ def cleanup(self):
509
+ """
510
+ Cleanup the widget and disconnect all signals.
511
+ """
512
+ if self.scatter_dialog is not None:
513
+ self.scatter_dialog.close()
514
+ self.scatter_dialog.deleteLater()
515
+ if self.scatter_curve_settings is not None:
516
+ self.scatter_curve_settings.cleanup()
517
+ self.bec_dispatcher.disconnect_slot(self.on_scan_status, MessageEndpoints.scan_status())
518
+ self.bec_dispatcher.disconnect_slot(self.on_scan_progress, MessageEndpoints.scan_progress())
519
+ self.plot_item.removeItem(self._main_curve)
520
+ self._main_curve = None
521
+ super().cleanup()
522
+
523
+
524
+ class DemoApp(QMainWindow): # pragma: no cover
525
+ def __init__(self):
526
+ super().__init__()
527
+ self.setWindowTitle("Waveform Demo")
528
+ self.resize(800, 600)
529
+ self.main_widget = QWidget()
530
+ self.layout = QHBoxLayout(self.main_widget)
531
+ self.setCentralWidget(self.main_widget)
532
+
533
+ self.waveform_popup = ScatterWaveform(popups=True)
534
+ self.waveform_popup.plot("samx", "samy", "bpm4i")
535
+
536
+ self.waveform_side = ScatterWaveform(popups=False)
537
+ self.waveform_popup.plot("samx", "samy", "bpm3a")
538
+
539
+ self.layout.addWidget(self.waveform_side)
540
+ self.layout.addWidget(self.waveform_popup)
541
+
542
+
543
+ if __name__ == "__main__": # pragma: no cover
544
+ import sys
545
+
546
+ from qtpy.QtWidgets import QApplication
547
+
548
+ app = QApplication(sys.argv)
549
+ set_theme("dark")
550
+ widget = DemoApp()
551
+ widget.show()
552
+ widget.resize(1400, 600)
553
+ sys.exit(app.exec_())
@@ -0,0 +1 @@
1
+ {'files': ['scatter_waveform.py']}
@@ -1,43 +1,39 @@
1
1
  # Copyright (C) 2022 The Qt Company Ltd.
2
2
  # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
3
- import os
4
3
 
5
4
  from qtpy.QtDesigner import QDesignerCustomWidgetInterface
6
5
 
7
- import bec_widgets
8
6
  from bec_widgets.utils.bec_designer import designer_material_icon
9
- from bec_widgets.widgets.plots.image.image_widget import BECImageWidget
7
+ from bec_widgets.widgets.plots.scatter_waveform.scatter_waveform import ScatterWaveform
10
8
 
11
9
  DOM_XML = """
12
10
  <ui language='c++'>
13
- <widget class='BECImageWidget' name='bec_image_widget'>
11
+ <widget class='ScatterWaveform' name='scatter_waveform'>
14
12
  </widget>
15
13
  </ui>
16
14
  """
17
15
 
18
- MODULE_PATH = os.path.dirname(bec_widgets.__file__)
19
16
 
20
-
21
- class BECImageWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
17
+ class ScatterWaveformPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
22
18
  def __init__(self):
23
19
  super().__init__()
24
20
  self._form_editor = None
25
21
 
26
22
  def createWidget(self, parent):
27
- t = BECImageWidget(parent)
23
+ t = ScatterWaveform(parent)
28
24
  return t
29
25
 
30
26
  def domXml(self):
31
27
  return DOM_XML
32
28
 
33
29
  def group(self):
34
- return "BEC Plots"
30
+ return "Plot Widgets"
35
31
 
36
32
  def icon(self):
37
- return designer_material_icon(BECImageWidget.ICON_NAME)
33
+ return designer_material_icon(ScatterWaveform.ICON_NAME)
38
34
 
39
35
  def includeFile(self):
40
- return "bec_image_widget"
36
+ return "scatter_waveform"
41
37
 
42
38
  def initialize(self, form_editor):
43
39
  self._form_editor = form_editor
@@ -49,10 +45,10 @@ class BECImageWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
49
45
  return self._form_editor is not None
50
46
 
51
47
  def name(self):
52
- return "BECImageWidget"
48
+ return "ScatterWaveform"
53
49
 
54
50
  def toolTip(self):
55
- return "BECImageWidget"
51
+ return "ScatterWaveform"
56
52
 
57
53
  def whatsThis(self):
58
54
  return self.toolTip()