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.
- .gitlab-ci.yml +3 -5
- CHANGELOG.md +631 -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 +31 -13
- bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +3 -1
- 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/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.1.dist-info → bec_widgets-2.0.0.dist-info}/METADATA +3 -3
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/RECORD +168 -153
- 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.1.dist-info → bec_widgets-2.0.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,832 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
import pyqtgraph as pg
|
5
|
+
from bec_lib import bec_logger
|
6
|
+
from bec_lib.endpoints import MessageEndpoints
|
7
|
+
from pydantic import BaseModel, Field, field_validator
|
8
|
+
from pydantic_core import PydanticCustomError
|
9
|
+
from qtpy import QtCore, QtGui
|
10
|
+
from qtpy.QtCore import Signal
|
11
|
+
from qtpy.QtGui import QColor
|
12
|
+
from qtpy.QtWidgets import QHBoxLayout, QMainWindow, QWidget
|
13
|
+
|
14
|
+
from bec_widgets.utils import Colors, ConnectionConfig
|
15
|
+
from bec_widgets.utils.colors import set_theme
|
16
|
+
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
17
|
+
from bec_widgets.utils.settings_dialog import SettingsDialog
|
18
|
+
from bec_widgets.utils.toolbar import MaterialIconAction
|
19
|
+
from bec_widgets.widgets.plots.motor_map.settings.motor_map_settings import MotorMapSettings
|
20
|
+
from bec_widgets.widgets.plots.motor_map.toolbar_bundles.motor_selection import (
|
21
|
+
MotorSelectionToolbarBundle,
|
22
|
+
)
|
23
|
+
from bec_widgets.widgets.plots.plot_base import PlotBase
|
24
|
+
|
25
|
+
logger = bec_logger.logger
|
26
|
+
|
27
|
+
|
28
|
+
class FilledRectItem(pg.GraphicsObject):
|
29
|
+
"""
|
30
|
+
Custom rectangle item for the motor map plot defined by 4 points and a brush.
|
31
|
+
"""
|
32
|
+
|
33
|
+
def __init__(self, x: float, y: float, width: float, height: float, brush: QtGui.QBrush):
|
34
|
+
super().__init__()
|
35
|
+
self._rect = QtCore.QRectF(x, y, width, height)
|
36
|
+
self._brush = brush
|
37
|
+
self._pen = pg.mkPen(None)
|
38
|
+
|
39
|
+
def boundingRect(self):
|
40
|
+
return self._rect
|
41
|
+
|
42
|
+
def paint(self, painter, *args):
|
43
|
+
painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True)
|
44
|
+
painter.setBrush(self._brush)
|
45
|
+
painter.setPen(self._pen)
|
46
|
+
painter.drawRect(self.boundingRect())
|
47
|
+
|
48
|
+
|
49
|
+
class MotorConfig(BaseModel):
|
50
|
+
name: str | None = Field(None, description="Motor name.")
|
51
|
+
limits: list[float] | None = Field(None, description="Motor limits.")
|
52
|
+
|
53
|
+
|
54
|
+
# noinspection PyDataclass
|
55
|
+
class MotorMapConfig(ConnectionConfig):
|
56
|
+
x_motor: MotorConfig = Field(default_factory=MotorConfig, description="Motor X name.")
|
57
|
+
y_motor: MotorConfig = Field(default_factory=MotorConfig, description="Motor Y name.")
|
58
|
+
color: str | tuple | None = Field(
|
59
|
+
(255, 255, 255, 255), description="The color of the last point of current position."
|
60
|
+
)
|
61
|
+
scatter_size: int | None = Field(5, description="Size of the scatter points.")
|
62
|
+
max_points: int | None = Field(5000, description="Maximum number of points to display.")
|
63
|
+
num_dim_points: int | None = Field(
|
64
|
+
100,
|
65
|
+
description="Number of points to dim before the color remains same for older recorded position.",
|
66
|
+
)
|
67
|
+
precision: int | None = Field(2, description="Decimal precision of the motor position.")
|
68
|
+
background_value: int | None = Field(
|
69
|
+
25, description="Background value of the motor map. Has to be between 0 and 255."
|
70
|
+
)
|
71
|
+
|
72
|
+
model_config: dict = {"validate_assignment": True}
|
73
|
+
|
74
|
+
_validate_color = field_validator("color")(Colors.validate_color)
|
75
|
+
|
76
|
+
@field_validator("background_value")
|
77
|
+
def validate_background_value(cls, value):
|
78
|
+
if not 0 <= value <= 255:
|
79
|
+
raise PydanticCustomError(
|
80
|
+
"wrong_value", f"'{value}' hs to be between 0 and 255.", {"wrong_value": value}
|
81
|
+
)
|
82
|
+
return value
|
83
|
+
|
84
|
+
|
85
|
+
class MotorMap(PlotBase):
|
86
|
+
"""
|
87
|
+
Motor map widget for plotting motor positions in 2D including a trace of the last points.
|
88
|
+
"""
|
89
|
+
|
90
|
+
PLUGIN = True
|
91
|
+
RPC = True
|
92
|
+
ICON_NAME = "my_location"
|
93
|
+
USER_ACCESS = [
|
94
|
+
# General PlotBase Settings
|
95
|
+
"enable_toolbar",
|
96
|
+
"enable_toolbar.setter",
|
97
|
+
"enable_side_panel",
|
98
|
+
"enable_side_panel.setter",
|
99
|
+
"enable_fps_monitor",
|
100
|
+
"enable_fps_monitor.setter",
|
101
|
+
"set",
|
102
|
+
"title",
|
103
|
+
"title.setter",
|
104
|
+
"x_label",
|
105
|
+
"x_label.setter",
|
106
|
+
"y_label",
|
107
|
+
"y_label.setter",
|
108
|
+
"x_limits",
|
109
|
+
"x_limits.setter",
|
110
|
+
"y_limits",
|
111
|
+
"y_limits.setter",
|
112
|
+
"x_grid",
|
113
|
+
"x_grid.setter",
|
114
|
+
"y_grid",
|
115
|
+
"y_grid.setter",
|
116
|
+
"inner_axes",
|
117
|
+
"inner_axes.setter",
|
118
|
+
"outer_axes",
|
119
|
+
"outer_axes.setter",
|
120
|
+
"lock_aspect_ratio",
|
121
|
+
"lock_aspect_ratio.setter",
|
122
|
+
"auto_range_x",
|
123
|
+
"auto_range_x.setter",
|
124
|
+
"auto_range_y",
|
125
|
+
"auto_range_y.setter",
|
126
|
+
"x_log",
|
127
|
+
"x_log.setter",
|
128
|
+
"y_log",
|
129
|
+
"y_log.setter",
|
130
|
+
"legend_label_size",
|
131
|
+
"legend_label_size.setter",
|
132
|
+
# motor_map specific
|
133
|
+
"color",
|
134
|
+
"color.setter",
|
135
|
+
"max_points",
|
136
|
+
"max_points.setter",
|
137
|
+
"precision",
|
138
|
+
"precision.setter",
|
139
|
+
"num_dim_points",
|
140
|
+
"num_dim_points.setter",
|
141
|
+
"background_value",
|
142
|
+
"background_value.setter",
|
143
|
+
"scatter_size",
|
144
|
+
"scatter_size.setter",
|
145
|
+
"map",
|
146
|
+
"reset_history",
|
147
|
+
"get_data",
|
148
|
+
]
|
149
|
+
|
150
|
+
update_signal = Signal()
|
151
|
+
"""Motor map widget for plotting motor positions."""
|
152
|
+
|
153
|
+
def __init__(
|
154
|
+
self,
|
155
|
+
parent: QWidget | None = None,
|
156
|
+
config: MotorMapConfig | None = None,
|
157
|
+
client=None,
|
158
|
+
gui_id: str | None = None,
|
159
|
+
popups: bool = True,
|
160
|
+
**kwargs,
|
161
|
+
):
|
162
|
+
if config is None:
|
163
|
+
config = MotorMapConfig(widget_class=self.__class__.__name__)
|
164
|
+
super().__init__(
|
165
|
+
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
166
|
+
)
|
167
|
+
|
168
|
+
# Default values for PlotBase
|
169
|
+
self.x_grid = True
|
170
|
+
self.y_grid = True
|
171
|
+
|
172
|
+
# Gui specific
|
173
|
+
self._buffer = {"x": [], "y": []}
|
174
|
+
self._limit_map = None
|
175
|
+
self._trace = None
|
176
|
+
self.v_line = None
|
177
|
+
self.h_line = None
|
178
|
+
self.coord_label = None
|
179
|
+
self.motor_map_settings = None
|
180
|
+
|
181
|
+
# Connect slots
|
182
|
+
self.proxy_update_plot = pg.SignalProxy(
|
183
|
+
self.update_signal, rateLimit=25, slot=self._update_plot
|
184
|
+
)
|
185
|
+
self._add_motor_map_settings()
|
186
|
+
|
187
|
+
################################################################################
|
188
|
+
# Widget Specific GUI interactions
|
189
|
+
################################################################################
|
190
|
+
|
191
|
+
def _init_toolbar(self):
|
192
|
+
"""
|
193
|
+
Initialize the toolbar for the motor map widget.
|
194
|
+
"""
|
195
|
+
self.motor_selection_bundle = MotorSelectionToolbarBundle(
|
196
|
+
bundle_id="motor_selection", target_widget=self
|
197
|
+
)
|
198
|
+
self.toolbar.add_bundle(self.motor_selection_bundle, target_widget=self)
|
199
|
+
super()._init_toolbar()
|
200
|
+
self.toolbar.widgets["reset_legend"].action.setVisible(False)
|
201
|
+
|
202
|
+
self.reset_legend_action = MaterialIconAction(
|
203
|
+
icon_name="history", tooltip="Reset the position of legend."
|
204
|
+
)
|
205
|
+
self.toolbar.add_action_to_bundle(
|
206
|
+
bundle_id="roi",
|
207
|
+
action_id="motor_map_history",
|
208
|
+
action=self.reset_legend_action,
|
209
|
+
target_widget=self,
|
210
|
+
)
|
211
|
+
self.reset_legend_action.action.triggered.connect(self.reset_history)
|
212
|
+
|
213
|
+
def _add_motor_map_settings(self):
|
214
|
+
"""Add the motor map settings to the side panel."""
|
215
|
+
motor_map_settings = MotorMapSettings(parent=self, target_widget=self, popup=False)
|
216
|
+
self.side_panel.add_menu(
|
217
|
+
action_id="motor_map_settings",
|
218
|
+
icon_name="settings_brightness",
|
219
|
+
tooltip="Show Motor Map Settings",
|
220
|
+
widget=motor_map_settings,
|
221
|
+
title="Motor Map Settings",
|
222
|
+
)
|
223
|
+
|
224
|
+
def add_popups(self):
|
225
|
+
"""
|
226
|
+
Add popups to the ScatterWaveform widget.
|
227
|
+
"""
|
228
|
+
super().add_popups()
|
229
|
+
scatter_curve_setting_action = MaterialIconAction(
|
230
|
+
icon_name="settings_brightness",
|
231
|
+
tooltip="Show Motor Map Settings",
|
232
|
+
checkable=True,
|
233
|
+
parent=self,
|
234
|
+
)
|
235
|
+
self.toolbar.add_action_to_bundle(
|
236
|
+
bundle_id="popup_bundle",
|
237
|
+
action_id="motor_map_settings",
|
238
|
+
action=scatter_curve_setting_action,
|
239
|
+
target_widget=self,
|
240
|
+
)
|
241
|
+
self.toolbar.widgets["motor_map_settings"].action.triggered.connect(
|
242
|
+
self.show_motor_map_settings
|
243
|
+
)
|
244
|
+
|
245
|
+
def show_motor_map_settings(self):
|
246
|
+
"""
|
247
|
+
Show the DAP summary popup.
|
248
|
+
"""
|
249
|
+
action = self.toolbar.widgets["motor_map_settings"].action
|
250
|
+
if self.motor_map_settings is None or not self.motor_map_settings.isVisible():
|
251
|
+
motor_map_settings = MotorMapSettings(parent=self, target_widget=self, popup=True)
|
252
|
+
self.motor_map_settings = SettingsDialog(
|
253
|
+
self,
|
254
|
+
settings_widget=motor_map_settings,
|
255
|
+
window_title="Motor Map Settings",
|
256
|
+
modal=False,
|
257
|
+
)
|
258
|
+
self.motor_map_settings.setFixedSize(250, 300)
|
259
|
+
# When the dialog is closed, update the toolbar icon and clear the reference
|
260
|
+
self.motor_map_settings.finished.connect(self._motor_map_settings_closed)
|
261
|
+
self.motor_map_settings.show()
|
262
|
+
action.setChecked(True)
|
263
|
+
else:
|
264
|
+
# If already open, bring it to the front
|
265
|
+
self.motor_map_settings.raise_()
|
266
|
+
self.motor_map_settings.activateWindow()
|
267
|
+
action.setChecked(True) # keep it toggled
|
268
|
+
|
269
|
+
def _motor_map_settings_closed(self):
|
270
|
+
"""
|
271
|
+
Slot for when the axis settings dialog is closed.
|
272
|
+
"""
|
273
|
+
self.motor_map_settings.deleteLater()
|
274
|
+
self.motor_map_settings = None
|
275
|
+
self.toolbar.widgets["motor_map_settings"].action.setChecked(False)
|
276
|
+
|
277
|
+
################################################################################
|
278
|
+
# Widget Specific Properties
|
279
|
+
################################################################################
|
280
|
+
|
281
|
+
# color_scatter for designer, color for CLI to not bother users with QColor
|
282
|
+
@SafeProperty("QColor")
|
283
|
+
def color_scatter(self) -> QtGui.QColor:
|
284
|
+
"""
|
285
|
+
Get the color of the motor trace.
|
286
|
+
|
287
|
+
Returns:
|
288
|
+
QColor: Color of the motor trace.
|
289
|
+
"""
|
290
|
+
return QColor(*self.config.color)
|
291
|
+
|
292
|
+
@color_scatter.setter
|
293
|
+
def color_scatter(self, color: str | tuple | QColor) -> None:
|
294
|
+
"""
|
295
|
+
Set color of the motor trace.
|
296
|
+
|
297
|
+
Args:
|
298
|
+
color(str|tuple): Color of the motor trace. Can be HEX(str) or RGBA(tuple).
|
299
|
+
"""
|
300
|
+
if isinstance(color, str):
|
301
|
+
color = Colors.hex_to_rgba(color, 255)
|
302
|
+
if isinstance(color, QColor):
|
303
|
+
color = (color.red(), color.green(), color.blue(), color.alpha())
|
304
|
+
color = Colors.validate_color(color)
|
305
|
+
self.config.color = color
|
306
|
+
self.update_signal.emit()
|
307
|
+
self.property_changed.emit("color_scatter", color)
|
308
|
+
|
309
|
+
@property
|
310
|
+
def color(self) -> tuple:
|
311
|
+
"""
|
312
|
+
Get the color of the motor trace.
|
313
|
+
|
314
|
+
Returns:
|
315
|
+
tuple: Color of the motor trace.
|
316
|
+
"""
|
317
|
+
return self.config.color
|
318
|
+
|
319
|
+
@color.setter
|
320
|
+
def color(self, color: str | tuple) -> None:
|
321
|
+
"""
|
322
|
+
Set color of the motor trace.
|
323
|
+
|
324
|
+
Args:
|
325
|
+
color(str|tuple): Color of the motor trace. Can be HEX(str) or RGBA(tuple).
|
326
|
+
"""
|
327
|
+
self.color_scatter = color
|
328
|
+
|
329
|
+
@SafeProperty(int)
|
330
|
+
def max_points(self) -> int:
|
331
|
+
"""Get the maximum number of points to display."""
|
332
|
+
return self.config.max_points
|
333
|
+
|
334
|
+
@max_points.setter
|
335
|
+
def max_points(self, max_points: int) -> None:
|
336
|
+
"""
|
337
|
+
Set the maximum number of points to display.
|
338
|
+
|
339
|
+
Args:
|
340
|
+
max_points(int): Maximum number of points to display.
|
341
|
+
"""
|
342
|
+
self.config.max_points = max_points
|
343
|
+
self.update_signal.emit()
|
344
|
+
self.property_changed.emit("max_points", max_points)
|
345
|
+
|
346
|
+
@SafeProperty(int)
|
347
|
+
def precision(self) -> int:
|
348
|
+
"""
|
349
|
+
Set the decimal precision of the motor position.
|
350
|
+
"""
|
351
|
+
return self.config.precision
|
352
|
+
|
353
|
+
@precision.setter
|
354
|
+
def precision(self, precision: int) -> None:
|
355
|
+
"""
|
356
|
+
Set the decimal precision of the motor position.
|
357
|
+
|
358
|
+
Args:
|
359
|
+
precision(int): Decimal precision of the motor position.
|
360
|
+
"""
|
361
|
+
self.config.precision = precision
|
362
|
+
self.update_signal.emit()
|
363
|
+
self.property_changed.emit("precision", precision)
|
364
|
+
|
365
|
+
@SafeProperty(int)
|
366
|
+
def num_dim_points(self) -> int:
|
367
|
+
"""
|
368
|
+
Get the number of dim points for the motor map.
|
369
|
+
"""
|
370
|
+
return self.config.num_dim_points
|
371
|
+
|
372
|
+
@num_dim_points.setter
|
373
|
+
def num_dim_points(self, num_dim_points: int) -> None:
|
374
|
+
"""
|
375
|
+
Set the number of dim points for the motor map.
|
376
|
+
|
377
|
+
Args:
|
378
|
+
num_dim_points(int): Number of dim points.
|
379
|
+
"""
|
380
|
+
self.config.num_dim_points = num_dim_points
|
381
|
+
self.update_signal.emit()
|
382
|
+
self.property_changed.emit("num_dim_points", num_dim_points)
|
383
|
+
|
384
|
+
@SafeProperty(int)
|
385
|
+
def background_value(self) -> int:
|
386
|
+
"""
|
387
|
+
Get the background value of the motor map.
|
388
|
+
"""
|
389
|
+
return self.config.background_value
|
390
|
+
|
391
|
+
@background_value.setter
|
392
|
+
def background_value(self, background_value: int) -> None:
|
393
|
+
"""
|
394
|
+
Set the background value of the motor map.
|
395
|
+
|
396
|
+
Args:
|
397
|
+
background_value(int): Background value of the motor map.
|
398
|
+
"""
|
399
|
+
self.config.background_value = background_value
|
400
|
+
self._swap_limit_map()
|
401
|
+
self.property_changed.emit("background_value", background_value)
|
402
|
+
|
403
|
+
@SafeProperty(int)
|
404
|
+
def scatter_size(self) -> int:
|
405
|
+
"""
|
406
|
+
Get the scatter size of the motor map plot.
|
407
|
+
"""
|
408
|
+
return self.config.scatter_size
|
409
|
+
|
410
|
+
@scatter_size.setter
|
411
|
+
def scatter_size(self, scatter_size: int) -> None:
|
412
|
+
"""
|
413
|
+
Set the scatter size of the motor map plot.
|
414
|
+
|
415
|
+
Args:
|
416
|
+
scatter_size(int): Size of the scatter points.
|
417
|
+
"""
|
418
|
+
self.config.scatter_size = scatter_size
|
419
|
+
self.update_signal.emit()
|
420
|
+
self.property_changed.emit("scatter_size", scatter_size)
|
421
|
+
|
422
|
+
################################################################################
|
423
|
+
# High Level methods for API
|
424
|
+
################################################################################
|
425
|
+
@SafeSlot()
|
426
|
+
def map(self, x_name: str, y_name: str, validate_bec: bool = True) -> None:
|
427
|
+
"""
|
428
|
+
Set the x and y motor names.
|
429
|
+
|
430
|
+
Args:
|
431
|
+
x_name(str): The name of the x motor.
|
432
|
+
y_name(str): The name of the y motor.
|
433
|
+
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
434
|
+
"""
|
435
|
+
self.plot_item.clear()
|
436
|
+
|
437
|
+
if validate_bec:
|
438
|
+
self.entry_validator.validate_signal(x_name, None)
|
439
|
+
self.entry_validator.validate_signal(y_name, None)
|
440
|
+
|
441
|
+
self.config.x_motor.name = x_name
|
442
|
+
self.config.y_motor.name = y_name
|
443
|
+
|
444
|
+
motor_x_limit = self._get_motor_limit(self.config.x_motor.name)
|
445
|
+
motor_y_limit = self._get_motor_limit(self.config.y_motor.name)
|
446
|
+
|
447
|
+
self.config.x_motor.limits = motor_x_limit
|
448
|
+
self.config.y_motor.limits = motor_y_limit
|
449
|
+
|
450
|
+
# reconnect the signals
|
451
|
+
self._connect_motor_to_slots()
|
452
|
+
|
453
|
+
# Reset the buffer
|
454
|
+
self._buffer = {"x": [], "y": []}
|
455
|
+
|
456
|
+
# Redraw the motor map
|
457
|
+
self._make_motor_map()
|
458
|
+
|
459
|
+
self._sync_motor_map_selection_toolbar()
|
460
|
+
|
461
|
+
def reset_history(self):
|
462
|
+
"""
|
463
|
+
Reset the history of the motor map.
|
464
|
+
"""
|
465
|
+
self._buffer["x"] = [self._buffer["x"][-1]]
|
466
|
+
self._buffer["y"] = [self._buffer["y"][-1]]
|
467
|
+
self.update_signal.emit()
|
468
|
+
|
469
|
+
################################################################################
|
470
|
+
# BEC Update Methods
|
471
|
+
################################################################################
|
472
|
+
@SafeSlot()
|
473
|
+
def _update_plot(self, _=None):
|
474
|
+
"""Update the motor map plot."""
|
475
|
+
if self._trace is None:
|
476
|
+
return
|
477
|
+
# If the number of points exceeds max_points, delete the oldest points
|
478
|
+
if len(self._buffer["x"]) > self.config.max_points:
|
479
|
+
self._buffer["x"] = self._buffer["x"][-self.config.max_points :]
|
480
|
+
self._buffer["y"] = self._buffer["y"][-self.config.max_points :]
|
481
|
+
|
482
|
+
x = self._buffer["x"]
|
483
|
+
y = self._buffer["y"]
|
484
|
+
|
485
|
+
# Setup gradient brush for history
|
486
|
+
brushes = [pg.mkBrush(50, 50, 50, 255)] * len(x)
|
487
|
+
|
488
|
+
# RGB color
|
489
|
+
r, g, b, a = self.config.color
|
490
|
+
|
491
|
+
# Calculate the decrement step based on self.num_dim_points
|
492
|
+
num_dim_points = self.config.num_dim_points
|
493
|
+
decrement_step = (255 - 50) / num_dim_points
|
494
|
+
|
495
|
+
for i in range(1, min(num_dim_points + 1, len(x) + 1)):
|
496
|
+
brightness = max(60, 255 - decrement_step * (i - 1))
|
497
|
+
dim_r = int(r * (brightness / 255))
|
498
|
+
dim_g = int(g * (brightness / 255))
|
499
|
+
dim_b = int(b * (brightness / 255))
|
500
|
+
brushes[-i] = pg.mkBrush(dim_r, dim_g, dim_b, a)
|
501
|
+
brushes[-1] = pg.mkBrush(r, g, b, a) # Newest point is always full brightness
|
502
|
+
scatter_size = self.config.scatter_size
|
503
|
+
|
504
|
+
# Update the scatter plot
|
505
|
+
self._trace.setData(x=x, y=y, brush=brushes, pen=None, size=scatter_size)
|
506
|
+
|
507
|
+
# Get last know position for crosshair
|
508
|
+
current_x = x[-1]
|
509
|
+
current_y = y[-1]
|
510
|
+
|
511
|
+
# Update the crosshair
|
512
|
+
self._set_motor_indicator_position(current_x, current_y)
|
513
|
+
|
514
|
+
@SafeSlot(dict, dict)
|
515
|
+
def on_device_readback(self, msg: dict, metadata: dict) -> None:
|
516
|
+
"""
|
517
|
+
Update the motor map plot with the new motor position.
|
518
|
+
|
519
|
+
Args:
|
520
|
+
msg(dict): Message from the device readback.
|
521
|
+
metadata(dict): Metadata of the message.
|
522
|
+
"""
|
523
|
+
x_motor = self.config.x_motor.name
|
524
|
+
y_motor = self.config.y_motor.name
|
525
|
+
|
526
|
+
if x_motor is None or y_motor is None:
|
527
|
+
return
|
528
|
+
|
529
|
+
if x_motor in msg["signals"]:
|
530
|
+
x = msg["signals"][x_motor]["value"]
|
531
|
+
self._buffer["x"].append(x)
|
532
|
+
self._buffer["y"].append(self._buffer["y"][-1])
|
533
|
+
|
534
|
+
elif y_motor in msg["signals"]:
|
535
|
+
y = msg["signals"][y_motor]["value"]
|
536
|
+
self._buffer["y"].append(y)
|
537
|
+
self._buffer["x"].append(self._buffer["x"][-1])
|
538
|
+
|
539
|
+
self.update_signal.emit()
|
540
|
+
|
541
|
+
def _connect_motor_to_slots(self):
|
542
|
+
"""Connect motors to slots."""
|
543
|
+
self._disconnect_current_motors()
|
544
|
+
|
545
|
+
endpoints_readback = [
|
546
|
+
MessageEndpoints.device_readback(self.config.x_motor.name),
|
547
|
+
MessageEndpoints.device_readback(self.config.y_motor.name),
|
548
|
+
]
|
549
|
+
endpoints_limits = [
|
550
|
+
MessageEndpoints.device_limits(self.config.x_motor.name),
|
551
|
+
MessageEndpoints.device_limits(self.config.y_motor.name),
|
552
|
+
]
|
553
|
+
|
554
|
+
self.bec_dispatcher.connect_slot(self.on_device_readback, endpoints_readback)
|
555
|
+
self.bec_dispatcher.connect_slot(self.on_device_limits, endpoints_limits)
|
556
|
+
|
557
|
+
def _disconnect_current_motors(self):
|
558
|
+
"""Disconnect the current motors from the slots."""
|
559
|
+
if self.config.x_motor.name is not None and self.config.y_motor.name is not None:
|
560
|
+
endpoints_readback = [
|
561
|
+
MessageEndpoints.device_readback(self.config.x_motor.name),
|
562
|
+
MessageEndpoints.device_readback(self.config.y_motor.name),
|
563
|
+
]
|
564
|
+
endpoints_limits = [
|
565
|
+
MessageEndpoints.device_limits(self.config.x_motor.name),
|
566
|
+
MessageEndpoints.device_limits(self.config.y_motor.name),
|
567
|
+
]
|
568
|
+
self.bec_dispatcher.disconnect_slot(self.on_device_readback, endpoints_readback)
|
569
|
+
self.bec_dispatcher.disconnect_slot(self.on_device_limits, endpoints_limits)
|
570
|
+
|
571
|
+
################################################################################
|
572
|
+
# Utility Methods
|
573
|
+
################################################################################
|
574
|
+
@SafeSlot(dict, dict)
|
575
|
+
def on_device_limits(self, msg: dict, metadata: dict) -> None:
|
576
|
+
"""
|
577
|
+
Update the motor limits in the config.
|
578
|
+
|
579
|
+
Args:
|
580
|
+
msg(dict): Message from the device limits.
|
581
|
+
metadata(dict): Metadata of the message.
|
582
|
+
"""
|
583
|
+
self.config.x_motor.limits = self._get_motor_limit(self.config.x_motor.name)
|
584
|
+
self.config.y_motor.limits = self._get_motor_limit(self.config.y_motor.name)
|
585
|
+
self._swap_limit_map()
|
586
|
+
|
587
|
+
def _get_motor_limit(self, motor: str) -> list | None:
|
588
|
+
"""
|
589
|
+
Get the motor limit from the config.
|
590
|
+
|
591
|
+
Args:
|
592
|
+
motor(str): Motor name.
|
593
|
+
|
594
|
+
Returns:
|
595
|
+
float: Motor limit.
|
596
|
+
"""
|
597
|
+
try:
|
598
|
+
limits = self.dev[motor].limits
|
599
|
+
if limits == [0, 0]:
|
600
|
+
return None
|
601
|
+
return limits
|
602
|
+
except AttributeError: # TODO maybe not needed, if no limits it returns [0,0]
|
603
|
+
# If the motor doesn't have a 'limits' attribute, return a default value or raise a custom exception
|
604
|
+
logger.error(f"The device '{motor}' does not have defined limits.")
|
605
|
+
return None
|
606
|
+
|
607
|
+
def _make_motor_map(self) -> None:
|
608
|
+
"""
|
609
|
+
Make the motor map.
|
610
|
+
"""
|
611
|
+
|
612
|
+
motor_x_limit = self.config.x_motor.limits
|
613
|
+
motor_y_limit = self.config.y_motor.limits
|
614
|
+
|
615
|
+
self._limit_map = self._make_limit_map(motor_x_limit, motor_y_limit)
|
616
|
+
self.plot_item.addItem(self._limit_map)
|
617
|
+
self._limit_map.setZValue(-1)
|
618
|
+
|
619
|
+
# Create scatter plot
|
620
|
+
scatter_size = self.config.scatter_size
|
621
|
+
self._trace = pg.ScatterPlotItem(size=scatter_size, brush=pg.mkBrush(255, 255, 255, 255))
|
622
|
+
self.plot_item.addItem(self._trace)
|
623
|
+
self._trace.setZValue(0)
|
624
|
+
|
625
|
+
# Add the crosshair for initial motor coordinates
|
626
|
+
initial_position_x = self._get_motor_init_position(
|
627
|
+
self.config.x_motor.name, self.config.precision
|
628
|
+
)
|
629
|
+
initial_position_y = self._get_motor_init_position(
|
630
|
+
self.config.y_motor.name, self.config.precision
|
631
|
+
)
|
632
|
+
|
633
|
+
self._buffer["x"] = [initial_position_x]
|
634
|
+
self._buffer["y"] = [initial_position_y]
|
635
|
+
|
636
|
+
self._trace.setData([initial_position_x], [initial_position_y])
|
637
|
+
|
638
|
+
# Add initial crosshair
|
639
|
+
self._add_coordinates_crosshair(initial_position_x, initial_position_y)
|
640
|
+
|
641
|
+
# Set default labels for the plot
|
642
|
+
self.set_x_label_suffix(f"[{self.config.x_motor.name}-{self.config.x_motor.name}]")
|
643
|
+
self.set_y_label_suffix(f"[{self.config.y_motor.name}-{self.config.y_motor.name}]")
|
644
|
+
|
645
|
+
self.update_signal.emit()
|
646
|
+
|
647
|
+
def _add_coordinates_crosshair(self, x: float, y: float) -> None:
|
648
|
+
"""
|
649
|
+
Add position crosshair indicator to the plot.
|
650
|
+
|
651
|
+
Args:
|
652
|
+
x(float): X coordinate of the crosshair.
|
653
|
+
y(float): Y coordinate of the crosshair.
|
654
|
+
"""
|
655
|
+
if self.v_line is not None and self.h_line is not None and self.coord_label is not None:
|
656
|
+
self.plot_item.removeItem(self.h_line)
|
657
|
+
self.plot_item.removeItem(self.v_line)
|
658
|
+
self.plot_item.removeItem(self.coord_label)
|
659
|
+
|
660
|
+
self.h_line = pg.InfiniteLine(
|
661
|
+
angle=0, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
|
662
|
+
)
|
663
|
+
self.v_line = pg.InfiniteLine(
|
664
|
+
angle=90, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
|
665
|
+
)
|
666
|
+
|
667
|
+
self.coord_label = pg.TextItem("", anchor=(1, 1), fill=(0, 0, 0, 100))
|
668
|
+
|
669
|
+
# Add crosshair to the plot
|
670
|
+
self.plot_item.addItem(self.h_line)
|
671
|
+
self.plot_item.addItem(self.v_line)
|
672
|
+
self.plot_item.addItem(self.coord_label)
|
673
|
+
|
674
|
+
self._set_motor_indicator_position(x, y)
|
675
|
+
|
676
|
+
def _set_motor_indicator_position(self, x: float, y: float) -> None:
|
677
|
+
"""
|
678
|
+
Set the position of the motor indicator.
|
679
|
+
|
680
|
+
Args:
|
681
|
+
x(float): X coordinate of the motor indicator.
|
682
|
+
y(float): Y coordinate of the motor indicator.
|
683
|
+
"""
|
684
|
+
if self.v_line is None or self.h_line is None or self.coord_label is None:
|
685
|
+
return
|
686
|
+
|
687
|
+
text = f"({x:.{self.config.precision}f}, {y:.{self.config.precision}f})"
|
688
|
+
|
689
|
+
self.v_line.setPos(x)
|
690
|
+
self.h_line.setPos(y)
|
691
|
+
self.coord_label.setText(text)
|
692
|
+
self.coord_label.setPos(x, y)
|
693
|
+
|
694
|
+
def _make_limit_map(self, limits_x: list | None, limits_y: list | None) -> FilledRectItem:
|
695
|
+
"""
|
696
|
+
Create a limit map for the motor map plot. Each limit can be:
|
697
|
+
- [int, int]
|
698
|
+
- [None, None]
|
699
|
+
- [int, None]
|
700
|
+
- [None, int]
|
701
|
+
- or None
|
702
|
+
If any element of a limit list is None, it is treated as unbounded,
|
703
|
+
and replaced with ±1e6 (or any large float of your choice).
|
704
|
+
|
705
|
+
Args:
|
706
|
+
limits_x(list): Motor limits for the x-axis.
|
707
|
+
limits_y(list): Motor limits for the y-axis.
|
708
|
+
|
709
|
+
Returns:
|
710
|
+
FilledRectItem: Limit map.
|
711
|
+
"""
|
712
|
+
|
713
|
+
def fix_limit_pair(limits):
|
714
|
+
if not limits:
|
715
|
+
return [-1e6, 1e6]
|
716
|
+
low, high = limits
|
717
|
+
if low is None:
|
718
|
+
low = -1e6
|
719
|
+
if high is None:
|
720
|
+
high = 1e6
|
721
|
+
return [low, high]
|
722
|
+
|
723
|
+
limits_x = fix_limit_pair(limits_x)
|
724
|
+
limits_y = fix_limit_pair(limits_y)
|
725
|
+
|
726
|
+
limit_x_min, limit_x_max = limits_x
|
727
|
+
limit_y_min, limit_y_max = limits_y
|
728
|
+
|
729
|
+
rect_width = limit_x_max - limit_x_min
|
730
|
+
rect_height = limit_y_max - limit_y_min
|
731
|
+
background_value = self.config.background_value
|
732
|
+
|
733
|
+
brush_color = pg.mkBrush(background_value, background_value, background_value, 150)
|
734
|
+
|
735
|
+
filled_rect = FilledRectItem(
|
736
|
+
x=limit_x_min, y=limit_y_min, width=rect_width, height=rect_height, brush=brush_color
|
737
|
+
)
|
738
|
+
return filled_rect
|
739
|
+
|
740
|
+
def _swap_limit_map(self):
|
741
|
+
"""Swap the limit map."""
|
742
|
+
self.plot_item.removeItem(self._limit_map)
|
743
|
+
x_limits = self.config.x_motor.limits
|
744
|
+
y_limits = self.config.y_motor.limits
|
745
|
+
if x_limits is not None and y_limits is not None:
|
746
|
+
self._limit_map = self._make_limit_map(x_limits, y_limits)
|
747
|
+
self._limit_map.setZValue(-1)
|
748
|
+
self.plot_item.addItem(self._limit_map)
|
749
|
+
|
750
|
+
def _get_motor_init_position(self, name: str, precision: int) -> float:
|
751
|
+
"""
|
752
|
+
Get the motor initial position from the config.
|
753
|
+
|
754
|
+
Args:
|
755
|
+
name(str): Motor name.
|
756
|
+
precision(int): Decimal precision of the motor position.
|
757
|
+
|
758
|
+
Returns:
|
759
|
+
float: Motor initial position.
|
760
|
+
"""
|
761
|
+
entry = self.entry_validator.validate_signal(name, None)
|
762
|
+
init_position = round(float(self.dev[name].read()[entry]["value"]), precision)
|
763
|
+
return init_position
|
764
|
+
|
765
|
+
def _sync_motor_map_selection_toolbar(self):
|
766
|
+
"""
|
767
|
+
Sync the motor map selection toolbar with the current motor map.
|
768
|
+
"""
|
769
|
+
if self.motor_selection_bundle is not None:
|
770
|
+
motor_x = self.motor_selection_bundle.motor_x.currentText()
|
771
|
+
motor_y = self.motor_selection_bundle.motor_y.currentText()
|
772
|
+
|
773
|
+
if motor_x != self.config.x_motor.name:
|
774
|
+
self.motor_selection_bundle.motor_x.blockSignals(True)
|
775
|
+
self.motor_selection_bundle.motor_x.set_device(self.config.x_motor.name)
|
776
|
+
self.motor_selection_bundle.motor_x.check_validity(self.config.x_motor.name)
|
777
|
+
self.motor_selection_bundle.motor_x.blockSignals(False)
|
778
|
+
if motor_y != self.config.y_motor.name:
|
779
|
+
self.motor_selection_bundle.motor_y.blockSignals(True)
|
780
|
+
self.motor_selection_bundle.motor_y.set_device(self.config.y_motor.name)
|
781
|
+
self.motor_selection_bundle.motor_y.check_validity(self.config.y_motor.name)
|
782
|
+
self.motor_selection_bundle.motor_y.blockSignals(False)
|
783
|
+
|
784
|
+
################################################################################
|
785
|
+
# Export Methods
|
786
|
+
################################################################################
|
787
|
+
|
788
|
+
def get_data(self) -> dict:
|
789
|
+
"""
|
790
|
+
Get the data of the motor map.
|
791
|
+
|
792
|
+
Returns:
|
793
|
+
dict: Data of the motor map.
|
794
|
+
"""
|
795
|
+
data = {"x": self._buffer["x"], "y": self._buffer["y"]}
|
796
|
+
return data
|
797
|
+
|
798
|
+
def cleanup(self):
|
799
|
+
self.motor_selection_bundle.cleanup()
|
800
|
+
super().cleanup()
|
801
|
+
|
802
|
+
|
803
|
+
class DemoApp(QMainWindow): # pragma: no cover
|
804
|
+
def __init__(self):
|
805
|
+
super().__init__()
|
806
|
+
self.setWindowTitle("Waveform Demo")
|
807
|
+
self.resize(800, 600)
|
808
|
+
self.main_widget = QWidget()
|
809
|
+
self.layout = QHBoxLayout(self.main_widget)
|
810
|
+
self.setCentralWidget(self.main_widget)
|
811
|
+
|
812
|
+
self.motor_map_popup = MotorMap(popups=True)
|
813
|
+
self.motor_map_popup.map(x_name="samx", y_name="samy", validate_bec=True)
|
814
|
+
|
815
|
+
self.motor_map_side = MotorMap(popups=False)
|
816
|
+
self.motor_map_side.map(x_name="samx", y_name="samy", validate_bec=True)
|
817
|
+
|
818
|
+
self.layout.addWidget(self.motor_map_side)
|
819
|
+
self.layout.addWidget(self.motor_map_popup)
|
820
|
+
|
821
|
+
|
822
|
+
if __name__ == "__main__": # pragma: no cover
|
823
|
+
import sys
|
824
|
+
|
825
|
+
from qtpy.QtWidgets import QApplication
|
826
|
+
|
827
|
+
app = QApplication(sys.argv)
|
828
|
+
set_theme("dark")
|
829
|
+
widget = DemoApp()
|
830
|
+
widget.show()
|
831
|
+
widget.resize(1400, 600)
|
832
|
+
sys.exit(app.exec_())
|