bec-widgets 2.10.3__py3-none-any.whl → 2.12.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.
- CHANGELOG.md +84 -0
- PKG-INFO +1 -1
- bec_widgets/cli/client.py +5 -5
- bec_widgets/tests/utils.py +2 -2
- bec_widgets/utils/clickable_label.py +13 -0
- bec_widgets/utils/colors.py +7 -4
- bec_widgets/utils/expandable_frame.py +58 -7
- bec_widgets/utils/forms_from_types/forms.py +107 -28
- bec_widgets/utils/forms_from_types/items.py +88 -12
- bec_widgets/utils/forms_from_types/styles.py +21 -0
- bec_widgets/utils/generate_designer_plugin.py +10 -21
- bec_widgets/widgets/control/scan_control/scan_control.py +1 -1
- bec_widgets/widgets/editors/dict_backed_table.py +31 -11
- bec_widgets/widgets/editors/scan_metadata/_util.py +7 -4
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +10 -4
- bec_widgets/widgets/plots/image/image.py +128 -856
- bec_widgets/widgets/plots/image/image_base.py +1062 -0
- bec_widgets/widgets/plots/image/image_item.py +7 -6
- bec_widgets/widgets/services/device_browser/device_browser.py +61 -29
- bec_widgets/widgets/services/device_browser/device_item/device_item.py +97 -19
- bec_widgets/widgets/services/device_browser/util.py +11 -0
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/METADATA +1 -1
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/RECORD +27 -23
- pyproject.toml +1 -1
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/WHEEL +0 -0
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1062 @@
|
|
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 pydantic import BaseModel, ConfigDict, Field, ValidationError
|
9
|
+
from qtpy.QtCore import QPointF, Signal, SignalInstance
|
10
|
+
from qtpy.QtWidgets import QDialog, QVBoxLayout
|
11
|
+
|
12
|
+
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
13
|
+
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
14
|
+
from bec_widgets.utils.side_panel import SidePanel
|
15
|
+
from bec_widgets.utils.toolbar import MaterialIconAction, SwitchableToolBarAction
|
16
|
+
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
17
|
+
from bec_widgets.widgets.plots.image.image_roi_plot import ImageROIPlot
|
18
|
+
from bec_widgets.widgets.plots.image.setting_widgets.image_roi_tree import ROIPropertyTree
|
19
|
+
from bec_widgets.widgets.plots.image.toolbar_bundles.image_selection import (
|
20
|
+
MonitorSelectionToolbarBundle,
|
21
|
+
)
|
22
|
+
from bec_widgets.widgets.plots.image.toolbar_bundles.processing import ImageProcessingToolbarBundle
|
23
|
+
from bec_widgets.widgets.plots.plot_base import PlotBase
|
24
|
+
from bec_widgets.widgets.plots.roi.image_roi import (
|
25
|
+
BaseROI,
|
26
|
+
CircularROI,
|
27
|
+
RectangularROI,
|
28
|
+
ROIController,
|
29
|
+
)
|
30
|
+
|
31
|
+
logger = bec_logger.logger
|
32
|
+
|
33
|
+
|
34
|
+
class ImageLayerSync(BaseModel):
|
35
|
+
"""
|
36
|
+
Model for the image layer synchronization.
|
37
|
+
"""
|
38
|
+
|
39
|
+
autorange: bool = Field(
|
40
|
+
True, description="Whether to synchronize the autorange of the image layer."
|
41
|
+
)
|
42
|
+
autorange_mode: bool = Field(
|
43
|
+
True, description="Whether to synchronize the autorange mode of the image layer."
|
44
|
+
)
|
45
|
+
color_map: bool = Field(
|
46
|
+
True, description="Whether to synchronize the color map of the image layer."
|
47
|
+
)
|
48
|
+
v_range: bool = Field(
|
49
|
+
True, description="Whether to synchronize the v_range of the image layer."
|
50
|
+
)
|
51
|
+
fft: bool = Field(True, description="Whether to synchronize the FFT of the image layer.")
|
52
|
+
log: bool = Field(True, description="Whether to synchronize the log of the image layer.")
|
53
|
+
rotation: bool = Field(
|
54
|
+
True, description="Whether to synchronize the rotation of the image layer."
|
55
|
+
)
|
56
|
+
transpose: bool = Field(
|
57
|
+
True, description="Whether to synchronize the transpose of the image layer."
|
58
|
+
)
|
59
|
+
|
60
|
+
|
61
|
+
class ImageLayer(BaseModel):
|
62
|
+
"""
|
63
|
+
Model for the image layer.
|
64
|
+
"""
|
65
|
+
|
66
|
+
name: str = Field(description="The name of the image layer.")
|
67
|
+
image: ImageItem = Field(description="The image item to be displayed.")
|
68
|
+
sync: ImageLayerSync = Field(
|
69
|
+
default_factory=ImageLayerSync,
|
70
|
+
description="The synchronization settings for the image layer.",
|
71
|
+
)
|
72
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
73
|
+
|
74
|
+
|
75
|
+
class ImageLayerManager:
|
76
|
+
"""
|
77
|
+
Manager for the image layers.
|
78
|
+
"""
|
79
|
+
|
80
|
+
Z_RANGE_USER = (-100, 100)
|
81
|
+
|
82
|
+
def __init__(
|
83
|
+
self,
|
84
|
+
parent: ImageBase,
|
85
|
+
plot_item: pg.PlotItem,
|
86
|
+
on_add: SignalInstance | None = None,
|
87
|
+
on_remove: SignalInstance | None = None,
|
88
|
+
):
|
89
|
+
self.parent = parent
|
90
|
+
self.plot_item = plot_item
|
91
|
+
self.on_add = on_add
|
92
|
+
self.on_remove = on_remove
|
93
|
+
self.layers: dict[str, ImageLayer] = {}
|
94
|
+
|
95
|
+
def add(
|
96
|
+
self,
|
97
|
+
name: str | None = None,
|
98
|
+
z_position: int | Literal["top", "bottom"] | None = None,
|
99
|
+
sync: ImageLayerSync | None = None,
|
100
|
+
**kwargs,
|
101
|
+
) -> ImageLayer:
|
102
|
+
"""
|
103
|
+
Add an image layer to the widget.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
name (str | None): The name of the image layer. If None, a default name is generated.
|
107
|
+
image (ImageItem): The image layer to add.
|
108
|
+
z_position (int | None): The z position of the image layer. If None, the layer is added to the top.
|
109
|
+
sync (ImageLayerSync | None): The synchronization settings for the image layer.
|
110
|
+
**kwargs: ImageLayerSync settings. Only used if sync is None.
|
111
|
+
"""
|
112
|
+
if name is None:
|
113
|
+
name = WidgetContainerUtils.generate_unique_name(
|
114
|
+
"image_layer", list(self.layers.keys())
|
115
|
+
)
|
116
|
+
if name in self.layers:
|
117
|
+
raise ValueError(f"Layer with name '{name}' already exists.")
|
118
|
+
if sync is None:
|
119
|
+
sync = ImageLayerSync(**kwargs)
|
120
|
+
if z_position is None or z_position == "top":
|
121
|
+
z_position = self._get_top_z_position()
|
122
|
+
elif z_position == "bottom":
|
123
|
+
z_position = self._get_bottom_z_position()
|
124
|
+
image = ImageItem(parent_image=self.parent, object_name=name)
|
125
|
+
image.setZValue(z_position)
|
126
|
+
image.removed.connect(self._remove_destroyed_layer)
|
127
|
+
|
128
|
+
# FIXME: For now, we hard-code the default color map here. In the future, this should be configurable.
|
129
|
+
image.color_map = "plasma"
|
130
|
+
|
131
|
+
self.layers[name] = ImageLayer(name=name, image=image, sync=sync)
|
132
|
+
self.plot_item.addItem(image)
|
133
|
+
|
134
|
+
if self.on_add is not None:
|
135
|
+
self.on_add.emit(name)
|
136
|
+
|
137
|
+
return self.layers[name]
|
138
|
+
|
139
|
+
@SafeSlot(str)
|
140
|
+
def _remove_destroyed_layer(self, layer: str):
|
141
|
+
"""
|
142
|
+
Remove a layer that has been destroyed.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
layer (str): The name of the layer to remove.
|
146
|
+
"""
|
147
|
+
self.remove(layer)
|
148
|
+
if self.on_remove is not None:
|
149
|
+
self.on_remove.emit(layer)
|
150
|
+
|
151
|
+
def remove(self, layer: ImageLayer | str):
|
152
|
+
"""
|
153
|
+
Remove an image layer from the widget.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
layer (ImageLayer | str): The image layer to remove. Can be the layer object or the name of the layer.
|
157
|
+
"""
|
158
|
+
if isinstance(layer, str):
|
159
|
+
name = layer
|
160
|
+
else:
|
161
|
+
name = layer.name
|
162
|
+
|
163
|
+
removed_layer = self.layers.pop(name, None)
|
164
|
+
|
165
|
+
if not removed_layer:
|
166
|
+
return
|
167
|
+
self.plot_item.removeItem(removed_layer.image)
|
168
|
+
removed_layer.image.remove(emit=False)
|
169
|
+
removed_layer.image.deleteLater()
|
170
|
+
removed_layer.image = None
|
171
|
+
|
172
|
+
def clear(self):
|
173
|
+
"""
|
174
|
+
Clear all image layers from the manager.
|
175
|
+
"""
|
176
|
+
for layer in list(self.layers.keys()):
|
177
|
+
# Remove each layer from the plot item and delete it
|
178
|
+
self.remove(layer)
|
179
|
+
self.layers.clear()
|
180
|
+
|
181
|
+
def _get_top_z_position(self) -> int:
|
182
|
+
"""
|
183
|
+
Get the top z position of the image layers, capping it to the maximum z value.
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
int: The top z position of the image layers.
|
187
|
+
"""
|
188
|
+
if not self.layers:
|
189
|
+
return 0
|
190
|
+
z = max(layer.image.zValue() for layer in self.layers.values()) + 1
|
191
|
+
return min(z, self.Z_RANGE_USER[1])
|
192
|
+
|
193
|
+
def _get_bottom_z_position(self) -> int:
|
194
|
+
"""
|
195
|
+
Get the bottom z position of the image layers, capping it to the minimum z value.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
int: The bottom z position of the image layers.
|
199
|
+
"""
|
200
|
+
if not self.layers:
|
201
|
+
return 0
|
202
|
+
z = min(layer.image.zValue() for layer in self.layers.values()) - 1
|
203
|
+
return max(z, self.Z_RANGE_USER[0])
|
204
|
+
|
205
|
+
def __iter__(self):
|
206
|
+
"""
|
207
|
+
Iterate over the image layers.
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
Iterator[ImageLayer]: An iterator over the image layers.
|
211
|
+
"""
|
212
|
+
return iter(self.layers.values())
|
213
|
+
|
214
|
+
def __getitem__(self, name: str) -> ImageLayer:
|
215
|
+
"""
|
216
|
+
Get an image layer by name.
|
217
|
+
|
218
|
+
Args:
|
219
|
+
name (str): The name of the image layer.
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
ImageLayer: The image layer with the given name.
|
223
|
+
"""
|
224
|
+
if not isinstance(name, str):
|
225
|
+
raise TypeError("name must be a string")
|
226
|
+
if name == "main" and name not in self.layers:
|
227
|
+
# If 'main' is requested, create a default layer if it doesn't exist
|
228
|
+
return self.add(name=name, z_position="top")
|
229
|
+
return self.layers[name]
|
230
|
+
|
231
|
+
def __len__(self) -> int:
|
232
|
+
"""
|
233
|
+
Get the number of image layers.
|
234
|
+
|
235
|
+
Returns:
|
236
|
+
int: The number of image layers.
|
237
|
+
"""
|
238
|
+
return len(self.layers)
|
239
|
+
|
240
|
+
|
241
|
+
class ImageBase(PlotBase):
|
242
|
+
"""
|
243
|
+
Base class for the Image widget.
|
244
|
+
"""
|
245
|
+
|
246
|
+
sync_colorbar_with_autorange = Signal()
|
247
|
+
image_updated = Signal()
|
248
|
+
layer_added = Signal(str)
|
249
|
+
layer_removed = Signal(str)
|
250
|
+
|
251
|
+
def __init__(self, *args, **kwargs):
|
252
|
+
"""
|
253
|
+
Initialize the ImageBase widget.
|
254
|
+
"""
|
255
|
+
self.x_roi = None
|
256
|
+
self.y_roi = None
|
257
|
+
super().__init__(*args, **kwargs)
|
258
|
+
self.roi_controller = ROIController(colormap="viridis")
|
259
|
+
|
260
|
+
# Headless controller keeps the canonical list.
|
261
|
+
self.roi_manager_dialog = None
|
262
|
+
self.layer_manager: ImageLayerManager = ImageLayerManager(
|
263
|
+
self, plot_item=self.plot_item, on_add=self.layer_added, on_remove=self.layer_removed
|
264
|
+
)
|
265
|
+
self.layer_manager.add("main")
|
266
|
+
|
267
|
+
self.autorange = True
|
268
|
+
self.autorange_mode = "mean"
|
269
|
+
|
270
|
+
# Initialize ROI plots and side panels
|
271
|
+
self._add_roi_plots()
|
272
|
+
|
273
|
+
# Refresh theme for ROI plots
|
274
|
+
self._update_theme()
|
275
|
+
|
276
|
+
################################################################################
|
277
|
+
# Widget Specific GUI interactions
|
278
|
+
################################################################################
|
279
|
+
|
280
|
+
def apply_theme(self, theme: str):
|
281
|
+
super().apply_theme(theme)
|
282
|
+
if self.x_roi is not None and self.y_roi is not None:
|
283
|
+
self.x_roi.apply_theme(theme)
|
284
|
+
self.y_roi.apply_theme(theme)
|
285
|
+
|
286
|
+
def add_layer(self, name: str | None = None, **kwargs) -> ImageLayer:
|
287
|
+
"""
|
288
|
+
Add a new image layer to the widget.
|
289
|
+
|
290
|
+
Args:
|
291
|
+
name (str | None): The name of the image layer. If None, a default name is generated.
|
292
|
+
**kwargs: Additional arguments for the image layer.
|
293
|
+
|
294
|
+
Returns:
|
295
|
+
ImageLayer: The added image layer.
|
296
|
+
"""
|
297
|
+
layer = self.layer_manager.add(name=name, **kwargs)
|
298
|
+
self.image_updated.emit()
|
299
|
+
return layer
|
300
|
+
|
301
|
+
def remove_layer(self, layer: ImageLayer | str):
|
302
|
+
"""
|
303
|
+
Remove an image layer from the widget.
|
304
|
+
|
305
|
+
Args:
|
306
|
+
layer (ImageLayer | str): The image layer to remove. Can be the layer object or the name of the layer.
|
307
|
+
"""
|
308
|
+
self.layer_manager.remove(layer)
|
309
|
+
self.image_updated.emit()
|
310
|
+
|
311
|
+
def layers(self) -> list[ImageLayer]:
|
312
|
+
"""
|
313
|
+
Get the list of image layers.
|
314
|
+
|
315
|
+
Returns:
|
316
|
+
list[ImageLayer]: The list of image layers.
|
317
|
+
"""
|
318
|
+
return list(self.layer_manager.layers.values())
|
319
|
+
|
320
|
+
def _init_toolbar(self):
|
321
|
+
|
322
|
+
try:
|
323
|
+
# add to the first position
|
324
|
+
self.selection_bundle = MonitorSelectionToolbarBundle(
|
325
|
+
bundle_id="selection", target_widget=self
|
326
|
+
)
|
327
|
+
self.toolbar.add_bundle(self.selection_bundle, self)
|
328
|
+
|
329
|
+
super()._init_toolbar()
|
330
|
+
|
331
|
+
# Image specific changes to PlotBase toolbar
|
332
|
+
self.toolbar.widgets["reset_legend"].action.setVisible(False)
|
333
|
+
|
334
|
+
# ROI Bundle replacement with switchable crosshair
|
335
|
+
self.toolbar.remove_bundle("roi")
|
336
|
+
crosshair = MaterialIconAction(
|
337
|
+
icon_name="point_scan", tooltip="Show Crosshair", checkable=True, parent=self
|
338
|
+
)
|
339
|
+
crosshair_roi = MaterialIconAction(
|
340
|
+
icon_name="my_location",
|
341
|
+
tooltip="Show Crosshair with ROI plots",
|
342
|
+
checkable=True,
|
343
|
+
parent=self,
|
344
|
+
)
|
345
|
+
crosshair_roi.action.toggled.connect(self.toggle_roi_panels)
|
346
|
+
crosshair.action.toggled.connect(self.toggle_crosshair)
|
347
|
+
switch_crosshair = SwitchableToolBarAction(
|
348
|
+
actions={"crosshair_simple": crosshair, "crosshair_roi": crosshair_roi},
|
349
|
+
initial_action="crosshair_simple",
|
350
|
+
tooltip="Crosshair",
|
351
|
+
checkable=True,
|
352
|
+
parent=self,
|
353
|
+
)
|
354
|
+
self.toolbar.add_action(
|
355
|
+
action_id="switch_crosshair", action=switch_crosshair, target_widget=self
|
356
|
+
)
|
357
|
+
|
358
|
+
# Lock aspect ratio button
|
359
|
+
self.lock_aspect_ratio_action = MaterialIconAction(
|
360
|
+
icon_name="aspect_ratio", tooltip="Lock Aspect Ratio", checkable=True, parent=self
|
361
|
+
)
|
362
|
+
self.toolbar.add_action_to_bundle(
|
363
|
+
bundle_id="mouse_interaction",
|
364
|
+
action_id="lock_aspect_ratio",
|
365
|
+
action=self.lock_aspect_ratio_action,
|
366
|
+
target_widget=self,
|
367
|
+
)
|
368
|
+
self.lock_aspect_ratio_action.action.toggled.connect(
|
369
|
+
lambda checked: self.setProperty("lock_aspect_ratio", checked)
|
370
|
+
)
|
371
|
+
self.lock_aspect_ratio_action.action.setChecked(True)
|
372
|
+
|
373
|
+
self._init_autorange_action()
|
374
|
+
self._init_colorbar_action()
|
375
|
+
|
376
|
+
# Processing Bundle
|
377
|
+
self.processing_bundle = ImageProcessingToolbarBundle(
|
378
|
+
bundle_id="processing", target_widget=self
|
379
|
+
)
|
380
|
+
self.toolbar.add_bundle(self.processing_bundle, target_widget=self)
|
381
|
+
except Exception as e:
|
382
|
+
logger.error(f"Error initializing toolbar: {e}")
|
383
|
+
|
384
|
+
def _init_autorange_action(self):
|
385
|
+
|
386
|
+
self.autorange_mean_action = MaterialIconAction(
|
387
|
+
icon_name="hdr_auto", tooltip="Enable Auto Range (Mean)", checkable=True, parent=self
|
388
|
+
)
|
389
|
+
self.autorange_max_action = MaterialIconAction(
|
390
|
+
icon_name="hdr_auto",
|
391
|
+
tooltip="Enable Auto Range (Max)",
|
392
|
+
checkable=True,
|
393
|
+
filled=True,
|
394
|
+
parent=self,
|
395
|
+
)
|
396
|
+
|
397
|
+
self.autorange_switch = SwitchableToolBarAction(
|
398
|
+
actions={
|
399
|
+
"auto_range_mean": self.autorange_mean_action,
|
400
|
+
"auto_range_max": self.autorange_max_action,
|
401
|
+
},
|
402
|
+
initial_action="auto_range_mean",
|
403
|
+
tooltip="Enable Auto Range",
|
404
|
+
checkable=True,
|
405
|
+
parent=self,
|
406
|
+
)
|
407
|
+
|
408
|
+
self.toolbar.add_action(
|
409
|
+
action_id="autorange_image", action=self.autorange_switch, target_widget=self
|
410
|
+
)
|
411
|
+
|
412
|
+
self.autorange_mean_action.action.toggled.connect(
|
413
|
+
lambda checked: self.toggle_autorange(checked, mode="mean")
|
414
|
+
)
|
415
|
+
self.autorange_max_action.action.toggled.connect(
|
416
|
+
lambda checked: self.toggle_autorange(checked, mode="max")
|
417
|
+
)
|
418
|
+
|
419
|
+
def _init_colorbar_action(self):
|
420
|
+
self.full_colorbar_action = MaterialIconAction(
|
421
|
+
icon_name="edgesensor_low", tooltip="Enable Full Colorbar", checkable=True, parent=self
|
422
|
+
)
|
423
|
+
self.simple_colorbar_action = MaterialIconAction(
|
424
|
+
icon_name="smartphone", tooltip="Enable Simple Colorbar", checkable=True, parent=self
|
425
|
+
)
|
426
|
+
|
427
|
+
self.colorbar_switch = SwitchableToolBarAction(
|
428
|
+
actions={
|
429
|
+
"full_colorbar": self.full_colorbar_action,
|
430
|
+
"simple_colorbar": self.simple_colorbar_action,
|
431
|
+
},
|
432
|
+
initial_action="full_colorbar",
|
433
|
+
tooltip="Enable Full Colorbar",
|
434
|
+
checkable=True,
|
435
|
+
parent=self,
|
436
|
+
)
|
437
|
+
|
438
|
+
self.toolbar.add_action(
|
439
|
+
action_id="switch_colorbar", action=self.colorbar_switch, target_widget=self
|
440
|
+
)
|
441
|
+
|
442
|
+
self.simple_colorbar_action.action.toggled.connect(
|
443
|
+
lambda checked: self.enable_colorbar(checked, style="simple")
|
444
|
+
)
|
445
|
+
self.full_colorbar_action.action.toggled.connect(
|
446
|
+
lambda checked: self.enable_colorbar(checked, style="full")
|
447
|
+
)
|
448
|
+
|
449
|
+
########################################
|
450
|
+
# ROI Gui Manager
|
451
|
+
def add_side_menus(self):
|
452
|
+
super().add_side_menus()
|
453
|
+
|
454
|
+
roi_mgr = ROIPropertyTree(parent=self, image_widget=self)
|
455
|
+
self.side_panel.add_menu(
|
456
|
+
action_id="roi_mgr",
|
457
|
+
icon_name="view_list",
|
458
|
+
tooltip="ROI Manager",
|
459
|
+
widget=roi_mgr,
|
460
|
+
title="ROI Manager",
|
461
|
+
)
|
462
|
+
|
463
|
+
def add_popups(self):
|
464
|
+
super().add_popups() # keep Axis Settings
|
465
|
+
|
466
|
+
roi_action = MaterialIconAction(
|
467
|
+
icon_name="view_list", tooltip="ROI Manager", checkable=True, parent=self
|
468
|
+
)
|
469
|
+
# self.popup_bundle.add_action("roi_mgr", roi_action)
|
470
|
+
self.toolbar.add_action_to_bundle(
|
471
|
+
bundle_id="popup_bundle", action_id="roi_mgr", action=roi_action, target_widget=self
|
472
|
+
)
|
473
|
+
self.toolbar.widgets["roi_mgr"].action.triggered.connect(self.show_roi_manager_popup)
|
474
|
+
|
475
|
+
def show_roi_manager_popup(self):
|
476
|
+
roi_action = self.toolbar.widgets["roi_mgr"].action
|
477
|
+
if self.roi_manager_dialog is None or not self.roi_manager_dialog.isVisible():
|
478
|
+
self.roi_mgr = ROIPropertyTree(parent=self, image_widget=self)
|
479
|
+
self.roi_manager_dialog = QDialog(modal=False)
|
480
|
+
self.roi_manager_dialog.layout = QVBoxLayout(self.roi_manager_dialog)
|
481
|
+
self.roi_manager_dialog.layout.addWidget(self.roi_mgr)
|
482
|
+
self.roi_manager_dialog.finished.connect(self._roi_mgr_closed)
|
483
|
+
self.roi_manager_dialog.show()
|
484
|
+
roi_action.setChecked(True)
|
485
|
+
else:
|
486
|
+
self.roi_manager_dialog.raise_()
|
487
|
+
self.roi_manager_dialog.activateWindow()
|
488
|
+
roi_action.setChecked(True)
|
489
|
+
|
490
|
+
def _roi_mgr_closed(self):
|
491
|
+
self.roi_mgr.close()
|
492
|
+
self.roi_mgr.deleteLater()
|
493
|
+
self.roi_manager_dialog.close()
|
494
|
+
self.roi_manager_dialog.deleteLater()
|
495
|
+
self.roi_manager_dialog = None
|
496
|
+
self.toolbar.widgets["roi_mgr"].action.setChecked(False)
|
497
|
+
|
498
|
+
def enable_colorbar(
|
499
|
+
self,
|
500
|
+
enabled: bool,
|
501
|
+
style: Literal["full", "simple"] = "full",
|
502
|
+
vrange: tuple[int, int] | None = None,
|
503
|
+
):
|
504
|
+
"""
|
505
|
+
Enable the colorbar and switch types of colorbars.
|
506
|
+
|
507
|
+
Args:
|
508
|
+
enabled(bool): Whether to enable the colorbar.
|
509
|
+
style(Literal["full", "simple"]): The type of colorbar to enable.
|
510
|
+
vrange(tuple): The range of values to use for the colorbar.
|
511
|
+
"""
|
512
|
+
autorange_state = self.layer_manager["main"].image.autorange
|
513
|
+
if enabled:
|
514
|
+
if self._color_bar:
|
515
|
+
if self.config.color_bar == "full":
|
516
|
+
self.cleanup_histogram_lut_item(self._color_bar)
|
517
|
+
self.plot_widget.removeItem(self._color_bar)
|
518
|
+
self._color_bar = None
|
519
|
+
|
520
|
+
if style == "simple":
|
521
|
+
|
522
|
+
def disable_autorange():
|
523
|
+
print("Disabling autorange")
|
524
|
+
self.setProperty("autorange", False)
|
525
|
+
|
526
|
+
self._color_bar = pg.ColorBarItem(colorMap=self.config.color_map)
|
527
|
+
self._color_bar.setImageItem(self.layer_manager["main"].image)
|
528
|
+
self._color_bar.sigLevelsChangeFinished.connect(disable_autorange)
|
529
|
+
|
530
|
+
elif style == "full":
|
531
|
+
self._color_bar = pg.HistogramLUTItem()
|
532
|
+
self._color_bar.setImageItem(self.layer_manager["main"].image)
|
533
|
+
self._color_bar.gradient.loadPreset(self.config.color_map)
|
534
|
+
self._color_bar.sigLevelsChanged.connect(
|
535
|
+
lambda: self.setProperty("autorange", False)
|
536
|
+
)
|
537
|
+
|
538
|
+
self.plot_widget.addItem(self._color_bar, row=0, col=1)
|
539
|
+
self.config.color_bar = style
|
540
|
+
else:
|
541
|
+
if self._color_bar:
|
542
|
+
self.plot_widget.removeItem(self._color_bar)
|
543
|
+
self._color_bar = None
|
544
|
+
self.config.color_bar = None
|
545
|
+
|
546
|
+
self.autorange = autorange_state
|
547
|
+
self._sync_colorbar_actions()
|
548
|
+
|
549
|
+
if vrange: # should be at the end to disable the autorange if defined
|
550
|
+
self.v_range = vrange
|
551
|
+
|
552
|
+
################################################################################
|
553
|
+
# Static rois with roi manager
|
554
|
+
|
555
|
+
def add_roi(
|
556
|
+
self,
|
557
|
+
kind: Literal["rect", "circle"] = "rect",
|
558
|
+
name: str | None = None,
|
559
|
+
line_width: int | None = 5,
|
560
|
+
pos: tuple[float, float] | None = (10, 10),
|
561
|
+
size: tuple[float, float] | None = (50, 50),
|
562
|
+
**pg_kwargs,
|
563
|
+
) -> RectangularROI | CircularROI:
|
564
|
+
"""
|
565
|
+
Add a ROI to the image.
|
566
|
+
|
567
|
+
Args:
|
568
|
+
kind(str): The type of ROI to add. Options are "rect" or "circle".
|
569
|
+
name(str): The name of the ROI.
|
570
|
+
line_width(int): The line width of the ROI.
|
571
|
+
pos(tuple): The position of the ROI.
|
572
|
+
size(tuple): The size of the ROI.
|
573
|
+
**pg_kwargs: Additional arguments for the ROI.
|
574
|
+
|
575
|
+
Returns:
|
576
|
+
RectangularROI | CircularROI: The created ROI object.
|
577
|
+
"""
|
578
|
+
if name is None:
|
579
|
+
name = f"ROI_{len(self.roi_controller.rois) + 1}"
|
580
|
+
if kind == "rect":
|
581
|
+
roi = RectangularROI(
|
582
|
+
pos=pos,
|
583
|
+
size=size,
|
584
|
+
parent_image=self,
|
585
|
+
line_width=line_width,
|
586
|
+
label=name,
|
587
|
+
**pg_kwargs,
|
588
|
+
)
|
589
|
+
elif kind == "circle":
|
590
|
+
roi = CircularROI(
|
591
|
+
pos=pos,
|
592
|
+
size=size,
|
593
|
+
parent_image=self,
|
594
|
+
line_width=line_width,
|
595
|
+
label=name,
|
596
|
+
**pg_kwargs,
|
597
|
+
)
|
598
|
+
else:
|
599
|
+
raise ValueError("kind must be 'rect' or 'circle'")
|
600
|
+
|
601
|
+
# Add to plot and controller (controller assigns color)
|
602
|
+
self.plot_item.addItem(roi)
|
603
|
+
self.roi_controller.add_roi(roi)
|
604
|
+
roi.add_scale_handle()
|
605
|
+
return roi
|
606
|
+
|
607
|
+
def remove_roi(self, roi: int | str):
|
608
|
+
"""Remove an ROI by index or label via the ROIController."""
|
609
|
+
if isinstance(roi, int):
|
610
|
+
self.roi_controller.remove_roi_by_index(roi)
|
611
|
+
elif isinstance(roi, str):
|
612
|
+
self.roi_controller.remove_roi_by_name(roi)
|
613
|
+
else:
|
614
|
+
raise ValueError("roi must be an int index or str name")
|
615
|
+
|
616
|
+
def _add_roi_plots(self):
|
617
|
+
"""
|
618
|
+
Initialize the ROI plots and side panels.
|
619
|
+
"""
|
620
|
+
# Create ROI plot widgets
|
621
|
+
self.x_roi = ImageROIPlot(parent=self)
|
622
|
+
self.y_roi = ImageROIPlot(parent=self)
|
623
|
+
self.x_roi.apply_theme("dark")
|
624
|
+
self.y_roi.apply_theme("dark")
|
625
|
+
|
626
|
+
# Set titles for the plots
|
627
|
+
self.x_roi.plot_item.setTitle("X ROI")
|
628
|
+
self.y_roi.plot_item.setTitle("Y ROI")
|
629
|
+
|
630
|
+
# Create side panels
|
631
|
+
self.side_panel_x = SidePanel(
|
632
|
+
parent=self, orientation="bottom", panel_max_width=200, show_toolbar=False
|
633
|
+
)
|
634
|
+
self.side_panel_y = SidePanel(
|
635
|
+
parent=self, orientation="left", panel_max_width=200, show_toolbar=False
|
636
|
+
)
|
637
|
+
|
638
|
+
# Add ROI plots to side panels
|
639
|
+
self.x_panel_index = self.side_panel_x.add_menu(widget=self.x_roi)
|
640
|
+
self.y_panel_index = self.side_panel_y.add_menu(widget=self.y_roi)
|
641
|
+
|
642
|
+
# # Add side panels to the layout
|
643
|
+
self.layout_manager.add_widget_relative(
|
644
|
+
self.side_panel_x, self.round_plot_widget, position="bottom", shift_direction="down"
|
645
|
+
)
|
646
|
+
self.layout_manager.add_widget_relative(
|
647
|
+
self.side_panel_y, self.round_plot_widget, position="left", shift_direction="right"
|
648
|
+
)
|
649
|
+
|
650
|
+
def toggle_roi_panels(self, checked: bool):
|
651
|
+
"""
|
652
|
+
Show or hide the ROI panels based on the test action toggle state.
|
653
|
+
|
654
|
+
Args:
|
655
|
+
checked (bool): Whether the test action is checked.
|
656
|
+
"""
|
657
|
+
if checked:
|
658
|
+
# Show the ROI panels
|
659
|
+
self.hook_crosshair()
|
660
|
+
self.side_panel_x.show_panel(self.x_panel_index)
|
661
|
+
self.side_panel_y.show_panel(self.y_panel_index)
|
662
|
+
self.crosshair.coordinatesChanged2D.connect(self.update_image_slices)
|
663
|
+
self.image_updated.connect(self.update_image_slices)
|
664
|
+
else:
|
665
|
+
self.unhook_crosshair()
|
666
|
+
# Hide the ROI panels
|
667
|
+
self.side_panel_x.hide_panel()
|
668
|
+
self.side_panel_y.hide_panel()
|
669
|
+
self.image_updated.disconnect(self.update_image_slices)
|
670
|
+
|
671
|
+
@SafeSlot()
|
672
|
+
def update_image_slices(self, coordinates: tuple[int, int, int] = None):
|
673
|
+
"""
|
674
|
+
Update the image slices based on the crosshair position.
|
675
|
+
|
676
|
+
Args:
|
677
|
+
coordinates(tuple): The coordinates of the crosshair.
|
678
|
+
"""
|
679
|
+
if coordinates is None:
|
680
|
+
# Try to get coordinates from crosshair position (like in crosshair mouse_moved)
|
681
|
+
if (
|
682
|
+
hasattr(self, "crosshair")
|
683
|
+
and hasattr(self.crosshair, "v_line")
|
684
|
+
and hasattr(self.crosshair, "h_line")
|
685
|
+
):
|
686
|
+
x = int(round(self.crosshair.v_line.value()))
|
687
|
+
y = int(round(self.crosshair.h_line.value()))
|
688
|
+
else:
|
689
|
+
return
|
690
|
+
else:
|
691
|
+
x = coordinates[1]
|
692
|
+
y = coordinates[2]
|
693
|
+
image = self.layer_manager["main"].image.image
|
694
|
+
if image is None:
|
695
|
+
return
|
696
|
+
max_row, max_col = image.shape[0] - 1, image.shape[1] - 1
|
697
|
+
row, col = x, y
|
698
|
+
if not (0 <= row <= max_row and 0 <= col <= max_col):
|
699
|
+
return
|
700
|
+
# Horizontal slice
|
701
|
+
h_slice = image[:, col]
|
702
|
+
x_axis = np.arange(h_slice.shape[0])
|
703
|
+
self.x_roi.plot_item.clear()
|
704
|
+
self.x_roi.plot_item.plot(x_axis, h_slice, pen=pg.mkPen(self.x_roi.curve_color, width=3))
|
705
|
+
# Vertical slice
|
706
|
+
v_slice = image[row, :]
|
707
|
+
y_axis = np.arange(v_slice.shape[0])
|
708
|
+
self.y_roi.plot_item.clear()
|
709
|
+
self.y_roi.plot_item.plot(v_slice, y_axis, pen=pg.mkPen(self.y_roi.curve_color, width=3))
|
710
|
+
|
711
|
+
################################################################################
|
712
|
+
# Widget Specific Properties
|
713
|
+
################################################################################
|
714
|
+
################################################################################
|
715
|
+
# Rois
|
716
|
+
|
717
|
+
@property
|
718
|
+
def rois(self) -> list[BaseROI]:
|
719
|
+
"""
|
720
|
+
Get the list of ROIs.
|
721
|
+
"""
|
722
|
+
return self.roi_controller.rois
|
723
|
+
|
724
|
+
################################################################################
|
725
|
+
# Colorbar toggle
|
726
|
+
|
727
|
+
@SafeProperty(bool)
|
728
|
+
def enable_simple_colorbar(self) -> bool:
|
729
|
+
"""
|
730
|
+
Enable the simple colorbar.
|
731
|
+
"""
|
732
|
+
enabled = False
|
733
|
+
if self.config.color_bar == "simple":
|
734
|
+
enabled = True
|
735
|
+
return enabled
|
736
|
+
|
737
|
+
@enable_simple_colorbar.setter
|
738
|
+
def enable_simple_colorbar(self, value: bool):
|
739
|
+
"""
|
740
|
+
Enable the simple colorbar.
|
741
|
+
|
742
|
+
Args:
|
743
|
+
value(bool): Whether to enable the simple colorbar.
|
744
|
+
"""
|
745
|
+
self.enable_colorbar(enabled=value, style="simple")
|
746
|
+
|
747
|
+
@SafeProperty(bool)
|
748
|
+
def enable_full_colorbar(self) -> bool:
|
749
|
+
"""
|
750
|
+
Enable the full colorbar.
|
751
|
+
"""
|
752
|
+
enabled = False
|
753
|
+
if self.config.color_bar == "full":
|
754
|
+
enabled = True
|
755
|
+
return enabled
|
756
|
+
|
757
|
+
@enable_full_colorbar.setter
|
758
|
+
def enable_full_colorbar(self, value: bool):
|
759
|
+
"""
|
760
|
+
Enable the full colorbar.
|
761
|
+
|
762
|
+
Args:
|
763
|
+
value(bool): Whether to enable the full colorbar.
|
764
|
+
"""
|
765
|
+
self.enable_colorbar(enabled=value, style="full")
|
766
|
+
|
767
|
+
################################################################################
|
768
|
+
# Appearance
|
769
|
+
|
770
|
+
@SafeProperty(str)
|
771
|
+
def color_map(self) -> str:
|
772
|
+
"""
|
773
|
+
Set the color map of the image.
|
774
|
+
"""
|
775
|
+
return self.config.color_map
|
776
|
+
|
777
|
+
@color_map.setter
|
778
|
+
def color_map(self, value: str):
|
779
|
+
"""
|
780
|
+
Set the color map of the image.
|
781
|
+
|
782
|
+
Args:
|
783
|
+
value(str): The color map to set.
|
784
|
+
"""
|
785
|
+
try:
|
786
|
+
self.config.color_map = value
|
787
|
+
for layer in self.layer_manager:
|
788
|
+
if not layer.sync.color_map:
|
789
|
+
continue
|
790
|
+
layer.image.color_map = value
|
791
|
+
|
792
|
+
if self._color_bar:
|
793
|
+
if self.config.color_bar == "simple":
|
794
|
+
self._color_bar.setColorMap(value)
|
795
|
+
elif self.config.color_bar == "full":
|
796
|
+
self._color_bar.gradient.loadPreset(value)
|
797
|
+
except ValidationError:
|
798
|
+
return
|
799
|
+
|
800
|
+
@SafeProperty("QPointF")
|
801
|
+
def v_range(self) -> QPointF:
|
802
|
+
"""
|
803
|
+
Set the v_range of the main image.
|
804
|
+
"""
|
805
|
+
vmin, vmax = self.layer_manager["main"].image.v_range
|
806
|
+
return QPointF(vmin, vmax)
|
807
|
+
|
808
|
+
@v_range.setter
|
809
|
+
def v_range(self, value: tuple | list | QPointF):
|
810
|
+
"""
|
811
|
+
Set the v_range of the main image.
|
812
|
+
|
813
|
+
Args:
|
814
|
+
value(tuple | list | QPointF): The range of values to set.
|
815
|
+
"""
|
816
|
+
if isinstance(value, (tuple, list)):
|
817
|
+
value = self._tuple_to_qpointf(value)
|
818
|
+
|
819
|
+
vmin, vmax = value.x(), value.y()
|
820
|
+
|
821
|
+
for layer in self.layer_manager:
|
822
|
+
if not layer.sync.v_range:
|
823
|
+
continue
|
824
|
+
layer.image.v_range = (vmin, vmax)
|
825
|
+
|
826
|
+
# propagate to colorbar if exists
|
827
|
+
if self._color_bar:
|
828
|
+
if self.config.color_bar == "simple":
|
829
|
+
self._color_bar.setLevels(low=vmin, high=vmax)
|
830
|
+
elif self.config.color_bar == "full":
|
831
|
+
self._color_bar.setLevels(min=vmin, max=vmax)
|
832
|
+
self._color_bar.setHistogramRange(vmin - 0.1 * vmin, vmax + 0.1 * vmax)
|
833
|
+
|
834
|
+
self.autorange_switch.set_state_all(False)
|
835
|
+
|
836
|
+
@property
|
837
|
+
def v_min(self) -> float:
|
838
|
+
"""
|
839
|
+
Get the minimum value of the v_range.
|
840
|
+
"""
|
841
|
+
return self.v_range.x()
|
842
|
+
|
843
|
+
@v_min.setter
|
844
|
+
def v_min(self, value: float):
|
845
|
+
"""
|
846
|
+
Set the minimum value of the v_range.
|
847
|
+
|
848
|
+
Args:
|
849
|
+
value(float): The minimum value to set.
|
850
|
+
"""
|
851
|
+
self.v_range = (value, self.v_range.y())
|
852
|
+
|
853
|
+
@property
|
854
|
+
def v_max(self) -> float:
|
855
|
+
"""
|
856
|
+
Get the maximum value of the v_range.
|
857
|
+
"""
|
858
|
+
return self.v_range.y()
|
859
|
+
|
860
|
+
@v_max.setter
|
861
|
+
def v_max(self, value: float):
|
862
|
+
"""
|
863
|
+
Set the maximum value of the v_range.
|
864
|
+
|
865
|
+
Args:
|
866
|
+
value(float): The maximum value to set.
|
867
|
+
"""
|
868
|
+
self.v_range = (self.v_range.x(), value)
|
869
|
+
|
870
|
+
@SafeProperty(bool)
|
871
|
+
def lock_aspect_ratio(self) -> bool:
|
872
|
+
"""
|
873
|
+
Whether the aspect ratio is locked.
|
874
|
+
"""
|
875
|
+
return self.config.lock_aspect_ratio
|
876
|
+
|
877
|
+
@lock_aspect_ratio.setter
|
878
|
+
def lock_aspect_ratio(self, value: bool):
|
879
|
+
"""
|
880
|
+
Set the aspect ratio lock.
|
881
|
+
|
882
|
+
Args:
|
883
|
+
value(bool): Whether to lock the aspect ratio.
|
884
|
+
"""
|
885
|
+
self.config.lock_aspect_ratio = bool(value)
|
886
|
+
self.plot_item.setAspectLocked(value)
|
887
|
+
|
888
|
+
################################################################################
|
889
|
+
# Autorange + Colorbar sync
|
890
|
+
|
891
|
+
@SafeProperty(bool)
|
892
|
+
def autorange(self) -> bool:
|
893
|
+
"""
|
894
|
+
Whether autorange is enabled.
|
895
|
+
"""
|
896
|
+
|
897
|
+
# FIXME: this should be made more general
|
898
|
+
return self.layer_manager["main"].image.autorange
|
899
|
+
|
900
|
+
@autorange.setter
|
901
|
+
def autorange(self, enabled: bool):
|
902
|
+
"""
|
903
|
+
Set autorange.
|
904
|
+
|
905
|
+
Args:
|
906
|
+
enabled(bool): Whether to enable autorange.
|
907
|
+
"""
|
908
|
+
for layer in self.layer_manager:
|
909
|
+
if not layer.sync.autorange:
|
910
|
+
continue
|
911
|
+
layer.image.autorange = enabled
|
912
|
+
if enabled and layer.image.raw_data is not None:
|
913
|
+
layer.image.apply_autorange()
|
914
|
+
self._sync_colorbar_levels()
|
915
|
+
self._sync_autorange_switch()
|
916
|
+
|
917
|
+
@SafeProperty(str)
|
918
|
+
def autorange_mode(self) -> str:
|
919
|
+
"""
|
920
|
+
Autorange mode.
|
921
|
+
|
922
|
+
Options:
|
923
|
+
- "max": Use the maximum value of the image for autoranging.
|
924
|
+
- "mean": Use the mean value of the image for autoranging.
|
925
|
+
|
926
|
+
"""
|
927
|
+
return self.layer_manager["main"].image.autorange_mode
|
928
|
+
|
929
|
+
@autorange_mode.setter
|
930
|
+
def autorange_mode(self, mode: str):
|
931
|
+
"""
|
932
|
+
Set the autorange mode.
|
933
|
+
|
934
|
+
Args:
|
935
|
+
mode(str): The autorange mode. Options are "max" or "mean".
|
936
|
+
"""
|
937
|
+
# for qt Designer
|
938
|
+
if mode not in ["max", "mean"]:
|
939
|
+
return
|
940
|
+
for layer in self.layer_manager:
|
941
|
+
if not layer.sync.autorange_mode:
|
942
|
+
continue
|
943
|
+
layer.image.autorange_mode = mode
|
944
|
+
|
945
|
+
self._sync_autorange_switch()
|
946
|
+
|
947
|
+
@SafeSlot(bool, str, bool)
|
948
|
+
def toggle_autorange(self, enabled: bool, mode: str):
|
949
|
+
"""
|
950
|
+
Toggle autorange.
|
951
|
+
|
952
|
+
Args:
|
953
|
+
enabled(bool): Whether to enable autorange.
|
954
|
+
mode(str): The autorange mode. Options are "max" or "mean".
|
955
|
+
"""
|
956
|
+
if not self.layer_manager:
|
957
|
+
return
|
958
|
+
|
959
|
+
for layer in self.layer_manager:
|
960
|
+
if layer.sync.autorange:
|
961
|
+
layer.image.autorange = enabled
|
962
|
+
if layer.sync.autorange_mode:
|
963
|
+
layer.image.autorange_mode = mode
|
964
|
+
|
965
|
+
if not enabled:
|
966
|
+
continue
|
967
|
+
# We only need to apply autorange if we enabled it
|
968
|
+
layer.image.apply_autorange()
|
969
|
+
|
970
|
+
if enabled:
|
971
|
+
self._sync_colorbar_levels()
|
972
|
+
|
973
|
+
def _sync_autorange_switch(self):
|
974
|
+
"""
|
975
|
+
Synchronize the autorange switch with the current autorange state and mode if changed from outside.
|
976
|
+
"""
|
977
|
+
self.autorange_switch.block_all_signals(True)
|
978
|
+
self.autorange_switch.set_default_action(
|
979
|
+
f"auto_range_{self.layer_manager['main'].image.autorange_mode}"
|
980
|
+
)
|
981
|
+
self.autorange_switch.set_state_all(self.layer_manager["main"].image.autorange)
|
982
|
+
self.autorange_switch.block_all_signals(False)
|
983
|
+
|
984
|
+
def _sync_colorbar_levels(self):
|
985
|
+
"""Immediately propagate current levels to the active colorbar."""
|
986
|
+
|
987
|
+
if not self._color_bar:
|
988
|
+
return
|
989
|
+
|
990
|
+
total_vrange = (0, 0)
|
991
|
+
for layer in self.layer_manager:
|
992
|
+
if not layer.sync.v_range:
|
993
|
+
continue
|
994
|
+
img = layer.image
|
995
|
+
total_vrange = (min(total_vrange[0], img.v_min), max(total_vrange[1], img.v_max))
|
996
|
+
|
997
|
+
self._color_bar.blockSignals(True)
|
998
|
+
self.v_range = total_vrange # type: ignore
|
999
|
+
self._color_bar.blockSignals(False)
|
1000
|
+
|
1001
|
+
def _sync_colorbar_actions(self):
|
1002
|
+
"""
|
1003
|
+
Synchronize the colorbar actions with the current colorbar state.
|
1004
|
+
"""
|
1005
|
+
self.colorbar_switch.block_all_signals(True)
|
1006
|
+
if self._color_bar is not None:
|
1007
|
+
self.colorbar_switch.set_default_action(f"{self.config.color_bar}_colorbar")
|
1008
|
+
self.colorbar_switch.set_state_all(True)
|
1009
|
+
else:
|
1010
|
+
self.colorbar_switch.set_state_all(False)
|
1011
|
+
self.colorbar_switch.block_all_signals(False)
|
1012
|
+
|
1013
|
+
@staticmethod
|
1014
|
+
def cleanup_histogram_lut_item(histogram_lut_item: pg.HistogramLUTItem):
|
1015
|
+
"""
|
1016
|
+
Clean up HistogramLUTItem safely, including open ViewBox menus and child widgets.
|
1017
|
+
|
1018
|
+
Args:
|
1019
|
+
histogram_lut_item(pg.HistogramLUTItem): The HistogramLUTItem to clean up.
|
1020
|
+
"""
|
1021
|
+
histogram_lut_item.vb.menu.close()
|
1022
|
+
histogram_lut_item.vb.menu.deleteLater()
|
1023
|
+
|
1024
|
+
histogram_lut_item.gradient.menu.close()
|
1025
|
+
histogram_lut_item.gradient.menu.deleteLater()
|
1026
|
+
histogram_lut_item.gradient.colorDialog.close()
|
1027
|
+
histogram_lut_item.gradient.colorDialog.deleteLater()
|
1028
|
+
|
1029
|
+
def cleanup(self):
|
1030
|
+
"""
|
1031
|
+
Cleanup the widget.
|
1032
|
+
"""
|
1033
|
+
|
1034
|
+
# Remove all ROIs
|
1035
|
+
rois = self.rois
|
1036
|
+
for roi in rois:
|
1037
|
+
roi.remove()
|
1038
|
+
|
1039
|
+
# Colorbar Cleanup
|
1040
|
+
if self._color_bar:
|
1041
|
+
if self.config.color_bar == "full":
|
1042
|
+
self.cleanup_histogram_lut_item(self._color_bar)
|
1043
|
+
if self.config.color_bar == "simple":
|
1044
|
+
self.plot_widget.removeItem(self._color_bar)
|
1045
|
+
self._color_bar.deleteLater()
|
1046
|
+
self._color_bar = None
|
1047
|
+
|
1048
|
+
# Popup cleanup
|
1049
|
+
if self.roi_manager_dialog is not None:
|
1050
|
+
self.roi_manager_dialog.reject()
|
1051
|
+
self.roi_manager_dialog = None
|
1052
|
+
|
1053
|
+
# ROI plots cleanup
|
1054
|
+
if self.x_roi is not None:
|
1055
|
+
self.x_roi.cleanup_pyqtgraph()
|
1056
|
+
if self.y_roi is not None:
|
1057
|
+
self.y_roi.cleanup_pyqtgraph()
|
1058
|
+
|
1059
|
+
self.layer_manager.clear()
|
1060
|
+
self.layer_manager = None
|
1061
|
+
|
1062
|
+
super().cleanup()
|