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.
Files changed (197) hide show
  1. .gitlab-ci.yml +11 -6
  2. CHANGELOG.md +650 -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 +37 -18
  60. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +28 -4
  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/spinner/spinner.py +2 -2
  143. bec_widgets/widgets/utility/visual/color_button/color_button.py +1 -1
  144. bec_widgets/widgets/utility/visual/colormap_widget/colormap_widget.py +4 -6
  145. bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py +4 -8
  146. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/METADATA +3 -3
  147. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/RECORD +169 -154
  148. pyproject.toml +3 -3
  149. bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +0 -198
  150. bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +0 -615
  151. bec_widgets/applications/bec_app.py +0 -84
  152. bec_widgets/cli/auto_updates.py +0 -168
  153. bec_widgets/widgets/containers/figure/__init__.py +0 -1
  154. bec_widgets/widgets/containers/figure/figure.py +0 -796
  155. bec_widgets/widgets/containers/figure/plots/axis_settings.py +0 -91
  156. bec_widgets/widgets/containers/figure/plots/axis_settings.ui +0 -256
  157. bec_widgets/widgets/containers/figure/plots/image/image.py +0 -772
  158. bec_widgets/widgets/containers/figure/plots/image/image_item.py +0 -337
  159. bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py +0 -525
  160. bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py +0 -340
  161. bec_widgets/widgets/containers/figure/plots/plot_base.py +0 -505
  162. bec_widgets/widgets/containers/figure/plots/waveform/waveform.py +0 -1563
  163. bec_widgets/widgets/plots/image/bec_image_widget.pyproject +0 -1
  164. bec_widgets/widgets/plots/image/image_widget.py +0 -515
  165. bec_widgets/widgets/plots/motor_map/bec_motor_map_widget.pyproject +0 -1
  166. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.py +0 -56
  167. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.ui +0 -108
  168. bec_widgets/widgets/plots/motor_map/motor_map_widget.py +0 -234
  169. bec_widgets/widgets/plots/multi_waveform/bec_multi_waveform_widget.pyproject +0 -1
  170. bec_widgets/widgets/plots/multi_waveform/multi_waveform_controls.ui +0 -99
  171. bec_widgets/widgets/plots/multi_waveform/multi_waveform_widget.py +0 -536
  172. bec_widgets/widgets/plots/waveform/bec_waveform_widget.pyproject +0 -1
  173. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.py +0 -336
  174. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.ui +0 -372
  175. bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py +0 -25
  176. bec_widgets/widgets/plots/waveform/waveform_widget.py +0 -751
  177. /bec_widgets/{qt_utils → utils}/collapsible_panel_manager.py +0 -0
  178. /bec_widgets/{applications/alignment → utils/forms_from_types}/__init__.py +0 -0
  179. /bec_widgets/{qt_utils → utils}/redis_message_waiter.py +0 -0
  180. /bec_widgets/{applications/alignment/alignment_1d → widgets/containers/auto_update}/__init__.py +0 -0
  181. /bec_widgets/{qt_utils → widgets/containers/main_window/addons}/__init__.py +0 -0
  182. /bec_widgets/widgets/{containers/figure/plots → plots/image/toolbar_bundles}/__init__.py +0 -0
  183. /bec_widgets/widgets/{containers/figure/plots/image → plots/motor_map/settings}/__init__.py +0 -0
  184. /bec_widgets/widgets/{containers/figure/plots/motor_map → plots/motor_map/toolbar_bundles}/__init__.py +0 -0
  185. /bec_widgets/widgets/{containers/figure/plots/multi_waveform → plots/multi_waveform/settings}/__init__.py +0 -0
  186. /bec_widgets/widgets/{containers/figure/plots/waveform → plots/multi_waveform/toolbar_bundles}/__init__.py +0 -0
  187. /bec_widgets/widgets/plots/{motor_map/motor_map_dialog → scatter_waveform}/__init__.py +0 -0
  188. /bec_widgets/widgets/plots/{waveform/waveform_popups → scatter_waveform/settings}/__init__.py +0 -0
  189. /bec_widgets/widgets/plots/{waveform/waveform_popups/curve_dialog → setting_menus}/__init__.py +0 -0
  190. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_horizontal.ui +0 -0
  191. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_vertical.ui +0 -0
  192. /bec_widgets/widgets/plots/{waveform/waveform_popups/dap_summary_dialog → toolbar_bundles}/__init__.py +0 -0
  193. /bec_widgets/widgets/{plots_next_gen/setting_menus → plots/waveform/settings}/__init__.py +0 -0
  194. /bec_widgets/widgets/{plots_next_gen/toolbar_bundles → plots/waveform/settings/curve_settings}/__init__.py +0 -0
  195. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/WHEEL +0 -0
  196. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/entry_points.txt +0 -0
  197. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,960 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ import numpy as np
