bec-widgets 1.25.0__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.
- .gitlab-ci.yml +11 -6
- CHANGELOG.md +650 -0
- PKG-INFO +3 -3
- bec_widgets/__init__.py +4 -0
- bec_widgets/applications/bw_launch.py +23 -0
- bec_widgets/applications/launch_window.py +430 -0
- bec_widgets/assets/app_icons/auto_update.png +0 -0
- bec_widgets/assets/app_icons/ui_loader_tile.png +0 -0
- bec_widgets/cli/__init__.py +0 -1
- bec_widgets/cli/client.py +1779 -2064
- bec_widgets/cli/client_utils.py +346 -174
- bec_widgets/cli/generate_cli.py +143 -37
- bec_widgets/cli/rpc/rpc_base.py +152 -21
- bec_widgets/cli/rpc/rpc_register.py +113 -6
- bec_widgets/cli/rpc/rpc_widget_handler.py +13 -11
- bec_widgets/cli/server.py +125 -239
- bec_widgets/examples/jupyter_console/jupyter_console_window.py +97 -145
- bec_widgets/examples/plugin_example_pyside/tictactoetaskmenu.py +1 -1
- bec_widgets/utils/bec_connector.py +190 -21
- bec_widgets/utils/bec_designer.py +7 -0
- bec_widgets/utils/bec_dispatcher.py +71 -4
- bec_widgets/utils/bec_plugin_helper.py +89 -0
- bec_widgets/utils/bec_signal_proxy.py +1 -1
- bec_widgets/utils/bec_widget.py +26 -10
- bec_widgets/utils/colors.py +1 -1
- bec_widgets/{qt_utils → utils}/compact_popup.py +2 -0
- bec_widgets/utils/container_utils.py +37 -12
- bec_widgets/utils/crosshair.py +25 -8
- bec_widgets/utils/entry_validator.py +3 -1
- bec_widgets/{qt_utils → utils}/error_popups.py +18 -0
- bec_widgets/{qt_utils → utils}/expandable_frame.py +2 -2
- bec_widgets/utils/forms_from_types/forms.py +182 -0
- bec_widgets/{widgets/editors/scan_metadata/_metadata_widgets.py → utils/forms_from_types/items.py} +41 -30
- bec_widgets/utils/generate_designer_plugin.py +40 -36
- bec_widgets/utils/linear_region_selector.py +2 -0
- bec_widgets/utils/name_utils.py +16 -0
- bec_widgets/{qt_utils → utils}/palette_viewer.py +2 -2
- bec_widgets/utils/plot_indicator_items.py +2 -5
- bec_widgets/utils/plugin_utils.py +47 -1
- bec_widgets/{qt_utils → utils}/round_frame.py +14 -14
- bec_widgets/utils/rpc_server.py +277 -0
- bec_widgets/utils/serialization.py +44 -0
- bec_widgets/{qt_utils → utils}/settings_dialog.py +26 -1
- bec_widgets/{qt_utils → utils}/side_panel.py +17 -10
- bec_widgets/{qt_utils → utils}/toolbar.py +69 -25
- bec_widgets/utils/ui_loader.py +8 -8
- bec_widgets/utils/widget_io.py +166 -25
- bec_widgets/widgets/containers/auto_update/auto_updates.py +364 -0
- bec_widgets/widgets/containers/dock/dock.py +157 -49
- bec_widgets/widgets/containers/dock/dock_area.py +186 -138
- bec_widgets/widgets/containers/layout_manager/layout_manager.py +2 -1
- bec_widgets/widgets/containers/main_window/addons/web_links.py +15 -0
- bec_widgets/widgets/containers/main_window/main_window.py +189 -41
- bec_widgets/widgets/control/buttons/button_abort/button_abort.py +3 -4
- bec_widgets/widgets/control/buttons/button_reset/button_reset.py +3 -4
- bec_widgets/widgets/control/buttons/button_resume/button_resume.py +3 -3
- bec_widgets/widgets/control/buttons/stop_button/stop_button.py +18 -7
- bec_widgets/widgets/control/device_control/position_indicator/position_indicator.py +22 -3
- bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +37 -18
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +28 -4
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.ui +27 -4
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +5 -2
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +97 -31
- bec_widgets/widgets/control/device_control/positioner_box/positioner_control_line/positioner_control_line.ui +11 -4
- bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +2 -3
- bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +29 -4
- bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +1 -0
- bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +2 -2
- bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +2 -2
- bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py +1 -2
- bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.py +1 -2
- bec_widgets/widgets/control/scan_control/scan_control.py +7 -5
- bec_widgets/widgets/control/scan_control/scan_group_box.py +28 -5
- bec_widgets/widgets/dap/dap_combo_box/dap_combo_box.py +1 -2
- bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog.py +3 -4
- bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog_vertical.ui +14 -8
- bec_widgets/widgets/editors/console/console.py +1 -1
- bec_widgets/widgets/editors/{scan_metadata/additional_metadata_table.py → dict_backed_table.py} +29 -6
- bec_widgets/widgets/editors/scan_metadata/__init__.py +0 -7
- bec_widgets/widgets/editors/scan_metadata/_util.py +1 -1
- bec_widgets/widgets/{plots/motor_map/register_bec_motor_map_widget.py → editors/scan_metadata/register_scan_metadata.py} +2 -4
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +42 -136
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.pyproject +1 -0
- bec_widgets/widgets/{plots/multi_waveform/bec_multi_waveform_widget_plugin.py → editors/scan_metadata/scan_metadata_plugin.py} +9 -9
- bec_widgets/widgets/editors/text_box/text_box.py +2 -3
- bec_widgets/widgets/editors/website/website.py +2 -2
- bec_widgets/widgets/games/minesweeper.py +3 -2
- bec_widgets/widgets/plots/image/image.py +960 -0
- bec_widgets/widgets/plots/image/image.pyproject +1 -0
- bec_widgets/widgets/plots/image/image_item.py +279 -0
- bec_widgets/widgets/plots/{motor_map/bec_motor_map_widget_plugin.py → image/image_plugin.py} +11 -13
- bec_widgets/widgets/{containers/figure/plots → plots}/image/image_processor.py +31 -64
- bec_widgets/widgets/plots/image/{register_bec_image_widget.py → register_image.py} +2 -2
- bec_widgets/widgets/plots/image/toolbar_bundles/image_selection.py +59 -0
- bec_widgets/widgets/plots/image/toolbar_bundles/processing.py +79 -0
- bec_widgets/widgets/plots/motor_map/motor_map.py +832 -0
- bec_widgets/widgets/plots/motor_map/motor_map.pyproject +1 -0
- bec_widgets/widgets/plots/motor_map/motor_map_plugin.py +54 -0
- bec_widgets/widgets/plots/{multi_waveform/register_bec_multi_waveform_widget.py → motor_map/register_motor_map.py} +2 -4
- bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py +129 -0
- bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui +120 -0
- bec_widgets/widgets/plots/motor_map/toolbar_bundles/motor_selection.py +70 -0
- bec_widgets/widgets/plots/multi_waveform/multi_waveform.py +508 -0
- bec_widgets/widgets/plots/multi_waveform/multi_waveform.pyproject +1 -0
- bec_widgets/widgets/plots/multi_waveform/multi_waveform_plugin.py +54 -0
- bec_widgets/widgets/plots/multi_waveform/register_multi_waveform.py +15 -0
- bec_widgets/widgets/plots/multi_waveform/settings/control_panel.py +144 -0
- bec_widgets/widgets/plots/multi_waveform/settings/multi_waveform_controls.ui +164 -0
- bec_widgets/widgets/plots/multi_waveform/toolbar_bundles/monitor_selection.py +65 -0
- bec_widgets/widgets/{plots_next_gen → plots}/plot_base.py +321 -40
- bec_widgets/widgets/plots/{waveform/register_bec_waveform_widget.py → scatter_waveform/register_scatter_waveform.py} +3 -3
- bec_widgets/widgets/plots/scatter_waveform/scatter_curve.py +197 -0
- bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py +553 -0
- bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.pyproject +1 -0
- bec_widgets/widgets/plots/{image/bec_image_widget_plugin.py → scatter_waveform/scatter_waveform_plugin.py} +9 -13
- bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_setting.py +138 -0
- bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_horizontal.ui +195 -0
- bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_vertical.ui +204 -0
- bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings.py +8 -8
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/mouse_interactions.py +4 -18
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/plot_export.py +14 -3
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/roi_bundle.py +6 -1
- bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/save_state.py +2 -2
- bec_widgets/widgets/{containers/figure/plots/waveform/waveform_curve.py → plots/waveform/curve.py} +119 -49
- bec_widgets/widgets/plots/waveform/register_waveform.py +15 -0
- bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_setting.py +125 -0
- bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +576 -0
- bec_widgets/widgets/plots/waveform/utils/__init__.py +0 -0
- bec_widgets/widgets/plots/waveform/utils/roi_manager.py +84 -0
- bec_widgets/widgets/plots/waveform/waveform.py +1794 -0
- bec_widgets/widgets/plots/waveform/waveform.pyproject +1 -0
- bec_widgets/widgets/plots/waveform/{bec_waveform_widget_plugin.py → waveform_plugin.py} +9 -13
- bec_widgets/widgets/progress/bec_progressbar/bec_progressbar.py +1 -2
- bec_widgets/widgets/progress/ring_progress_bar/ring.py +11 -10
- bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +24 -14
- bec_widgets/widgets/services/bec_queue/bec_queue.py +13 -11
- bec_widgets/widgets/services/bec_status_box/bec_status_box.py +3 -4
- bec_widgets/widgets/services/device_browser/device_browser.py +5 -2
- bec_widgets/widgets/services/device_browser/device_item/device_item.py +1 -1
- bec_widgets/widgets/utility/logpanel/logpanel.py +36 -17
- bec_widgets/widgets/utility/spinbox/decimal_spinbox.py +3 -3
- bec_widgets/widgets/utility/spinner/spinner.py +2 -2
- bec_widgets/widgets/utility/visual/color_button/color_button.py +1 -1
- bec_widgets/widgets/utility/visual/colormap_widget/colormap_widget.py +4 -6
- bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py +4 -8
- {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/METADATA +3 -3
- {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/RECORD +169 -154
- pyproject.toml +3 -3
- bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +0 -198
- bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +0 -615
- bec_widgets/applications/bec_app.py +0 -84
- bec_widgets/cli/auto_updates.py +0 -168
- bec_widgets/widgets/containers/figure/__init__.py +0 -1
- bec_widgets/widgets/containers/figure/figure.py +0 -796
- bec_widgets/widgets/containers/figure/plots/axis_settings.py +0 -91
- bec_widgets/widgets/containers/figure/plots/axis_settings.ui +0 -256
- bec_widgets/widgets/containers/figure/plots/image/image.py +0 -772
- bec_widgets/widgets/containers/figure/plots/image/image_item.py +0 -337
- bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py +0 -525
- bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py +0 -340
- bec_widgets/widgets/containers/figure/plots/plot_base.py +0 -505
- bec_widgets/widgets/containers/figure/plots/waveform/waveform.py +0 -1563
- bec_widgets/widgets/plots/image/bec_image_widget.pyproject +0 -1
- bec_widgets/widgets/plots/image/image_widget.py +0 -515
- bec_widgets/widgets/plots/motor_map/bec_motor_map_widget.pyproject +0 -1
- bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.py +0 -56
- bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.ui +0 -108
- bec_widgets/widgets/plots/motor_map/motor_map_widget.py +0 -234
- bec_widgets/widgets/plots/multi_waveform/bec_multi_waveform_widget.pyproject +0 -1
- bec_widgets/widgets/plots/multi_waveform/multi_waveform_controls.ui +0 -99
- bec_widgets/widgets/plots/multi_waveform/multi_waveform_widget.py +0 -536
- bec_widgets/widgets/plots/waveform/bec_waveform_widget.pyproject +0 -1
- bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.py +0 -336
- bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.ui +0 -372
- bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py +0 -25
- bec_widgets/widgets/plots/waveform/waveform_widget.py +0 -751
- /bec_widgets/{qt_utils → utils}/collapsible_panel_manager.py +0 -0
- /bec_widgets/{applications/alignment → utils/forms_from_types}/__init__.py +0 -0
- /bec_widgets/{qt_utils → utils}/redis_message_waiter.py +0 -0
- /bec_widgets/{applications/alignment/alignment_1d → widgets/containers/auto_update}/__init__.py +0 -0
- /bec_widgets/{qt_utils → widgets/containers/main_window/addons}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots → plots/image/toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/image → plots/motor_map/settings}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/motor_map → plots/motor_map/toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/multi_waveform → plots/multi_waveform/settings}/__init__.py +0 -0
- /bec_widgets/widgets/{containers/figure/plots/waveform → plots/multi_waveform/toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/plots/{motor_map/motor_map_dialog → scatter_waveform}/__init__.py +0 -0
- /bec_widgets/widgets/plots/{waveform/waveform_popups → scatter_waveform/settings}/__init__.py +0 -0
- /bec_widgets/widgets/plots/{waveform/waveform_popups/curve_dialog → setting_menus}/__init__.py +0 -0
- /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_horizontal.ui +0 -0
- /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_vertical.ui +0 -0
- /bec_widgets/widgets/plots/{waveform/waveform_popups/dap_summary_dialog → toolbar_bundles}/__init__.py +0 -0
- /bec_widgets/widgets/{plots_next_gen/setting_menus → plots/waveform/settings}/__init__.py +0 -0
- /bec_widgets/widgets/{plots_next_gen/toolbar_bundles → plots/waveform/settings/curve_settings}/__init__.py +0 -0
- {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.25.0.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.
|
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='
|
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 =
|
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 "
|
30
|
+
return "Plot Widgets"
|
35
31
|
|
36
32
|
def icon(self):
|
37
|
-
return designer_material_icon(
|
33
|
+
return designer_material_icon(ScatterWaveform.ICON_NAME)
|
38
34
|
|
39
35
|
def includeFile(self):
|
40
|
-
return "
|
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 "
|
48
|
+
return "ScatterWaveform"
|
53
49
|
|
54
50
|
def toolTip(self):
|
55
|
-
return "
|
51
|
+
return "ScatterWaveform"
|
56
52
|
|
57
53
|
def whatsThis(self):
|
58
54
|
return self.toolTip()
|