bec-widgets 1.25.1__py3-none-any.whl → 2.0.1__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 +639 -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 +188 -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.1.dist-info}/METADATA +3 -3
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.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.1.dist-info}/WHEEL +0 -0
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.25.1.dist-info → bec_widgets-2.0.1.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_())
|