6
+ import pyqtgraph as pg
7
+ from bec_lib import bec_logger
8
+ from bec_lib.endpoints import MessageEndpoints
9
+ from pydantic import Field, ValidationError, field_validator
10
+ from qtpy.QtCore import QPointF, Signal
11
+ from qtpy.QtWidgets import QWidget
12
+
13
+ from bec_widgets.utils import ConnectionConfig
14
+ from bec_widgets.utils.colors import Colors
15
+ from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
16
+ from bec_widgets.utils.toolbar import MaterialIconAction, SwitchableToolBarAction
17
+ from bec_widgets.widgets.plots.image.image_item import ImageItem
18
+ from bec_widgets.widgets.plots.image.toolbar_bundles.image_selection import (
19
+ MonitorSelectionToolbarBundle,
20
+ )
21
+ from bec_widgets.widgets.plots.image.toolbar_bundles.processing import ImageProcessingToolbarBundle
22
+ from bec_widgets.widgets.plots.plot_base import PlotBase
23
+
24
+ logger = bec_logger.logger
25
+
26
+
27
+ # noinspection PyDataclass
28
+ class ImageConfig(ConnectionConfig):
29
+ color_map: str = Field(
30
+ "plasma", description="The colormap of the figure widget.", validate_default=True
31
+ )
32
+ color_bar: Literal["full", "simple"] | None = Field(
33
+ None, description="The type of the color bar."
34
+ )
35
+ lock_aspect_ratio: bool = Field(
36
+ False, description="Whether to lock the aspect ratio of the image."
37
+ )
38
+
39
+ model_config: dict = {"validate_assignment": True}
40
+ _validate_color_map = field_validator("color_map")(Colors.validate_color_map)
41
+
42
+
43
+ class Image(PlotBase):
44
+ """
45
+ Image widget for displaying 2D data.
46
+ """
47
+
48
+ PLUGIN = True
49
+ RPC = True
50
+ ICON_NAME = "image"
51
+ USER_ACCESS = [
52
+ # General PlotBase Settings
53
+ "enable_toolbar",
54
+ "enable_toolbar.setter",
55
+ "enable_side_panel",
56
+ "enable_side_panel.setter",
57
+ "enable_fps_monitor",
58
+ "enable_fps_monitor.setter",
59
+ "set",
60
+ "title",
61
+ "title.setter",
62
+ "x_label",
63
+ "x_label.setter",
64
+ "y_label",
65
+ "y_label.setter",
66
+ "x_limits",
67
+ "x_limits.setter",
68
+ "y_limits",
69
+ "y_limits.setter",
70
+ "x_grid",
71
+ "x_grid.setter",
72
+ "y_grid",
73
+ "y_grid.setter",
74
+ "inner_axes",
75
+ "inner_axes.setter",
76
+ "outer_axes",
77
+ "outer_axes.setter",
78
+ "auto_range_x",
79
+ "auto_range_x.setter",
80
+ "auto_range_y",
81
+ "auto_range_y.setter",
82
+ # ImageView Specific Settings
83
+ "color_map",
84
+ "color_map.setter",
85
+ "vrange",
86
+ "vrange.setter",
87
+ "v_min",
88
+ "v_min.setter",
89
+ "v_max",
90
+ "v_max.setter",
91
+ "lock_aspect_ratio",
92
+ "lock_aspect_ratio.setter",
93
+ "autorange",
94
+ "autorange.setter",
95
+ "autorange_mode",
96
+ "autorange_mode.setter",
97
+ "monitor",
98
+ "monitor.setter",
99
+ "enable_colorbar",
100
+ "enable_simple_colorbar",
101
+ "enable_simple_colorbar.setter",
102
+ "enable_full_colorbar",
103
+ "enable_full_colorbar.setter",
104
+ "fft",
105
+ "fft.setter",
106
+ "log",
107
+ "log.setter",
108
+ "num_rotation_90",
109
+ "num_rotation_90.setter",
110
+ "transpose",
111
+ "transpose.setter",
112
+ "image",
113
+ "main_image",
114
+ ]
115
+ sync_colorbar_with_autorange = Signal()
116
+
117
+ def __init__(
118
+ self,
119
+ parent: QWidget | None = None,
120
+ config: ImageConfig | None = None,
121
+ client=None,
122
+ gui_id: str | None = None,
123
+ popups: bool = True,
124
+ **kwargs,
125
+ ):
126
+ if config is None:
127
+ config = ImageConfig(widget_class=self.__class__.__name__)
128
+ self.gui_id = config.gui_id
129
+ self._color_bar = None
130
+ self._main_image = ImageItem()
131
+ super().__init__(
132
+ parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
133
+ )
134
+ self._main_image = ImageItem(parent_image=self)
135
+
136
+ self.plot_item.addItem(self._main_image)
137
+ self.scan_id = None
138
+
139
+ # Default Color map to plasma
140
+ self.color_map = "plasma"
141
+
142
+ ################################################################################
143
+ # Widget Specific GUI interactions
144
+ ################################################################################
145
+ def _init_toolbar(self):
146
+
147
+ # add to the first position
148
+ self.selection_bundle = MonitorSelectionToolbarBundle(
149
+ bundle_id="selection", target_widget=self
150
+ )
151
+ self.toolbar.add_bundle(self.selection_bundle, self)
152
+
153
+ super()._init_toolbar()
154
+
155
+ # Image specific changes to PlotBase toolbar
156
+ self.toolbar.widgets["reset_legend"].action.setVisible(False)
157
+
158
+ # Lock aspect ratio button
159
+ self.lock_aspect_ratio_action = MaterialIconAction(
160
+ icon_name="aspect_ratio", tooltip="Lock Aspect Ratio", checkable=True, parent=self
161
+ )
162
+ self.toolbar.add_action_to_bundle(
163
+ bundle_id="mouse_interaction",
164
+ action_id="lock_aspect_ratio",
165
+ action=self.lock_aspect_ratio_action,
166
+ target_widget=self,
167
+ )
168
+ self.lock_aspect_ratio_action.action.toggled.connect(
169
+ lambda checked: self.setProperty("lock_aspect_ratio", checked)
170
+ )
171
+ self.lock_aspect_ratio_action.action.setChecked(True)
172
+
173
+ self._init_autorange_action()
174
+ self._init_colorbar_action()
175
+
176
+ # Processing Bundle
177
+ self.processing_bundle = ImageProcessingToolbarBundle(
178
+ bundle_id="processing", target_widget=self
179
+ )
180
+ self.toolbar.add_bundle(self.processing_bundle, target_widget=self)
181
+
182
+ def _init_autorange_action(self):
183
+
184
+ self.autorange_mean_action = MaterialIconAction(
185
+ icon_name="hdr_auto", tooltip="Enable Auto Range (Mean)", checkable=True, parent=self
186
+ )
187
+ self.autorange_max_action = MaterialIconAction(
188
+ icon_name="hdr_auto",
189
+ tooltip="Enable Auto Range (Max)",
190
+ checkable=True,
191
+ filled=True,
192
+ parent=self,
193
+ )
194
+
195
+ self.autorange_switch = SwitchableToolBarAction(
196
+ actions={
197
+ "auto_range_mean": self.autorange_mean_action,
198
+ "auto_range_max": self.autorange_max_action,
199
+ },
200
+ initial_action="auto_range_mean",
201
+ tooltip="Enable Auto Range",
202
+ checkable=True,
203
+ parent=self,
204
+ )
205
+
206
+ self.toolbar.add_action_to_bundle(
207
+ bundle_id="roi",
208
+ action_id="autorange_image",
209
+ action=self.autorange_switch,
210
+ target_widget=self,
211
+ )
212
+
213
+ self.autorange_mean_action.action.toggled.connect(
214
+ lambda checked: self.toggle_autorange(checked, mode="mean")
215
+ )
216
+ self.autorange_max_action.action.toggled.connect(
217
+ lambda checked: self.toggle_autorange(checked, mode="max")
218
+ )
219
+
220
+ self.autorange = True
221
+ self.autorange_mode = "mean"
222
+
223
+ def _init_colorbar_action(self):
224
+ self.full_colorbar_action = MaterialIconAction(
225
+ icon_name="edgesensor_low", tooltip="Enable Full Colorbar", checkable=True, parent=self
226
+ )
227
+ self.simple_colorbar_action = MaterialIconAction(
228
+ icon_name="smartphone", tooltip="Enable Simple Colorbar", checkable=True, parent=self
229
+ )
230
+
231
+ self.colorbar_switch = SwitchableToolBarAction(
232
+ actions={
233
+ "full_colorbar": self.full_colorbar_action,
234
+ "simple_colorbar": self.simple_colorbar_action,
235
+ },
236
+ initial_action="full_colorbar",
237
+ tooltip="Enable Full Colorbar",
238
+ checkable=True,
239
+ parent=self,
240
+ )
241
+
242
+ self.toolbar.add_action_to_bundle(
243
+ bundle_id="roi",
244
+ action_id="switch_colorbar",
245
+ action=self.colorbar_switch,
246
+ target_widget=self,
247
+ )
248
+
249
+ self.simple_colorbar_action.action.toggled.connect(
250
+ lambda checked: self.enable_colorbar(checked, style="simple")
251
+ )
252
+ self.full_colorbar_action.action.toggled.connect(
253
+ lambda checked: self.enable_colorbar(checked, style="full")
254
+ )
255
+
256
+ def enable_colorbar(
257
+ self,
258
+ enabled: bool,
259
+ style: Literal["full", "simple"] = "full",
260
+ vrange: tuple[int, int] | None = None,
261
+ ):
262
+ """
263
+ Enable the colorbar and switch types of colorbars.
264
+
265
+ Args:
266
+ enabled(bool): Whether to enable the colorbar.
267
+ style(Literal["full", "simple"]): The type of colorbar to enable.
268
+ vrange(tuple): The range of values to use for the colorbar.
269
+ """
270
+ autorange_state = self._main_image.autorange
271
+ if enabled:
272
+ if self._color_bar:
273
+ if self.config.color_bar == "full":
274
+ self.cleanup_histogram_lut_item(self._color_bar)
275
+ self.plot_widget.removeItem(self._color_bar)
276
+ self._color_bar = None
277
+
278
+ if style == "simple":
279
+ self._color_bar = pg.ColorBarItem(colorMap=self.config.color_map)
280
+ self._color_bar.setImageItem(self._main_image)
281
+ self._color_bar.sigLevelsChangeFinished.connect(
282
+ lambda: self.setProperty("autorange", False)
283
+ )
284
+
285
+ elif style == "full":
286
+ self._color_bar = pg.HistogramLUTItem()
287
+ self._color_bar.setImageItem(self._main_image)
288
+ self._color_bar.gradient.loadPreset(self.config.color_map)
289
+ self._color_bar.sigLevelsChanged.connect(
290
+ lambda: self.setProperty("autorange", False)
291
+ )
292
+
293
+ self.plot_widget.addItem(self._color_bar, row=0, col=1)
294
+ self.config.color_bar = style
295
+ else:
296
+ if self._color_bar:
297
+ self.plot_widget.removeItem(self._color_bar)
298
+ self._color_bar = None
299
+ self.config.color_bar = None
300
+
301
+ self.autorange = autorange_state
302
+ self._sync_colorbar_actions()
303
+
304
+ if vrange: # should be at the end to disable the autorange if defined
305
+ self.v_range = vrange
306
+
307
+ ################################################################################
308
+ # Widget Specific Properties
309
+ ################################################################################
310
+
311
+ ################################################################################
312
+ # Colorbar toggle
313
+
314
+ @SafeProperty(bool)
315
+ def enable_simple_colorbar(self) -> bool:
316
+ """
317
+ Enable the simple colorbar.
318
+ """
319
+ enabled = False
320
+ if self.config.color_bar == "simple":
321
+ enabled = True
322
+ return enabled
323
+
324
+ @enable_simple_colorbar.setter
325
+ def enable_simple_colorbar(self, value: bool):
326
+ """
327
+ Enable the simple colorbar.
328
+
329
+ Args:
330
+ value(bool): Whether to enable the simple colorbar.
331
+ """
332
+ self.enable_colorbar(enabled=value, style="simple")
333
+
334
+ @SafeProperty(bool)
335
+ def enable_full_colorbar(self) -> bool:
336
+ """
337
+ Enable the full colorbar.
338
+ """
339
+ enabled = False
340
+ if self.config.color_bar == "full":
341
+ enabled = True
342
+ return enabled
343
+
344
+ @enable_full_colorbar.setter
345
+ def enable_full_colorbar(self, value: bool):
346
+ """
347
+ Enable the full colorbar.
348
+
349
+ Args:
350
+ value(bool): Whether to enable the full colorbar.
351
+ """
352
+ self.enable_colorbar(enabled=value, style="full")
353
+
354
+ ################################################################################
355
+ # Appearance
356
+
357
+ @SafeProperty(str)
358
+ def color_map(self) -> str:
359
+ """
360
+ Set the color map of the image.
361
+ """
362
+ return self.config.color_map
363
+
364
+ @color_map.setter
365
+ def color_map(self, value: str):
366
+ """
367
+ Set the color map of the image.
368
+
369
+ Args:
370
+ value(str): The color map to set.
371
+ """
372
+ try:
373
+ self.config.color_map = value
374
+ self._main_image.color_map = value
375
+
376
+ if self._color_bar:
377
+ if self.config.color_bar == "simple":
378
+ self._color_bar.setColorMap(value)
379
+ elif self.config.color_bar == "full":
380
+ self._color_bar.gradient.loadPreset(value)
381
+ except ValidationError:
382
+ return
383
+
384
+ # v_range is for designer, vrange is for RPC
385
+ @SafeProperty("QPointF")
386
+ def v_range(self) -> QPointF:
387
+ """
388
+ Set the v_range of the main image.
389
+ """
390
+ vmin, vmax = self._main_image.v_range
391
+ return QPointF(vmin, vmax)
392
+
393
+ @v_range.setter
394
+ def v_range(self, value: tuple | list | QPointF):
395
+ """
396
+ Set the v_range of the main image.
397
+
398
+ Args:
399
+ value(tuple | list | QPointF): The range of values to set.
400
+ """
401
+ if isinstance(value, (tuple, list)):
402
+ value = self._tuple_to_qpointf(value)
403
+
404
+ vmin, vmax = value.x(), value.y()
405
+
406
+ self._main_image.v_range = (vmin, vmax)
407
+
408
+ # propagate to colorbar if exists
409
+ if self._color_bar:
410
+ if self.config.color_bar == "simple":
411
+ self._color_bar.setLevels(low=vmin, high=vmax)
412
+ elif self.config.color_bar == "full":
413
+ self._color_bar.setLevels(min=vmin, max=vmax)
414
+ self._color_bar.setHistogramRange(vmin - 0.1 * vmin, vmax + 0.1 * vmax)
415
+
416
+ self.autorange_switch.set_state_all(False)
417
+
418
+ @property
419
+ def vrange(self) -> tuple:
420
+ """
421
+ Get the vrange of the image.
422
+ """
423
+ return (self.v_range.x(), self.v_range.y())
424
+
425
+ @vrange.setter
426
+ def vrange(self, value):
427
+ """
428
+ Set the vrange of the image.
429
+
430
+ Args:
431
+ value(tuple):
432
+ """
433
+ self.v_range = value
434
+
435
+ @property
436
+ def v_min(self) -> float:
437
+ """
438
+ Get the minimum value of the v_range.
439
+ """
440
+ return self.v_range.x()
441
+
442
+ @v_min.setter
443
+ def v_min(self, value: float):
444
+ """
445
+ Set the minimum value of the v_range.
446
+
447
+ Args:
448
+ value(float): The minimum value to set.
449
+ """
450
+ self.v_range = (value, self.v_range.y())
451
+
452
+ @property
453
+ def v_max(self) -> float:
454
+ """
455
+ Get the maximum value of the v_range.
456
+ """
457
+ return self.v_range.y()
458
+
459
+ @v_max.setter
460
+ def v_max(self, value: float):
461
+ """
462
+ Set the maximum value of the v_range.
463
+
464
+ Args:
465
+ value(float): The maximum value to set.
466
+ """
467
+ self.v_range = (self.v_range.x(), value)
468
+
469
+ @SafeProperty(bool)
470
+ def lock_aspect_ratio(self) -> bool:
471
+ """
472
+ Whether the aspect ratio is locked.
473
+ """
474
+ return self.config.lock_aspect_ratio
475
+
476
+ @lock_aspect_ratio.setter
477
+ def lock_aspect_ratio(self, value: bool):
478
+ """
479
+ Set the aspect ratio lock.
480
+
481
+ Args:
482
+ value(bool): Whether to lock the aspect ratio.
483
+ """
484
+ self.config.lock_aspect_ratio = bool(value)
485
+ self.plot_item.setAspectLocked(value)
486
+
487
+ ################################################################################
488
+ # Data Acquisition
489
+
490
+ @SafeProperty(str)
491
+ def monitor(self) -> str:
492
+ """
493
+ The name of the monitor to use for the image.
494
+ """
495
+ return self._main_image.config.monitor
496
+
497
+ @monitor.setter
498
+ def monitor(self, value: str):
499
+ """
500
+ Set the monitor for the image.
501
+
502
+ Args:
503
+ value(str): The name of the monitor to set.
504
+ """
505
+ if self._main_image.config.monitor == value:
506
+ return
507
+ try:
508
+ self.entry_validator.validate_monitor(value)
509
+ except ValueError:
510
+ return
511
+ self.image(monitor=value)
512
+
513
+ @property
514
+ def main_image(self) -> ImageItem:
515
+ """Access the main image item."""
516
+ return self._main_image
517
+
518
+ ################################################################################
519
+ # Autorange + Colorbar sync
520
+
521
+ @SafeProperty(bool)
522
+ def autorange(self) -> bool:
523
+ """
524
+ Whether autorange is enabled.
525
+ """
526
+ return self._main_image.autorange
527
+
528
+ @autorange.setter
529
+ def autorange(self, enabled: bool):
530
+ """
531
+ Set autorange.
532
+
533
+ Args:
534
+ enabled(bool): Whether to enable autorange.
535
+ """
536
+ self._main_image.autorange = enabled
537
+ if enabled and self._main_image.raw_data is not None:
538
+ self._main_image.apply_autorange()
539
+ self._sync_colorbar_levels()
540
+ self._sync_autorange_switch()
541
+
542
+ @SafeProperty(str)
543
+ def autorange_mode(self) -> str:
544
+ """
545
+ Autorange mode.
546
+
547
+ Options:
548
+ - "max": Use the maximum value of the image for autoranging.
549
+ - "mean": Use the mean value of the image for autoranging.
550
+
551
+ """
552
+ return self._main_image.autorange_mode
553
+
554
+ @autorange_mode.setter
555
+ def autorange_mode(self, mode: str):
556
+ """
557
+ Set the autorange mode.
558
+
559
+ Args:
560
+ mode(str): The autorange mode. Options are "max" or "mean".
561
+ """
562
+ # for qt Designer
563
+ if mode not in ["max", "mean"]:
564
+ return
565
+ self._main_image.autorange_mode = mode
566
+
567
+ self._sync_autorange_switch()
568
+
569
+ @SafeSlot(bool, str, bool)
570
+ def toggle_autorange(self, enabled: bool, mode: str):
571
+ """
572
+ Toggle autorange.
573
+
574
+ Args:
575
+ enabled(bool): Whether to enable autorange.
576
+ mode(str): The autorange mode. Options are "max" or "mean".
577
+ """
578
+ if self._main_image is not None:
579
+ self._main_image.autorange = enabled
580
+ self._main_image.autorange_mode = mode
581
+ if enabled:
582
+ self._main_image.apply_autorange()
583
+ self._sync_colorbar_levels()
584
+
585
+ def _sync_autorange_switch(self):
586
+ """
587
+ Synchronize the autorange switch with the current autorange state and mode if changed from outside.
588
+ """
589
+ self.autorange_switch.block_all_signals(True)
590
+ self.autorange_switch.set_default_action(f"auto_range_{self._main_image.autorange_mode}")
591
+ self.autorange_switch.set_state_all(self._main_image.autorange)
592
+ self.autorange_switch.block_all_signals(False)
593
+
594
+ def _sync_colorbar_levels(self):
595
+ """Immediately propagate current levels to the active colorbar."""
596
+ vrange = self._main_image.v_range
597
+ if self._color_bar:
598
+ self._color_bar.blockSignals(True)
599
+ self.v_range = vrange
600
+ self._color_bar.blockSignals(False)
601
+
602
+ def _sync_colorbar_actions(self):
603
+ """
604
+ Synchronize the colorbar actions with the current colorbar state.
605
+ """
606
+ self.colorbar_switch.block_all_signals(True)
607
+ if self._color_bar is not None:
608
+ self.colorbar_switch.set_default_action(f"{self.config.color_bar}_colorbar")
609
+ self.colorbar_switch.set_state_all(True)
610
+ else:
611
+ self.colorbar_switch.set_state_all(False)
612
+ self.colorbar_switch.block_all_signals(False)
613
+
614
+ ################################################################################
615
+ # Post Processing
616
+ ################################################################################
617
+
618
+ @SafeProperty(bool)
619
+ def fft(self) -> bool:
620
+ """
621
+ Whether FFT postprocessing is enabled.
622
+ """
623
+ return self._main_image.fft
624
+
625
+ @fft.setter
626
+ def fft(self, enable: bool):
627
+ """
628
+ Set FFT postprocessing.
629
+
630
+ Args:
631
+ enable(bool): Whether to enable FFT postprocessing.
632
+ """
633
+ self._main_image.fft = enable
634
+
635
+ @SafeProperty(bool)
636
+ def log(self) -> bool:
637
+ """
638
+ Whether logarithmic scaling is applied.
639
+ """
640
+ return self._main_image.log
641
+
642
+ @log.setter
643
+ def log(self, enable: bool):
644
+ """
645
+ Set logarithmic scaling.
646
+
647
+ Args:
648
+ enable(bool): Whether to enable logarithmic scaling.
649
+ """
650
+ self._main_image.log = enable
651
+
652
+ @SafeProperty(int)
653
+ def num_rotation_90(self) -> int:
654
+ """
655
+ The number of 90° rotations to apply counterclockwise.
656
+ """
657
+ return self._main_image.num_rotation_90
658
+
659
+ @num_rotation_90.setter
660
+ def num_rotation_90(self, value: int):
661
+ """
662
+ Set the number of 90° rotations to apply counterclockwise.
663
+
664
+ Args:
665
+ value(int): The number of 90° rotations to apply.
666
+ """
667
+ self._main_image.num_rotation_90 = value
668
+
669
+ @SafeProperty(bool)
670
+ def transpose(self) -> bool:
671
+ """
672
+ Whether the image is transposed.
673
+ """
674
+ return self._main_image.transpose
675
+
676
+ @transpose.setter
677
+ def transpose(self, enable: bool):
678
+ """
679
+ Set the image to be transposed.
680
+
681
+ Args:
682
+ enable(bool): Whether to enable transposing the image.
683
+ """
684
+ self._main_image.transpose = enable
685
+
686
+ ################################################################################
687
+ # High Level methods for API
688
+ ################################################################################
689
+ @SafeSlot(popup_error=True)
690
+ def image(
691
+ self,
692
+ monitor: str | None = None,
693
+ monitor_type: Literal["auto", "1d", "2d"] = "auto",
694
+ color_map: str | None = None,
695
+ color_bar: Literal["simple", "full"] | None = None,
696
+ vrange: tuple[int, int] | None = None,
697
+ ) -> ImageItem:
698
+ """
699
+ Set the image source and update the image.
700
+
701
+ Args:
702
+ monitor(str): The name of the monitor to use for the image.
703
+ monitor_type(str): The type of monitor to use. Options are "1d", "2d", or "auto".
704
+ color_map(str): The color map to use for the image.
705
+ color_bar(str): The type of color bar to use. Options are "simple" or "full".
706
+ vrange(tuple): The range of values to use for the color map.
707
+
708
+ Returns:
709
+ ImageItem: The image object.
710
+ """
711
+
712
+ if self._main_image.config.monitor is not None:
713
+ self.disconnect_monitor(self._main_image.config.monitor)
714
+ self.entry_validator.validate_monitor(monitor)
715
+ self._main_image.config.monitor = monitor
716
+
717
+ if monitor_type == "1d":
718
+ self._main_image.config.source = "device_monitor_1d"
719
+ self._main_image.config.monitor_type = "1d"
720
+ elif monitor_type == "2d":
721
+ self._main_image.config.source = "device_monitor_2d"
722
+ self._main_image.config.monitor_type = "2d"
723
+ elif monitor_type == "auto":
724
+ self._main_image.config.source = "auto"
725
+ logger.warning(
726
+ f"Updates for '{monitor}' will be fetch from both 1D and 2D monitor endpoints."
727
+ )
728
+ self._main_image.config.monitor_type = "auto"
729
+
730
+ self.set_image_update(monitor=monitor, type=monitor_type)
731
+ if color_map is not None:
732
+ self._main_image.color_map = color_map
733
+ if color_bar is not None:
734
+ self.enable_colorbar(True, color_bar)
735
+ if vrange is not None:
736
+ self.vrange = vrange
737
+
738
+ self._sync_device_selection()
739
+
740
+ return self._main_image
741
+
742
+ def _sync_device_selection(self):
743
+ """
744
+ Synchronize the device selection with the current monitor.
745
+ """
746
+ if self._main_image.config.monitor is not None:
747
+ for combo in (
748
+ self.selection_bundle.device_combo_box,
749
+ self.selection_bundle.dim_combo_box,
750
+ ):
751
+ combo.blockSignals(True)
752
+ self.selection_bundle.device_combo_box.set_device(self._main_image.config.monitor)
753
+ self.selection_bundle.dim_combo_box.setCurrentText(self._main_image.config.monitor_type)
754
+ for combo in (
755
+ self.selection_bundle.device_combo_box,
756
+ self.selection_bundle.dim_combo_box,
757
+ ):
758
+ combo.blockSignals(False)
759
+ else:
760
+ for combo in (
761
+ self.selection_bundle.device_combo_box,
762
+ self.selection_bundle.dim_combo_box,
763
+ ):
764
+ combo.blockSignals(True)
765
+ self.selection_bundle.device_combo_box.setCurrentText("")
766
+ self.selection_bundle.dim_combo_box.setCurrentText("auto")
767
+ for combo in (
768
+ self.selection_bundle.device_combo_box,
769
+ self.selection_bundle.dim_combo_box,
770
+ ):
771
+ combo.blockSignals(False)
772
+
773
+ ################################################################################
774
+ # Image Update Methods
775
+ ################################################################################
776
+
777
+ ########################################
778
+ # Connections
779
+
780
+ def set_image_update(self, monitor: str, type: Literal["1d", "2d", "auto"]):
781
+ """
782
+ Set the image update method for the given monitor.
783
+
784
+ Args:
785
+ monitor(str): The name of the monitor to use for the image.
786
+ type(str): The type of monitor to use. Options are "1d", "2d", or "auto".
787
+ """
788
+
789
+ # TODO consider moving connecting and disconnecting logic to Image itself if multiple images
790
+ if type == "1d":
791
+ self.bec_dispatcher.connect_slot(
792
+ self.on_image_update_1d, MessageEndpoints.device_monitor_1d(monitor)
793
+ )
794
+ elif type == "2d":
795
+ self.bec_dispatcher.connect_slot(
796
+ self.on_image_update_2d, MessageEndpoints.device_monitor_2d(monitor)
797
+ )
798
+ elif type == "auto":
799
+ self.bec_dispatcher.connect_slot(
800
+ self.on_image_update_1d, MessageEndpoints.device_monitor_1d(monitor)
801
+ )
802
+ self.bec_dispatcher.connect_slot(
803
+ self.on_image_update_2d, MessageEndpoints.device_monitor_2d(monitor)
804
+ )
805
+ print(f"Connected to {monitor} with type {type}")
806
+ self._main_image.config.monitor = monitor
807
+
808
+ def disconnect_monitor(self, monitor: str):
809
+ """
810
+ Disconnect the monitor from the image update signals, both 1D and 2D.
811
+
812
+ Args:
813
+ monitor(str): The name of the monitor to disconnect.
814
+ """
815
+ self.bec_dispatcher.disconnect_slot(
816
+ self.on_image_update_1d, MessageEndpoints.device_monitor_1d(monitor)
817
+ )
818
+ self.bec_dispatcher.disconnect_slot(
819
+ self.on_image_update_2d, MessageEndpoints.device_monitor_2d(monitor)
820
+ )
821
+ self._main_image.config.monitor = None
822
+ self._sync_device_selection()
823
+
824
+ ########################################
825
+ # 1D updates
826
+
827
+ @SafeSlot(dict, dict)
828
+ def on_image_update_1d(self, msg: dict, metadata: dict):
829
+ """
830
+ Update the image with 1D data.
831
+
832
+ Args:
833
+ msg(dict): The message containing the data.
834
+ metadata(dict): The metadata associated with the message.
835
+ """
836
+ data = msg["data"]
837
+ current_scan_id = metadata.get("scan_id", None)
838
+
839
+ if current_scan_id is None:
840
+ return
841
+ if current_scan_id != self.scan_id:
842
+ self.scan_id = current_scan_id
843
+ self._main_image.clear()
844
+ self._main_image.buffer = []
845
+ self._main_image.max_len = 0
846
+ image_buffer = self.adjust_image_buffer(self._main_image, data)
847
+ if self._color_bar is not None:
848
+ self._color_bar.blockSignals(True)
849
+ self._main_image.set_data(image_buffer)
850
+ if self._color_bar is not None:
851
+ self._color_bar.blockSignals(False)
852
+
853
+ def adjust_image_buffer(self, image: ImageItem, new_data: np.ndarray) -> np.ndarray:
854
+ """
855
+ Adjusts the image buffer to accommodate the new data, ensuring that all rows have the same length.
856
+
857
+ Args:
858
+ image: The image object (used to store a buffer list and max_len).
859
+ new_data (np.ndarray): The new incoming 1D waveform data.
860
+
861
+ Returns:
862
+ np.ndarray: The updated image buffer with adjusted shapes.
863
+ """
864
+ new_len = new_data.shape[0]
865
+ if not hasattr(image, "buffer"):
866
+ image.buffer = []
867
+ image.max_len = 0
868
+
869
+ if new_len > image.max_len:
870
+ image.max_len = new_len
871
+ for i in range(len(image.buffer)):
872
+ wf = image.buffer[i]
873
+ pad_width = image.max_len - wf.shape[0]
874
+ if pad_width > 0:
875
+ image.buffer[i] = np.pad(wf, (0, pad_width), mode="constant", constant_values=0)
876
+ image.buffer.append(new_data)
877
+ else:
878
+ pad_width = image.max_len - new_len
879
+ if pad_width > 0:
880
+ new_data = np.pad(new_data, (0, pad_width), mode="constant", constant_values=0)
881
+ image.buffer.append(new_data)
882
+
883
+ image_buffer = np.array(image.buffer)
884
+ return image_buffer
885
+
886
+ ########################################
887
+ # 2D updates
888
+
889
+ def on_image_update_2d(self, msg: dict, metadata: dict):
890
+ """
891
+ Update the image with 2D data.
892
+
893
+ Args:
894
+ msg(dict): The message containing the data.
895
+ metadata(dict): The metadata associated with the message.
896
+ """
897
+ data = msg["data"]
898
+ if self._color_bar is not None:
899
+ self._color_bar.blockSignals(True)
900
+ self._main_image.set_data(data)
901
+ if self._color_bar is not None:
902
+ self._color_bar.blockSignals(False)
903
+
904
+ ################################################################################
905
+ # Clean up
906
+ ################################################################################
907
+
908
+ @staticmethod
909
+ def cleanup_histogram_lut_item(histogram_lut_item: pg.HistogramLUTItem):
910
+ """
911
+ Clean up HistogramLUTItem safely, including open ViewBox menus and child widgets.
912
+
913
+ Args:
914
+ histogram_lut_item(pg.HistogramLUTItem): The HistogramLUTItem to clean up.
915
+ """
916
+ histogram_lut_item.vb.menu.close()
917
+ histogram_lut_item.vb.menu.deleteLater()
918
+
919
+ histogram_lut_item.gradient.menu.close()
920
+ histogram_lut_item.gradient.menu.deleteLater()
921
+ histogram_lut_item.gradient.colorDialog.close()
922
+ histogram_lut_item.gradient.colorDialog.deleteLater()
923
+
924
+ def cleanup(self):
925
+ """
926
+ Disconnect the image update signals and clean up the image.
927
+ """
928
+ # Main Image cleanup
929
+ if self._main_image.config.monitor is not None:
930
+ self.disconnect_monitor(self._main_image.config.monitor)
931
+ self._main_image.config.monitor = None
932
+ self.plot_item.removeItem(self._main_image)
933
+ self._main_image = None
934
+
935
+ # Colorbar Cleanup
936
+ if self._color_bar:
937
+ if self.config.color_bar == "full":
938
+ self.cleanup_histogram_lut_item(self._color_bar)
939
+ if self.config.color_bar == "simple":
940
+ self.plot_widget.removeItem(self._color_bar)
941
+ self._color_bar.deleteLater()
942
+ self._color_bar = None
943
+
944
+ # Toolbar cleanup
945
+ self.toolbar.widgets["monitor"].widget.close()
946
+ self.toolbar.widgets["monitor"].widget.deleteLater()
947
+
948
+ super().cleanup()
949
+
950
+
951
+ if __name__ == "__main__": # pragma: no cover
952
+ import sys
953
+
954
+ from qtpy.QtWidgets import QApplication
955
+
956
+ app = QApplication(sys.argv)
957
+ widget = Image(popups=True)
958
+ widget.show()
959
+ widget.resize(1000, 800)
960
+ sys.exit(app.exec_())