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.
Files changed (27) hide show
  1. CHANGELOG.md +84 -0
  2. PKG-INFO +1 -1
  3. bec_widgets/cli/client.py +5 -5
  4. bec_widgets/tests/utils.py +2 -2
  5. bec_widgets/utils/clickable_label.py +13 -0
  6. bec_widgets/utils/colors.py +7 -4
  7. bec_widgets/utils/expandable_frame.py +58 -7
  8. bec_widgets/utils/forms_from_types/forms.py +107 -28
  9. bec_widgets/utils/forms_from_types/items.py +88 -12
  10. bec_widgets/utils/forms_from_types/styles.py +21 -0
  11. bec_widgets/utils/generate_designer_plugin.py +10 -21
  12. bec_widgets/widgets/control/scan_control/scan_control.py +1 -1
  13. bec_widgets/widgets/editors/dict_backed_table.py +31 -11
  14. bec_widgets/widgets/editors/scan_metadata/_util.py +7 -4
  15. bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +10 -4
  16. bec_widgets/widgets/plots/image/image.py +128 -856
  17. bec_widgets/widgets/plots/image/image_base.py +1062 -0
  18. bec_widgets/widgets/plots/image/image_item.py +7 -6
  19. bec_widgets/widgets/services/device_browser/device_browser.py +61 -29
  20. bec_widgets/widgets/services/device_browser/device_item/device_item.py +97 -19
  21. bec_widgets/widgets/services/device_browser/util.py +11 -0
  22. {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/METADATA +1 -1
  23. {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/RECORD +27 -23
  24. pyproject.toml +1 -1
  25. {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/WHEEL +0 -0
  26. {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/entry_points.txt +0 -0
  27. {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,34 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections import defaultdict
3
4
  from typing import Literal
4
5
 
5
6
  import numpy as np
6
- import pyqtgraph as pg
7
7
  from bec_lib import bec_logger
8
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 QDialog, QVBoxLayout, QWidget
9
+ from pydantic import BaseModel, Field, field_validator
10
+ from qtpy.QtWidgets import QWidget
12
11
 
13
12
  from bec_widgets.utils import ConnectionConfig
14
13
  from bec_widgets.utils.colors import Colors
15
14
  from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
16
- from bec_widgets.utils.side_panel import SidePanel
17
- from bec_widgets.utils.toolbar import MaterialIconAction, SwitchableToolBarAction
15
+ from bec_widgets.widgets.plots.image.image_base import ImageBase
18
16
  from bec_widgets.widgets.plots.image.image_item import ImageItem
19
- from bec_widgets.widgets.plots.image.image_roi_plot import ImageROIPlot
20
- from bec_widgets.widgets.plots.image.setting_widgets.image_roi_tree import ROIPropertyTree
21
- from bec_widgets.widgets.plots.image.toolbar_bundles.image_selection import (
22
- MonitorSelectionToolbarBundle,
23
- )
24
- from bec_widgets.widgets.plots.image.toolbar_bundles.processing import ImageProcessingToolbarBundle
25
- from bec_widgets.widgets.plots.plot_base import PlotBase
26
- from bec_widgets.widgets.plots.roi.image_roi import (
27
- BaseROI,
28
- CircularROI,
29
- RectangularROI,
30
- ROIController,
31
- )
32
17
 
33
18
  logger = bec_logger.logger
34
19
 
@@ -49,7 +34,15 @@ class ImageConfig(ConnectionConfig):
49
34
  _validate_color_map = field_validator("color_map")(Colors.validate_color_map)
50
35
 
51
36
 
52
- class Image(PlotBase):
37
+ class ImageLayerConfig(BaseModel):
38
+ monitor: str | None = Field(None, description="The name of the monitor.")
39
+ monitor_type: Literal["1d", "2d", "auto"] = Field("auto", description="The type of monitor.")
40
+ source: Literal["device_monitor_1d", "device_monitor_2d", "auto"] = Field(
41
+ "auto", description="The source of the image data."
42
+ )
43
+
44
+
45
+ class Image(ImageBase):
53
46
  """
54
47
  Image widget for displaying 2D data.
55
48
  """
@@ -93,8 +86,8 @@ class Image(PlotBase):
93
86
  # ImageView Specific Settings
94
87
  "color_map",
95
88
  "color_map.setter",
96
- "vrange",
97
- "vrange.setter",
89
+ "v_range",
90
+ "v_range.setter",
98
91
  "v_min",
99
92
  "v_min.setter",
100
93
  "v_max",
@@ -126,8 +119,6 @@ class Image(PlotBase):
126
119
  "remove_roi",
127
120
  "rois",
128
121
  ]
129
- sync_colorbar_with_autorange = Signal()
130
- image_updated = Signal()
131
122
 
132
123
  def __init__(
133
124
  self,
@@ -142,615 +133,15 @@ class Image(PlotBase):
142
133
  config = ImageConfig(widget_class=self.__class__.__name__)
143
134
  self.gui_id = config.gui_id
144
135
  self._color_bar = None
145
- self._main_image = ImageItem()
146
- self.roi_controller = ROIController(colormap="viridis")
147
- self.x_roi = None
148
- self.y_roi = None
136
+ self.subscriptions: defaultdict[str, ImageLayerConfig] = defaultdict(
137
+ lambda: ImageLayerConfig(monitor=None, monitor_type="auto", source="auto")
138
+ )
149
139
  super().__init__(
150
140
  parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
151
141
  )
152
- self._main_image = ImageItem(parent_image=self)
153
-
154
- self.plot_item.addItem(self._main_image)
142
+ self.layer_removed.connect(self._on_layer_removed)
155
143
  self.scan_id = None
156
144
 
157
- # Default Color map to plasma
158
- self.color_map = "plasma"
159
-
160
- # Initialize ROI plots and side panels
161
- self._add_roi_plots()
162
-
163
- self.roi_manager_dialog = None
164
-
165
- # Refresh theme for ROI plots
166
- self._update_theme()
167
-
168
- ################################################################################
169
- # Widget Specific GUI interactions
170
- ################################################################################
171
- def apply_theme(self, theme: str):
172
- super().apply_theme(theme)
173
- if self.x_roi is not None and self.y_roi is not None:
174
- self.x_roi.apply_theme(theme)
175
- self.y_roi.apply_theme(theme)
176
-
177
- def _init_toolbar(self):
178
-
179
- # add to the first position
180
- self.selection_bundle = MonitorSelectionToolbarBundle(
181
- bundle_id="selection", target_widget=self
182
- )
183
- self.toolbar.add_bundle(bundle=self.selection_bundle, target_widget=self)
184
-
185
- super()._init_toolbar()
186
-
187
- # Image specific changes to PlotBase toolbar
188
- self.toolbar.widgets["reset_legend"].action.setVisible(False)
189
-
190
- # ROI Bundle replacement with switchable crosshair
191
- self.toolbar.remove_bundle("roi")
192
- crosshair = MaterialIconAction(
193
- icon_name="point_scan", tooltip="Show Crosshair", checkable=True
194
- )
195
- crosshair_roi = MaterialIconAction(
196
- icon_name="my_location",
197
- tooltip="Show Crosshair with ROI plots",
198
- checkable=True,
199
- parent=self,
200
- )
201
- crosshair_roi.action.toggled.connect(self.toggle_roi_panels)
202
- crosshair.action.toggled.connect(self.toggle_crosshair)
203
- switch_crosshair = SwitchableToolBarAction(
204
- actions={"crosshair_simple": crosshair, "crosshair_roi": crosshair_roi},
205
- initial_action="crosshair_simple",
206
- tooltip="Crosshair",
207
- checkable=True,
208
- parent=self,
209
- )
210
- self.toolbar.add_action(
211
- action_id="switch_crosshair", action=switch_crosshair, target_widget=self
212
- )
213
-
214
- # Lock aspect ratio button
215
- self.lock_aspect_ratio_action = MaterialIconAction(
216
- icon_name="aspect_ratio", tooltip="Lock Aspect Ratio", checkable=True, parent=self
217
- )
218
- self.toolbar.add_action_to_bundle(
219
- bundle_id="mouse_interaction",
220
- action_id="lock_aspect_ratio",
221
- action=self.lock_aspect_ratio_action,
222
- target_widget=self,
223
- )
224
- self.lock_aspect_ratio_action.action.toggled.connect(
225
- lambda checked: self.setProperty("lock_aspect_ratio", checked)
226
- )
227
- self.lock_aspect_ratio_action.action.setChecked(True)
228
-
229
- self._init_autorange_action()
230
- self._init_colorbar_action()
231
-
232
- # Processing Bundle
233
- self.processing_bundle = ImageProcessingToolbarBundle(
234
- bundle_id="processing", target_widget=self
235
- )
236
- self.toolbar.add_bundle(self.processing_bundle, target_widget=self)
237
-
238
- def _init_autorange_action(self):
239
-
240
- self.autorange_mean_action = MaterialIconAction(
241
- icon_name="hdr_auto", tooltip="Enable Auto Range (Mean)", checkable=True, parent=self
242
- )
243
- self.autorange_max_action = MaterialIconAction(
244
- icon_name="hdr_auto",
245
- tooltip="Enable Auto Range (Max)",
246
- checkable=True,
247
- filled=True,
248
- parent=self,
249
- )
250
-
251
- self.autorange_switch = SwitchableToolBarAction(
252
- actions={
253
- "auto_range_mean": self.autorange_mean_action,
254
- "auto_range_max": self.autorange_max_action,
255
- },
256
- initial_action="auto_range_mean",
257
- tooltip="Enable Auto Range",
258
- checkable=True,
259
- parent=self,
260
- )
261
-
262
- self.toolbar.add_action(
263
- action_id="autorange_image", action=self.autorange_switch, target_widget=self
264
- )
265
-
266
- self.autorange_mean_action.action.toggled.connect(
267
- lambda checked: self.toggle_autorange(checked, mode="mean")
268
- )
269
- self.autorange_max_action.action.toggled.connect(
270
- lambda checked: self.toggle_autorange(checked, mode="max")
271
- )
272
-
273
- self.autorange = True
274
- self.autorange_mode = "mean"
275
-
276
- def _init_colorbar_action(self):
277
- self.full_colorbar_action = MaterialIconAction(
278
- icon_name="edgesensor_low", tooltip="Enable Full Colorbar", checkable=True, parent=self
279
- )
280
- self.simple_colorbar_action = MaterialIconAction(
281
- icon_name="smartphone", tooltip="Enable Simple Colorbar", checkable=True, parent=self
282
- )
283
-
284
- self.colorbar_switch = SwitchableToolBarAction(
285
- actions={
286
- "full_colorbar": self.full_colorbar_action,
287
- "simple_colorbar": self.simple_colorbar_action,
288
- },
289
- initial_action="full_colorbar",
290
- tooltip="Enable Full Colorbar",
291
- checkable=True,
292
- parent=self,
293
- )
294
-
295
- self.toolbar.add_action(
296
- action_id="switch_colorbar", action=self.colorbar_switch, target_widget=self
297
- )
298
-
299
- self.simple_colorbar_action.action.toggled.connect(
300
- lambda checked: self.enable_colorbar(checked, style="simple")
301
- )
302
- self.full_colorbar_action.action.toggled.connect(
303
- lambda checked: self.enable_colorbar(checked, style="full")
304
- )
305
-
306
- ########################################
307
- # ROI Gui Manager
308
- def add_side_menus(self):
309
- super().add_side_menus()
310
-
311
- roi_mgr = ROIPropertyTree(parent=self, image_widget=self)
312
- self.side_panel.add_menu(
313
- action_id="roi_mgr",
314
- icon_name="view_list",
315
- tooltip="ROI Manager",
316
- widget=roi_mgr,
317
- title="ROI Manager",
318
- )
319
-
320
- def add_popups(self):
321
- super().add_popups() # keep Axis Settings
322
-
323
- roi_action = MaterialIconAction(
324
- icon_name="view_list", tooltip="ROI Manager", checkable=True, parent=self
325
- )
326
- # self.popup_bundle.add_action("roi_mgr", roi_action)
327
- self.toolbar.add_action_to_bundle(
328
- bundle_id="popup_bundle", action_id="roi_mgr", action=roi_action, target_widget=self
329
- )
330
- self.toolbar.widgets["roi_mgr"].action.triggered.connect(self.show_roi_manager_popup)
331
-
332
- def show_roi_manager_popup(self):
333
- roi_action = self.toolbar.widgets["roi_mgr"].action
334
- if self.roi_manager_dialog is None or not self.roi_manager_dialog.isVisible():
335
- self.roi_mgr = ROIPropertyTree(parent=self, image_widget=self)
336
- self.roi_manager_dialog = QDialog(modal=False)
337
- self.roi_manager_dialog.layout = QVBoxLayout(self.roi_manager_dialog)
338
- self.roi_manager_dialog.layout.addWidget(self.roi_mgr)
339
- self.roi_manager_dialog.finished.connect(self._roi_mgr_closed)
340
- self.roi_manager_dialog.show()
341
- roi_action.setChecked(True)
342
- else:
343
- self.roi_manager_dialog.raise_()
344
- self.roi_manager_dialog.activateWindow()
345
- roi_action.setChecked(True)
346
-
347
- def _roi_mgr_closed(self):
348
- self.roi_mgr.close()
349
- self.roi_mgr.deleteLater()
350
- self.roi_manager_dialog.close()
351
- self.roi_manager_dialog.deleteLater()
352
- self.roi_manager_dialog = None
353
- self.toolbar.widgets["roi_mgr"].action.setChecked(False)
354
-
355
- def enable_colorbar(
356
- self,
357
- enabled: bool,
358
- style: Literal["full", "simple"] = "full",
359
- vrange: tuple[int, int] | None = None,
360
- ):
361
- """
362
- Enable the colorbar and switch types of colorbars.
363
-
364
- Args:
365
- enabled(bool): Whether to enable the colorbar.
366
- style(Literal["full", "simple"]): The type of colorbar to enable.
367
- vrange(tuple): The range of values to use for the colorbar.
368
- """
369
- autorange_state = self._main_image.autorange
370
- if enabled:
371
- if self._color_bar:
372
- if self.config.color_bar == "full":
373
- self.cleanup_histogram_lut_item(self._color_bar)
374
- self.plot_widget.removeItem(self._color_bar)
375
- self._color_bar = None
376
-
377
- if style == "simple":
378
- self._color_bar = pg.ColorBarItem(colorMap=self.config.color_map)
379
- self._color_bar.setImageItem(self._main_image)
380
- self._color_bar.sigLevelsChangeFinished.connect(
381
- lambda: self.setProperty("autorange", False)
382
- )
383
-
384
- elif style == "full":
385
- self._color_bar = pg.HistogramLUTItem()
386
- self._color_bar.setImageItem(self._main_image)
387
- self._color_bar.gradient.loadPreset(self.config.color_map)
388
- self._color_bar.sigLevelsChanged.connect(
389
- lambda: self.setProperty("autorange", False)
390
- )
391
-
392
- self.plot_widget.addItem(self._color_bar, row=0, col=1)
393
- self.config.color_bar = style
394
- else:
395
- if self._color_bar:
396
- self.plot_widget.removeItem(self._color_bar)
397
- self._color_bar = None
398
- self.config.color_bar = None
399
-
400
- self.autorange = autorange_state
401
- self._sync_colorbar_actions()
402
-
403
- if vrange: # should be at the end to disable the autorange if defined
404
- self.v_range = vrange
405
-
406
- ################################################################################
407
- # Static rois with roi manager
408
-
409
- def add_roi(
410
- self,
411
- kind: Literal["rect", "circle"] = "rect",
412
- name: str | None = None,
413
- line_width: int | None = 5,
414
- pos: tuple[float, float] | None = (10, 10),
415
- size: tuple[float, float] | None = (50, 50),
416
- **pg_kwargs,
417
- ) -> RectangularROI | CircularROI:
418
- """
419
- Add a ROI to the image.
420
-
421
- Args:
422
- kind(str): The type of ROI to add. Options are "rect" or "circle".
423
- name(str): The name of the ROI.
424
- line_width(int): The line width of the ROI.
425
- pos(tuple): The position of the ROI.
426
- size(tuple): The size of the ROI.
427
- **pg_kwargs: Additional arguments for the ROI.
428
-
429
- Returns:
430
- RectangularROI | CircularROI: The created ROI object.
431
- """
432
- if name is None:
433
- name = f"ROI_{len(self.roi_controller.rois) + 1}"
434
- if kind == "rect":
435
- roi = RectangularROI(
436
- pos=pos,
437
- size=size,
438
- parent_image=self,
439
- line_width=line_width,
440
- label=name,
441
- **pg_kwargs,
442
- )
443
- elif kind == "circle":
444
- roi = CircularROI(
445
- pos=pos,
446
- size=size,
447
- parent_image=self,
448
- line_width=line_width,
449
- label=name,
450
- **pg_kwargs,
451
- )
452
- else:
453
- raise ValueError("kind must be 'rect' or 'circle'")
454
-
455
- # Add to plot and controller (controller assigns color)
456
- self.plot_item.addItem(roi)
457
- self.roi_controller.add_roi(roi)
458
- roi.add_scale_handle()
459
- return roi
460
-
461
- def remove_roi(self, roi: int | str):
462
- """Remove an ROI by index or label via the ROIController."""
463
- if isinstance(roi, int):
464
- self.roi_controller.remove_roi_by_index(roi)
465
- elif isinstance(roi, str):
466
- self.roi_controller.remove_roi_by_name(roi)
467
- else:
468
- raise ValueError("roi must be an int index or str name")
469
-
470
- def _add_roi_plots(self):
471
- """
472
- Initialize the ROI plots and side panels.
473
- """
474
- # Create ROI plot widgets
475
- self.x_roi = ImageROIPlot(parent=self)
476
- self.y_roi = ImageROIPlot(parent=self)
477
- self.x_roi.apply_theme("dark")
478
- self.y_roi.apply_theme("dark")
479
-
480
- # Set titles for the plots
481
- self.x_roi.plot_item.setTitle("X ROI")
482
- self.y_roi.plot_item.setTitle("Y ROI")
483
-
484
- # Create side panels
485
- self.side_panel_x = SidePanel(
486
- parent=self, orientation="bottom", panel_max_width=200, show_toolbar=False
487
- )
488
- self.side_panel_y = SidePanel(
489
- parent=self, orientation="left", panel_max_width=200, show_toolbar=False
490
- )
491
-
492
- # Add ROI plots to side panels
493
- self.x_panel_index = self.side_panel_x.add_menu(widget=self.x_roi)
494
- self.y_panel_index = self.side_panel_y.add_menu(widget=self.y_roi)
495
-
496
- # # Add side panels to the layout
497
- self.layout_manager.add_widget_relative(
498
- self.side_panel_x, self.round_plot_widget, position="bottom", shift_direction="down"
499
- )
500
- self.layout_manager.add_widget_relative(
501
- self.side_panel_y, self.round_plot_widget, position="left", shift_direction="right"
502
- )
503
-
504
- def toggle_roi_panels(self, checked: bool):
505
- """
506
- Show or hide the ROI panels based on the test action toggle state.
507
-
508
- Args:
509
- checked (bool): Whether the test action is checked.
510
- """
511
- if checked:
512
- # Show the ROI panels
513
- self.hook_crosshair()
514
- self.side_panel_x.show_panel(self.x_panel_index)
515
- self.side_panel_y.show_panel(self.y_panel_index)
516
- self.crosshair.coordinatesChanged2D.connect(self.update_image_slices)
517
- self.image_updated.connect(self.update_image_slices)
518
- else:
519
- self.unhook_crosshair()
520
- # Hide the ROI panels
521
- self.side_panel_x.hide_panel()
522
- self.side_panel_y.hide_panel()
523
- self.image_updated.disconnect(self.update_image_slices)
524
-
525
- @SafeSlot()
526
- def update_image_slices(self, coordinates: tuple[int, int, int] = None):
527
- """
528
- Update the image slices based on the crosshair position.
529
-
530
- Args:
531
- coordinates(tuple): The coordinates of the crosshair.
532
- """
533
- if coordinates is None:
534
- # Try to get coordinates from crosshair position (like in crosshair mouse_moved)
535
- if (
536
- hasattr(self, "crosshair")
537
- and hasattr(self.crosshair, "v_line")
538
- and hasattr(self.crosshair, "h_line")
539
- ):
540
- x = int(round(self.crosshair.v_line.value()))
541
- y = int(round(self.crosshair.h_line.value()))
542
- else:
543
- return
544
- else:
545
- x = coordinates[1]
546
- y = coordinates[2]
547
- image = self._main_image.image
548
- if image is None:
549
- return
550
- max_row, max_col = image.shape[0] - 1, image.shape[1] - 1
551
- row, col = x, y
552
- if not (0 <= row <= max_row and 0 <= col <= max_col):
553
- return
554
- # Horizontal slice
555
- h_slice = image[:, col]
556
- x_axis = np.arange(h_slice.shape[0])
557
- self.x_roi.plot_item.clear()
558
- self.x_roi.plot_item.plot(x_axis, h_slice, pen=pg.mkPen(self.x_roi.curve_color, width=3))
559
- # Vertical slice
560
- v_slice = image[row, :]
561
- y_axis = np.arange(v_slice.shape[0])
562
- self.y_roi.plot_item.clear()
563
- self.y_roi.plot_item.plot(v_slice, y_axis, pen=pg.mkPen(self.y_roi.curve_color, width=3))
564
-
565
- ################################################################################
566
- # Widget Specific Properties
567
- ################################################################################
568
- ################################################################################
569
- # Rois
570
-
571
- @property
572
- def rois(self) -> list[BaseROI]:
573
- """
574
- Get the list of ROIs.
575
- """
576
- return self.roi_controller.rois
577
-
578
- ################################################################################
579
- # Colorbar toggle
580
-
581
- @SafeProperty(bool)
582
- def enable_simple_colorbar(self) -> bool:
583
- """
584
- Enable the simple colorbar.
585
- """
586
- enabled = False
587
- if self.config.color_bar == "simple":
588
- enabled = True
589
- return enabled
590
-
591
- @enable_simple_colorbar.setter
592
- def enable_simple_colorbar(self, value: bool):
593
- """
594
- Enable the simple colorbar.
595
-
596
- Args:
597
- value(bool): Whether to enable the simple colorbar.
598
- """
599
- self.enable_colorbar(enabled=value, style="simple")
600
-
601
- @SafeProperty(bool)
602
- def enable_full_colorbar(self) -> bool:
603
- """
604
- Enable the full colorbar.
605
- """
606
- enabled = False
607
- if self.config.color_bar == "full":
608
- enabled = True
609
- return enabled
610
-
611
- @enable_full_colorbar.setter
612
- def enable_full_colorbar(self, value: bool):
613
- """
614
- Enable the full colorbar.
615
-
616
- Args:
617
- value(bool): Whether to enable the full colorbar.
618
- """
619
- self.enable_colorbar(enabled=value, style="full")
620
-
621
- ################################################################################
622
- # Appearance
623
-
624
- @SafeProperty(str)
625
- def color_map(self) -> str:
626
- """
627
- Set the color map of the image.
628
- """
629
- return self.config.color_map
630
-
631
- @color_map.setter
632
- def color_map(self, value: str):
633
- """
634
- Set the color map of the image.
635
-
636
- Args:
637
- value(str): The color map to set.
638
- """
639
- try:
640
- self.config.color_map = value
641
- self._main_image.color_map = value
642
-
643
- if self._color_bar:
644
- if self.config.color_bar == "simple":
645
- self._color_bar.setColorMap(value)
646
- elif self.config.color_bar == "full":
647
- self._color_bar.gradient.loadPreset(value)
648
- except ValidationError:
649
- return
650
-
651
- # v_range is for designer, vrange is for RPC
652
- @SafeProperty("QPointF")
653
- def v_range(self) -> QPointF:
654
- """
655
- Set the v_range of the main image.
656
- """
657
- vmin, vmax = self._main_image.v_range
658
- return QPointF(vmin, vmax)
659
-
660
- @v_range.setter
661
- def v_range(self, value: tuple | list | QPointF):
662
- """
663
- Set the v_range of the main image.
664
-
665
- Args:
666
- value(tuple | list | QPointF): The range of values to set.
667
- """
668
- if isinstance(value, (tuple, list)):
669
- value = self._tuple_to_qpointf(value)
670
-
671
- vmin, vmax = value.x(), value.y()
672
-
673
- self._main_image.v_range = (vmin, vmax)
674
-
675
- # propagate to colorbar if exists
676
- if self._color_bar:
677
- if self.config.color_bar == "simple":
678
- self._color_bar.setLevels(low=vmin, high=vmax)
679
- elif self.config.color_bar == "full":
680
- self._color_bar.setLevels(min=vmin, max=vmax)
681
- self._color_bar.setHistogramRange(vmin - 0.1 * vmin, vmax + 0.1 * vmax)
682
-
683
- self.autorange_switch.set_state_all(False)
684
-
685
- @property
686
- def vrange(self) -> tuple:
687
- """
688
- Get the vrange of the image.
689
- """
690
- return (self.v_range.x(), self.v_range.y())
691
-
692
- @vrange.setter
693
- def vrange(self, value):
694
- """
695
- Set the vrange of the image.
696
-
697
- Args:
698
- value(tuple):
699
- """
700
- self.v_range = value
701
-
702
- @property
703
- def v_min(self) -> float:
704
- """
705
- Get the minimum value of the v_range.
706
- """
707
- return self.v_range.x()
708
-
709
- @v_min.setter
710
- def v_min(self, value: float):
711
- """
712
- Set the minimum value of the v_range.
713
-
714
- Args:
715
- value(float): The minimum value to set.
716
- """
717
- self.v_range = (value, self.v_range.y())
718
-
719
- @property
720
- def v_max(self) -> float:
721
- """
722
- Get the maximum value of the v_range.
723
- """
724
- return self.v_range.y()
725
-
726
- @v_max.setter
727
- def v_max(self, value: float):
728
- """
729
- Set the maximum value of the v_range.
730
-
731
- Args:
732
- value(float): The maximum value to set.
733
- """
734
- self.v_range = (self.v_range.x(), value)
735
-
736
- @SafeProperty(bool)
737
- def lock_aspect_ratio(self) -> bool:
738
- """
739
- Whether the aspect ratio is locked.
740
- """
741
- return self.config.lock_aspect_ratio
742
-
743
- @lock_aspect_ratio.setter
744
- def lock_aspect_ratio(self, value: bool):
745
- """
746
- Set the aspect ratio lock.
747
-
748
- Args:
749
- value(bool): Whether to lock the aspect ratio.
750
- """
751
- self.config.lock_aspect_ratio = bool(value)
752
- self.plot_item.setAspectLocked(value)
753
-
754
145
  ################################################################################
755
146
  # Data Acquisition
756
147
 
@@ -759,7 +150,7 @@ class Image(PlotBase):
759
150
  """
760
151
  The name of the monitor to use for the image.
761
152
  """
762
- return self._main_image.config.monitor
153
+ return self.subscriptions["main"].monitor or ""
763
154
 
764
155
  @monitor.setter
765
156
  def monitor(self, value: str):
@@ -769,7 +160,7 @@ class Image(PlotBase):
769
160
  Args:
770
161
  value(str): The name of the monitor to set.
771
162
  """
772
- if self._main_image.config.monitor == value:
163
+ if self.subscriptions["main"].monitor == value:
773
164
  return
774
165
  try:
775
166
  self.entry_validator.validate_monitor(value)
@@ -780,103 +171,95 @@ class Image(PlotBase):
780
171
  @property
781
172
  def main_image(self) -> ImageItem:
782
173
  """Access the main image item."""
783
- return self._main_image
174
+ return self.layer_manager["main"].image
784
175
 
785
176
  ################################################################################
786
- # Autorange + Colorbar sync
787
-
788
- @SafeProperty(bool)
789
- def autorange(self) -> bool:
790
- """
791
- Whether autorange is enabled.
792
- """
793
- return self._main_image.autorange
794
-
795
- @autorange.setter
796
- def autorange(self, enabled: bool):
177
+ # High Level methods for API
178
+ ################################################################################
179
+ @SafeSlot(popup_error=True)
180
+ def image(
181
+ self,
182
+ monitor: str | None = None,
183
+ monitor_type: Literal["auto", "1d", "2d"] = "auto",
184
+ color_map: str | None = None,
185
+ color_bar: Literal["simple", "full"] | None = None,
186
+ vrange: tuple[int, int] | None = None,
187
+ ) -> ImageItem:
797
188
  """
798
- Set autorange.
189
+ Set the image source and update the image.
799
190
 
800
191
  Args:
801
- enabled(bool): Whether to enable autorange.
802
- """
803
- self._main_image.autorange = enabled
804
- if enabled and self._main_image.raw_data is not None:
805
- self._main_image.apply_autorange()
806
- self._sync_colorbar_levels()
807
- self._sync_autorange_switch()
808
-
809
- @SafeProperty(str)
810
- def autorange_mode(self) -> str:
811
- """
812
- Autorange mode.
813
-
814
- Options:
815
- - "max": Use the maximum value of the image for autoranging.
816
- - "mean": Use the mean value of the image for autoranging.
192
+ monitor(str): The name of the monitor to use for the image.
193
+ monitor_type(str): The type of monitor to use. Options are "1d", "2d", or "auto".
194
+ color_map(str): The color map to use for the image.
195
+ color_bar(str): The type of color bar to use. Options are "simple" or "full".
196
+ vrange(tuple): The range of values to use for the color map.
817
197
 
198
+ Returns:
199
+ ImageItem: The image object.
818
200
  """
819
- return self._main_image.autorange_mode
820
201
 
821
- @autorange_mode.setter
822
- def autorange_mode(self, mode: str):
823
- """
824
- Set the autorange mode.
202
+ if self.subscriptions["main"].monitor:
203
+ self.disconnect_monitor(self.subscriptions["main"].monitor)
204
+ self.entry_validator.validate_monitor(monitor)
205
+ self.subscriptions["main"].monitor = monitor
825
206
 
826
- Args:
827
- mode(str): The autorange mode. Options are "max" or "mean".
828
- """
829
- # for qt Designer
830
- if mode not in ["max", "mean"]:
831
- return
832
- self._main_image.autorange_mode = mode
207
+ if monitor_type == "1d":
208
+ self.subscriptions["main"].source = "device_monitor_1d"
209
+ self.subscriptions["main"].monitor_type = "1d"
210
+ elif monitor_type == "2d":
211
+ self.subscriptions["main"].source = "device_monitor_2d"
212
+ self.subscriptions["main"].monitor_type = "2d"
213
+ elif monitor_type == "auto":
214
+ self.subscriptions["main"].source = "auto"
215
+ logger.warning(
216
+ f"Updates for '{monitor}' will be fetch from both 1D and 2D monitor endpoints."
217
+ )
218
+ self.subscriptions["main"].monitor_type = "auto"
833
219
 
834
- self._sync_autorange_switch()
220
+ self.set_image_update(monitor=monitor, type=monitor_type)
221
+ if color_map is not None:
222
+ self.main_image.color_map = color_map
223
+ if color_bar is not None:
224
+ self.enable_colorbar(True, color_bar)
225
+ if vrange is not None:
226
+ self.vrange = vrange
835
227
 
836
- @SafeSlot(bool, str, bool)
837
- def toggle_autorange(self, enabled: bool, mode: str):
838
- """
839
- Toggle autorange.
228
+ self._sync_device_selection()
840
229
 
841
- Args:
842
- enabled(bool): Whether to enable autorange.
843
- mode(str): The autorange mode. Options are "max" or "mean".
844
- """
845
- if self._main_image is not None:
846
- self._main_image.autorange = enabled
847
- self._main_image.autorange_mode = mode
848
- if enabled:
849
- self._main_image.apply_autorange()
850
- self._sync_colorbar_levels()
851
-
852
- def _sync_autorange_switch(self):
853
- """
854
- Synchronize the autorange switch with the current autorange state and mode if changed from outside.
855
- """
856
- self.autorange_switch.block_all_signals(True)
857
- self.autorange_switch.set_default_action(f"auto_range_{self._main_image.autorange_mode}")
858
- self.autorange_switch.set_state_all(self._main_image.autorange)
859
- self.autorange_switch.block_all_signals(False)
860
-
861
- def _sync_colorbar_levels(self):
862
- """Immediately propagate current levels to the active colorbar."""
863
- vrange = self._main_image.v_range
864
- if self._color_bar:
865
- self._color_bar.blockSignals(True)
866
- self.v_range = vrange
867
- self._color_bar.blockSignals(False)
230
+ return self.main_image
868
231
 
869
- def _sync_colorbar_actions(self):
232
+ def _sync_device_selection(self):
870
233
  """
871
- Synchronize the colorbar actions with the current colorbar state.
234
+ Synchronize the device selection with the current monitor.
872
235
  """
873
- self.colorbar_switch.block_all_signals(True)
874
- if self._color_bar is not None:
875
- self.colorbar_switch.set_default_action(f"{self.config.color_bar}_colorbar")
876
- self.colorbar_switch.set_state_all(True)
236
+ config = self.subscriptions["main"]
237
+ if config.monitor is not None:
238
+ for combo in (
239
+ self.selection_bundle.device_combo_box,
240
+ self.selection_bundle.dim_combo_box,
241
+ ):
242
+ combo.blockSignals(True)
243
+ self.selection_bundle.device_combo_box.set_device(config.monitor)
244
+ self.selection_bundle.dim_combo_box.setCurrentText(config.monitor_type)
245
+ for combo in (
246
+ self.selection_bundle.device_combo_box,
247
+ self.selection_bundle.dim_combo_box,
248
+ ):
249
+ combo.blockSignals(False)
877
250
  else:
878
- self.colorbar_switch.set_state_all(False)
879
- self.colorbar_switch.block_all_signals(False)
251
+ for combo in (
252
+ self.selection_bundle.device_combo_box,
253
+ self.selection_bundle.dim_combo_box,
254
+ ):
255
+ combo.blockSignals(True)
256
+ self.selection_bundle.device_combo_box.setCurrentText("")
257
+ self.selection_bundle.dim_combo_box.setCurrentText("auto")
258
+ for combo in (
259
+ self.selection_bundle.device_combo_box,
260
+ self.selection_bundle.dim_combo_box,
261
+ ):
262
+ combo.blockSignals(False)
880
263
 
881
264
  ################################################################################
882
265
  # Post Processing
@@ -887,7 +270,7 @@ class Image(PlotBase):
887
270
  """
888
271
  Whether FFT postprocessing is enabled.
889
272
  """
890
- return self._main_image.fft
273
+ return self.main_image.fft
891
274
 
892
275
  @fft.setter
893
276
  def fft(self, enable: bool):
@@ -897,14 +280,14 @@ class Image(PlotBase):
897
280
  Args:
898
281
  enable(bool): Whether to enable FFT postprocessing.
899
282
  """
900
- self._main_image.fft = enable
283
+ self.main_image.fft = enable
901
284
 
902
285
  @SafeProperty(bool)
903
286
  def log(self) -> bool:
904
287
  """
905
288
  Whether logarithmic scaling is applied.
906
289
  """
907
- return self._main_image.log
290
+ return self.main_image.log
908
291
 
909
292
  @log.setter
910
293
  def log(self, enable: bool):
@@ -914,14 +297,14 @@ class Image(PlotBase):
914
297
  Args:
915
298
  enable(bool): Whether to enable logarithmic scaling.
916
299
  """
917
- self._main_image.log = enable
300
+ self.main_image.log = enable
918
301
 
919
302
  @SafeProperty(int)
920
303
  def num_rotation_90(self) -> int:
921
304
  """
922
305
  The number of 90° rotations to apply counterclockwise.
923
306
  """
924
- return self._main_image.num_rotation_90
307
+ return self.main_image.num_rotation_90
925
308
 
926
309
  @num_rotation_90.setter
927
310
  def num_rotation_90(self, value: int):
@@ -931,14 +314,14 @@ class Image(PlotBase):
931
314
  Args:
932
315
  value(int): The number of 90° rotations to apply.
933
316
  """
934
- self._main_image.num_rotation_90 = value
317
+ self.main_image.num_rotation_90 = value
935
318
 
936
319
  @SafeProperty(bool)
937
320
  def transpose(self) -> bool:
938
321
  """
939
322
  Whether the image is transposed.
940
323
  """
941
- return self._main_image.transpose
324
+ return self.main_image.transpose
942
325
 
943
326
  @transpose.setter
944
327
  def transpose(self, enable: bool):
@@ -948,94 +331,7 @@ class Image(PlotBase):
948
331
  Args:
949
332
  enable(bool): Whether to enable transposing the image.
950
333
  """
951
- self._main_image.transpose = enable
952
-
953
- ################################################################################
954
- # High Level methods for API
955
- ################################################################################
956
- @SafeSlot(popup_error=True)
957
- def image(
958
- self,
959
- monitor: str | None = None,
960
- monitor_type: Literal["auto", "1d", "2d"] = "auto",
961
- color_map: str | None = None,
962
- color_bar: Literal["simple", "full"] | None = None,
963
- vrange: tuple[int, int] | None = None,
964
- ) -> ImageItem:
965
- """
966
- Set the image source and update the image.
967
-
968
- Args:
969
- monitor(str): The name of the monitor to use for the image.
970
- monitor_type(str): The type of monitor to use. Options are "1d", "2d", or "auto".
971
- color_map(str): The color map to use for the image.
972
- color_bar(str): The type of color bar to use. Options are "simple" or "full".
973
- vrange(tuple): The range of values to use for the color map.
974
-
975
- Returns:
976
- ImageItem: The image object.
977
- """
978
-
979
- if self._main_image.config.monitor is not None:
980
- self.disconnect_monitor(self._main_image.config.monitor)
981
- self.entry_validator.validate_monitor(monitor)
982
- self._main_image.config.monitor = monitor
983
-
984
- if monitor_type == "1d":
985
- self._main_image.config.source = "device_monitor_1d"
986
- self._main_image.config.monitor_type = "1d"
987
- elif monitor_type == "2d":
988
- self._main_image.config.source = "device_monitor_2d"
989
- self._main_image.config.monitor_type = "2d"
990
- elif monitor_type == "auto":
991
- self._main_image.config.source = "auto"
992
- logger.warning(
993
- f"Updates for '{monitor}' will be fetch from both 1D and 2D monitor endpoints."
994
- )
995
- self._main_image.config.monitor_type = "auto"
996
-
997
- self.set_image_update(monitor=monitor, type=monitor_type)
998
- if color_map is not None:
999
- self._main_image.color_map = color_map
1000
- if color_bar is not None:
1001
- self.enable_colorbar(True, color_bar)
1002
- if vrange is not None:
1003
- self.vrange = vrange
1004
-
1005
- self._sync_device_selection()
1006
-
1007
- return self._main_image
1008
-
1009
- def _sync_device_selection(self):
1010
- """
1011
- Synchronize the device selection with the current monitor.
1012
- """
1013
- if self._main_image.config.monitor is not None:
1014
- for combo in (
1015
- self.selection_bundle.device_combo_box,
1016
- self.selection_bundle.dim_combo_box,
1017
- ):
1018
- combo.blockSignals(True)
1019
- self.selection_bundle.device_combo_box.set_device(self._main_image.config.monitor)
1020
- self.selection_bundle.dim_combo_box.setCurrentText(self._main_image.config.monitor_type)
1021
- for combo in (
1022
- self.selection_bundle.device_combo_box,
1023
- self.selection_bundle.dim_combo_box,
1024
- ):
1025
- combo.blockSignals(False)
1026
- else:
1027
- for combo in (
1028
- self.selection_bundle.device_combo_box,
1029
- self.selection_bundle.dim_combo_box,
1030
- ):
1031
- combo.blockSignals(True)
1032
- self.selection_bundle.device_combo_box.setCurrentText("")
1033
- self.selection_bundle.dim_combo_box.setCurrentText("auto")
1034
- for combo in (
1035
- self.selection_bundle.device_combo_box,
1036
- self.selection_bundle.dim_combo_box,
1037
- ):
1038
- combo.blockSignals(False)
334
+ self.main_image.transpose = enable
1039
335
 
1040
336
  ################################################################################
1041
337
  # Image Update Methods
@@ -1069,8 +365,8 @@ class Image(PlotBase):
1069
365
  self.bec_dispatcher.connect_slot(
1070
366
  self.on_image_update_2d, MessageEndpoints.device_monitor_2d(monitor)
1071
367
  )
1072
- print(f"Connected to {monitor} with type {type}")
1073
- self._main_image.config.monitor = monitor
368
+ logger.info(f"Connected to {monitor} with type {type}")
369
+ self.subscriptions["main"].monitor = monitor
1074
370
 
1075
371
  def disconnect_monitor(self, monitor: str):
1076
372
  """
@@ -1085,7 +381,7 @@ class Image(PlotBase):
1085
381
  self.bec_dispatcher.disconnect_slot(
1086
382
  self.on_image_update_2d, MessageEndpoints.device_monitor_2d(monitor)
1087
383
  )
1088
- self._main_image.config.monitor = None
384
+ self.subscriptions["main"].monitor = None
1089
385
  self._sync_device_selection()
1090
386
 
1091
387
  ########################################
@@ -1107,13 +403,13 @@ class Image(PlotBase):
1107
403
  return
1108
404
  if current_scan_id != self.scan_id:
1109
405
  self.scan_id = current_scan_id
1110
- self._main_image.clear()
1111
- self._main_image.buffer = []
1112
- self._main_image.max_len = 0
1113
- image_buffer = self.adjust_image_buffer(self._main_image, data)
406
+ self.main_image.clear()
407
+ self.main_image.buffer = []
408
+ self.main_image.max_len = 0
409
+ image_buffer = self.adjust_image_buffer(self.main_image, data)
1114
410
  if self._color_bar is not None:
1115
411
  self._color_bar.blockSignals(True)
1116
- self._main_image.set_data(image_buffer)
412
+ self.main_image.set_data(image_buffer)
1117
413
  if self._color_bar is not None:
1118
414
  self._color_bar.blockSignals(False)
1119
415
  self.image_updated.emit()
@@ -1165,7 +461,7 @@ class Image(PlotBase):
1165
461
  data = msg["data"]
1166
462
  if self._color_bar is not None:
1167
463
  self._color_bar.blockSignals(True)
1168
- self._main_image.set_data(data)
464
+ self.main_image.set_data(data)
1169
465
  if self._color_bar is not None:
1170
466
  self._color_bar.blockSignals(False)
1171
467
  self.image_updated.emit()
@@ -1174,60 +470,36 @@ class Image(PlotBase):
1174
470
  # Clean up
1175
471
  ################################################################################
1176
472
 
1177
- @staticmethod
1178
- def cleanup_histogram_lut_item(histogram_lut_item: pg.HistogramLUTItem):
473
+ @SafeSlot(str)
474
+ def _on_layer_removed(self, layer_name: str):
1179
475
  """
1180
- Clean up HistogramLUTItem safely, including open ViewBox menus and child widgets.
476
+ Handle the removal of a layer by disconnecting the monitor.
1181
477
 
1182
478
  Args:
1183
- histogram_lut_item(pg.HistogramLUTItem): The HistogramLUTItem to clean up.
479
+ layer_name(str): The name of the layer that was removed.
1184
480
  """
1185
- histogram_lut_item.vb.menu.close()
1186
- histogram_lut_item.vb.menu.deleteLater()
1187
-
1188
- histogram_lut_item.gradient.menu.close()
1189
- histogram_lut_item.gradient.menu.deleteLater()
1190
- histogram_lut_item.gradient.colorDialog.close()
1191
- histogram_lut_item.gradient.colorDialog.deleteLater()
481
+ if layer_name not in self.subscriptions:
482
+ return
483
+ config = self.subscriptions[layer_name]
484
+ if config.monitor is not None:
485
+ self.disconnect_monitor(config.monitor)
486
+ config.monitor = None
1192
487
 
1193
488
  def cleanup(self):
1194
489
  """
1195
490
  Disconnect the image update signals and clean up the image.
1196
491
  """
1197
- # Remove all ROIs
1198
- rois = self.rois
1199
- for roi in rois:
1200
- roi.remove()
1201
-
1202
- # Main Image cleanup
1203
- if self._main_image.config.monitor is not None:
1204
- self.disconnect_monitor(self._main_image.config.monitor)
1205
- self._main_image.config.monitor = None
1206
- self.plot_item.removeItem(self._main_image)
1207
- self._main_image = None
1208
-
1209
- # Colorbar Cleanup
1210
- if self._color_bar:
1211
- if self.config.color_bar == "full":
1212
- self.cleanup_histogram_lut_item(self._color_bar)
1213
- if self.config.color_bar == "simple":
1214
- self.plot_widget.removeItem(self._color_bar)
1215
- self._color_bar.deleteLater()
1216
- self._color_bar = None
1217
-
1218
- # Popup cleanup
1219
- if self.roi_manager_dialog is not None:
1220
- self.roi_manager_dialog.reject()
1221
- self.roi_manager_dialog = None
492
+ self.layer_removed.disconnect(self._on_layer_removed)
493
+ for layer_name in list(self.subscriptions.keys()):
494
+ config = self.subscriptions[layer_name]
495
+ if config.monitor is not None:
496
+ self.disconnect_monitor(config.monitor)
497
+ del self.subscriptions[layer_name]
498
+ self.subscriptions.clear()
1222
499
 
1223
500
  # Toolbar cleanup
1224
501
  self.toolbar.widgets["monitor"].widget.close()
1225
502
  self.toolbar.widgets["monitor"].widget.deleteLater()
1226
-
1227
- # ROI plots cleanup
1228
- self.x_roi.cleanup_pyqtgraph()
1229
- self.y_roi.cleanup_pyqtgraph()
1230
-
1231
503
  super().cleanup()
1232
504
 
1233
505