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,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_())