bec-widgets 1.17.2__py3-none-any.whl → 1.18.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.
Files changed (27) hide show
  1. .gitlab-ci.yml +5 -6
  2. CHANGELOG.md +37 -0
  3. PKG-INFO +1 -4
  4. README.md +15 -19
  5. bec_widgets/cli/generate_cli.py +15 -6
  6. bec_widgets/examples/jupyter_console/jupyter_console_window.py +12 -0
  7. bec_widgets/qt_utils/round_frame.py +6 -4
  8. bec_widgets/qt_utils/side_panel.py +60 -79
  9. bec_widgets/widgets/control/device_input/signal_combobox/register_signal_combo_box.py +17 -0
  10. bec_widgets/widgets/control/device_input/signal_combobox/signal_combo_box.pyproject +1 -0
  11. bec_widgets/widgets/control/device_input/signal_combobox/signal_combo_box_plugin.py +54 -0
  12. bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit_plugin.py +1 -1
  13. bec_widgets/widgets/plots_next_gen/plot_base.py +571 -0
  14. bec_widgets/widgets/plots_next_gen/setting_menus/__init__.py +0 -0
  15. bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings.py +95 -0
  16. bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings_horizontal.ui +256 -0
  17. bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings_vertical.ui +240 -0
  18. bec_widgets/widgets/plots_next_gen/toolbar_bundles/__init__.py +0 -0
  19. bec_widgets/widgets/plots_next_gen/toolbar_bundles/mouse_interactions.py +88 -0
  20. bec_widgets/widgets/plots_next_gen/toolbar_bundles/plot_export.py +63 -0
  21. bec_widgets/widgets/plots_next_gen/toolbar_bundles/save_state.py +48 -0
  22. {bec_widgets-1.17.2.dist-info → bec_widgets-1.18.1.dist-info}/METADATA +1 -4
  23. {bec_widgets-1.17.2.dist-info → bec_widgets-1.18.1.dist-info}/RECORD +27 -15
  24. pyproject.toml +1 -2
  25. {bec_widgets-1.17.2.dist-info → bec_widgets-1.18.1.dist-info}/WHEEL +0 -0
  26. {bec_widgets-1.17.2.dist-info → bec_widgets-1.18.1.dist-info}/entry_points.txt +0 -0
  27. {bec_widgets-1.17.2.dist-info → bec_widgets-1.18.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,571 @@
1
+ from __future__ import annotations
2
+
3
+ import pyqtgraph as pg
4
+ from bec_lib import bec_logger
5
+ from qtpy.QtCore import QPoint, QPointF, Qt, Signal
6
+ from qtpy.QtWidgets import QLabel, QVBoxLayout, QWidget
7
+
8
+ from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
9
+ from bec_widgets.qt_utils.round_frame import RoundedFrame
10
+ from bec_widgets.qt_utils.side_panel import SidePanel
11
+ from bec_widgets.qt_utils.toolbar import MaterialIconAction, ModularToolBar, SeparatorAction
12
+ from bec_widgets.utils import ConnectionConfig, Crosshair, EntryValidator
13
+ from bec_widgets.utils.bec_widget import BECWidget
14
+ from bec_widgets.utils.colors import set_theme
15
+ from bec_widgets.utils.fps_counter import FPSCounter
16
+ from bec_widgets.utils.widget_state_manager import WidgetStateManager
17
+ from bec_widgets.widgets.containers.layout_manager.layout_manager import LayoutManagerWidget
18
+ from bec_widgets.widgets.plots_next_gen.setting_menus.axis_settings import AxisSettings
19
+ from bec_widgets.widgets.plots_next_gen.toolbar_bundles.mouse_interactions import (
20
+ MouseInteractionToolbarBundle,
21
+ )
22
+ from bec_widgets.widgets.plots_next_gen.toolbar_bundles.plot_export import PlotExportBundle
23
+ from bec_widgets.widgets.plots_next_gen.toolbar_bundles.save_state import SaveStateBundle
24
+ from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
25
+
26
+ logger = bec_logger.logger
27
+
28
+
29
+ class BECViewBox(pg.ViewBox):
30
+ sigPaint = Signal()
31
+
32
+ def paint(self, painter, opt, widget):
33
+ super().paint(painter, opt, widget)
34
+ self.sigPaint.emit()
35
+
36
+ def itemBoundsChanged(self, item):
37
+ self._itemBoundsCache.pop(item, None)
38
+ if (self.state["autoRange"][0] is not False) or (self.state["autoRange"][1] is not False):
39
+ # check if the call is coming from a mouse-move event
40
+ if hasattr(item, "skip_auto_range") and item.skip_auto_range:
41
+ return
42
+ self._autoRangeNeedsUpdate = True
43
+ self.update()
44
+
45
+
46
+ class PlotBase(BECWidget, QWidget):
47
+ PLUGIN = False
48
+ RPC = False
49
+
50
+ # Custom Signals
51
+ property_changed = Signal(str, object)
52
+ crosshair_position_changed = Signal(tuple)
53
+ crosshair_position_clicked = Signal(tuple)
54
+ crosshair_coordinates_changed = Signal(tuple)
55
+ crosshair_coordinates_clicked = Signal(tuple)
56
+
57
+ def __init__(
58
+ self,
59
+ parent: QWidget | None = None,
60
+ config: ConnectionConfig | None = None,
61
+ client=None,
62
+ gui_id: str | None = None,
63
+ ) -> None:
64
+ if config is None:
65
+ config = ConnectionConfig(widget_class=self.__class__.__name__)
66
+ super().__init__(client=client, gui_id=gui_id, config=config)
67
+ QWidget.__init__(self, parent=parent)
68
+
69
+ # For PropertyManager identification
70
+ self.setObjectName("PlotBase")
71
+ self.get_bec_shortcuts()
72
+
73
+ # Layout Management
74
+ self.layout = QVBoxLayout(self)
75
+ self.layout.setContentsMargins(0, 0, 0, 0)
76
+ self.layout.setSpacing(0)
77
+ self.layout_manager = LayoutManagerWidget(parent=self)
78
+
79
+ # Property Manager
80
+ self.state_manager = WidgetStateManager(self)
81
+
82
+ # Entry Validator
83
+ self.entry_validator = EntryValidator(self.dev)
84
+
85
+ # Base widgets elements
86
+ self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
87
+ self.plot_widget = pg.PlotWidget(plotItem=self.plot_item)
88
+ self.side_panel = SidePanel(self, orientation="left", panel_max_width=280)
89
+ self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
90
+ self.init_toolbar()
91
+
92
+ # PlotItem Addons
93
+ self.plot_item.addLegend()
94
+ self.crosshair = None
95
+ self.fps_monitor = None
96
+ self.fps_label = QLabel(alignment=Qt.AlignmentFlag.AlignRight)
97
+
98
+ self._init_ui()
99
+
100
+ def _init_ui(self):
101
+ self.layout.addWidget(self.layout_manager)
102
+ self.round_plot_widget = RoundedFrame(content_widget=self.plot_widget, theme_update=True)
103
+ self.round_plot_widget.apply_theme("dark")
104
+
105
+ self.layout_manager.add_widget(self.round_plot_widget)
106
+ self.layout_manager.add_widget_relative(self.fps_label, self.round_plot_widget, "top")
107
+ self.fps_label.hide()
108
+ self.layout_manager.add_widget_relative(self.side_panel, self.round_plot_widget, "left")
109
+ self.layout_manager.add_widget_relative(self.toolbar, self.fps_label, "top")
110
+
111
+ self.add_side_menus()
112
+
113
+ # PlotItem ViewBox Signals
114
+ self.plot_item.vb.sigStateChanged.connect(self.viewbox_state_changed)
115
+
116
+ def init_toolbar(self):
117
+
118
+ self.plot_export_bundle = PlotExportBundle("plot_export", target_widget=self)
119
+ self.mouse_bundle = MouseInteractionToolbarBundle("mouse_interaction", target_widget=self)
120
+ self.state_export_bundle = SaveStateBundle("state_export", target_widget=self)
121
+
122
+ # Add elements to toolbar
123
+ self.toolbar.add_bundle(self.plot_export_bundle, target_widget=self)
124
+ self.toolbar.add_bundle(self.state_export_bundle, target_widget=self)
125
+ self.toolbar.add_bundle(self.mouse_bundle, target_widget=self)
126
+
127
+ self.toolbar.add_action("separator_0", SeparatorAction(), target_widget=self)
128
+ self.toolbar.add_action(
129
+ "crosshair",
130
+ MaterialIconAction(icon_name="point_scan", tooltip="Show Crosshair", checkable=True),
131
+ target_widget=self,
132
+ )
133
+ self.toolbar.add_action("separator_1", SeparatorAction(), target_widget=self)
134
+ self.toolbar.add_action(
135
+ "fps_monitor",
136
+ MaterialIconAction(icon_name="speed", tooltip="Show FPS Monitor", checkable=True),
137
+ target_widget=self,
138
+ )
139
+ self.toolbar.addWidget(DarkModeButton(toolbar=True))
140
+
141
+ self.toolbar.widgets["fps_monitor"].action.toggled.connect(
142
+ lambda checked: setattr(self, "enable_fps_monitor", checked)
143
+ )
144
+ self.toolbar.widgets["crosshair"].action.toggled.connect(self.toggle_crosshair)
145
+
146
+ def add_side_menus(self):
147
+ """Adds multiple menus to the side panel."""
148
+ # Setting Axis Widget
149
+ axis_setting = AxisSettings(target_widget=self)
150
+ self.side_panel.add_menu(
151
+ action_id="axis",
152
+ icon_name="settings",
153
+ tooltip="Show Axis Settings",
154
+ widget=axis_setting,
155
+ title="Axis Settings",
156
+ )
157
+
158
+ ################################################################################
159
+ # Toggle UI Elements
160
+ ################################################################################
161
+
162
+ @SafeProperty(bool, doc="Show Toolbar")
163
+ def enable_toolbar(self) -> bool:
164
+ return self.toolbar.isVisible()
165
+
166
+ @enable_toolbar.setter
167
+ def enable_toolbar(self, value: bool):
168
+ self.toolbar.setVisible(value)
169
+
170
+ @SafeProperty(bool, doc="Show Side Panel")
171
+ def enable_side_panel(self) -> bool:
172
+ return self.side_panel.isVisible()
173
+
174
+ @enable_side_panel.setter
175
+ def enable_side_panel(self, value: bool):
176
+ self.side_panel.setVisible(value)
177
+
178
+ @SafeProperty(bool, doc="Enable the FPS monitor.")
179
+ def enable_fps_monitor(self) -> bool:
180
+ return self.fps_label.isVisible()
181
+
182
+ @enable_fps_monitor.setter
183
+ def enable_fps_monitor(self, value: bool):
184
+ if value and self.fps_monitor is None:
185
+ self.hook_fps_monitor()
186
+ elif not value and self.fps_monitor is not None:
187
+ self.unhook_fps_monitor()
188
+
189
+ ################################################################################
190
+ # ViewBox State Signals
191
+ ################################################################################
192
+
193
+ def viewbox_state_changed(self):
194
+ """
195
+ Emit a signal when the state of the viewbox has changed.
196
+ Merges the default pyqtgraphs signal states and also CTRL menu toggles.
197
+ """
198
+
199
+ viewbox_state = self.plot_item.vb.getState()
200
+ # Range Limits
201
+ x_min, x_max = viewbox_state["targetRange"][0]
202
+ y_min, y_max = viewbox_state["targetRange"][1]
203
+ self.property_changed.emit("x_min", x_min)
204
+ self.property_changed.emit("x_max", x_max)
205
+ self.property_changed.emit("y_min", y_min)
206
+ self.property_changed.emit("y_max", y_max)
207
+
208
+ # Grid Toggles
209
+
210
+ ################################################################################
211
+ # Plot Properties
212
+ ################################################################################
213
+
214
+ def set(self, **kwargs):
215
+ """
216
+ Set the properties of the plot widget.
217
+
218
+ Args:
219
+ **kwargs: Keyword arguments for the properties to be set.
220
+
221
+ Possible properties:
222
+
223
+ """
224
+ property_map = {
225
+ "title": self.title,
226
+ "x_label": self.x_label,
227
+ "y_label": self.y_label,
228
+ "x_limits": self.x_limits,
229
+ "y_limits": self.y_limits,
230
+ "x_grid": self.x_grid,
231
+ "y_grid": self.y_grid,
232
+ "inner_axes": self.inner_axes,
233
+ "outer_axes": self.outer_axes,
234
+ "lock_aspect_ratio": self.lock_aspect_ratio,
235
+ "auto_range_x": self.auto_range_x,
236
+ "auto_range_y": self.auto_range_y,
237
+ "x_log": self.x_log,
238
+ "y_log": self.y_log,
239
+ "legend_label_size": self.legend_label_size,
240
+ }
241
+
242
+ for key, value in kwargs.items():
243
+ if key in property_map:
244
+ setattr(self, key, value)
245
+ else:
246
+ logger.warning(f"Property {key} not found.")
247
+
248
+ @SafeProperty(str, doc="The title of the axes.")
249
+ def title(self) -> str:
250
+ return self.plot_item.titleLabel.text
251
+
252
+ @title.setter
253
+ def title(self, value: str):
254
+ self.plot_item.setTitle(value)
255
+ self.property_changed.emit("title", value)
256
+
257
+ @SafeProperty(str, doc="The text of the x label")
258
+ def x_label(self) -> str:
259
+ return self.plot_item.getAxis("bottom").labelText
260
+
261
+ @x_label.setter
262
+ def x_label(self, value: str):
263
+ self.plot_item.setLabel("bottom", text=value)
264
+ self.property_changed.emit("x_label", value)
265
+
266
+ @SafeProperty(str, doc="The text of the y label")
267
+ def y_label(self) -> str:
268
+ return self.plot_item.getAxis("left").labelText
269
+
270
+ @y_label.setter
271
+ def y_label(self, value: str):
272
+ self.plot_item.setLabel("left", text=value)
273
+ self.property_changed.emit("y_label", value)
274
+
275
+ def _tuple_to_qpointf(self, tuple: tuple | list):
276
+ """
277
+ Helper function to convert a tuple to a QPointF.
278
+
279
+ Args:
280
+ tuple(tuple|list): Tuple or list of two numbers.
281
+
282
+ Returns:
283
+ QPointF: The tuple converted to a QPointF.
284
+ """
285
+ if len(tuple) != 2:
286
+ raise ValueError("Limits must be a tuple or list of two numbers.")
287
+ min_val, max_val = tuple
288
+ if not isinstance(min_val, (int, float)) or not isinstance(max_val, (int, float)):
289
+ raise TypeError("Limits must be numbers.")
290
+ if min_val > max_val:
291
+ raise ValueError("Minimum limit cannot be greater than maximum limit.")
292
+ return QPoint(*tuple)
293
+
294
+ ################################################################################
295
+ # X limits, has to be SaveProperty("QPointF") because of the tuple conversion for designer,
296
+ # the python properties are used for CLI and API for context dialog settings.
297
+
298
+ @SafeProperty("QPointF")
299
+ def x_limits(self) -> QPointF:
300
+ current_lim = self.plot_item.vb.viewRange()[0]
301
+ return QPointF(current_lim[0], current_lim[1])
302
+
303
+ @x_limits.setter
304
+ def x_limits(self, value):
305
+ if isinstance(value, (tuple, list)):
306
+ value = self._tuple_to_qpointf(value)
307
+ self.plot_item.vb.setXRange(value.x(), value.y(), padding=0)
308
+
309
+ @property
310
+ def x_lim(self) -> tuple:
311
+ return (self.x_limits.x(), self.x_limits.y())
312
+
313
+ @x_lim.setter
314
+ def x_lim(self, value):
315
+ self.x_limits = value
316
+
317
+ @property
318
+ def x_min(self) -> float:
319
+ return self.x_limits.x()
320
+
321
+ @x_min.setter
322
+ def x_min(self, value: float):
323
+ self.x_limits = (value, self.x_lim[1])
324
+
325
+ @property
326
+ def x_max(self) -> float:
327
+ return self.x_limits.y()
328
+
329
+ @x_max.setter
330
+ def x_max(self, value: float):
331
+ self.x_limits = (self.x_lim[0], value)
332
+
333
+ ################################################################################
334
+ # Y limits, has to be SaveProperty("QPointF") because of the tuple conversion for designer,
335
+ # the python properties are used for CLI and API for context dialog settings.
336
+
337
+ @SafeProperty("QPointF")
338
+ def y_limits(self) -> QPointF:
339
+ current_lim = self.plot_item.vb.viewRange()[1]
340
+ return QPointF(current_lim[0], current_lim[1])
341
+
342
+ @y_limits.setter
343
+ def y_limits(self, value):
344
+ if isinstance(value, (tuple, list)):
345
+ value = self._tuple_to_qpointf(value)
346
+ self.plot_item.vb.setYRange(value.x(), value.y(), padding=0)
347
+
348
+ @property
349
+ def y_lim(self) -> tuple:
350
+ return (self.y_limits.x(), self.y_limits.y())
351
+
352
+ @y_lim.setter
353
+ def y_lim(self, value):
354
+ self.y_limits = value
355
+
356
+ @property
357
+ def y_min(self) -> float:
358
+ return self.y_limits.x()
359
+
360
+ @y_min.setter
361
+ def y_min(self, value: float):
362
+ self.y_limits = (value, self.y_lim[1])
363
+
364
+ @property
365
+ def y_max(self) -> float:
366
+ return self.y_limits.y()
367
+
368
+ @y_max.setter
369
+ def y_max(self, value: float):
370
+ self.y_limits = (self.y_lim[0], value)
371
+
372
+ @SafeProperty(bool, doc="Show grid on the x-axis.")
373
+ def x_grid(self) -> bool:
374
+ return self.plot_item.ctrl.xGridCheck.isChecked()
375
+
376
+ @x_grid.setter
377
+ def x_grid(self, value: bool):
378
+ self.plot_item.showGrid(x=value)
379
+ self.property_changed.emit("x_grid", value)
380
+
381
+ @SafeProperty(bool, doc="Show grid on the y-axis.")
382
+ def y_grid(self) -> bool:
383
+ return self.plot_item.ctrl.yGridCheck.isChecked()
384
+
385
+ @y_grid.setter
386
+ def y_grid(self, value: bool):
387
+ self.plot_item.showGrid(y=value)
388
+ self.property_changed.emit("y_grid", value)
389
+
390
+ @SafeProperty(bool, doc="Set X-axis to log scale if True, linear if False.")
391
+ def x_log(self) -> bool:
392
+ return bool(self.plot_item.vb.state.get("logMode", [False, False])[0])
393
+
394
+ @x_log.setter
395
+ def x_log(self, value: bool):
396
+ self.plot_item.setLogMode(x=value)
397
+ self.property_changed.emit("x_log", value)
398
+
399
+ @SafeProperty(bool, doc="Set Y-axis to log scale if True, linear if False.")
400
+ def y_log(self) -> bool:
401
+ return bool(self.plot_item.vb.state.get("logMode", [False, False])[1])
402
+
403
+ @y_log.setter
404
+ def y_log(self, value: bool):
405
+ self.plot_item.setLogMode(y=value)
406
+ self.property_changed.emit("y_log", value)
407
+
408
+ @SafeProperty(bool, doc="Show the outer axes of the plot widget.")
409
+ def outer_axes(self) -> bool:
410
+ return self.plot_item.getAxis("top").isVisible()
411
+
412
+ @outer_axes.setter
413
+ def outer_axes(self, value: bool):
414
+ self.plot_item.showAxis("top", value)
415
+ self.plot_item.showAxis("right", value)
416
+ self.property_changed.emit("outer_axes", value)
417
+
418
+ @SafeProperty(bool, doc="Show inner axes of the plot widget.")
419
+ def inner_axes(self) -> bool:
420
+ return self.plot_item.getAxis("bottom").isVisible()
421
+
422
+ @inner_axes.setter
423
+ def inner_axes(self, value: bool):
424
+ self.plot_item.showAxis("bottom", value)
425
+ self.plot_item.showAxis("left", value)
426
+ self.property_changed.emit("inner_axes", value)
427
+
428
+ @SafeProperty(bool, doc="Lock aspect ratio of the plot widget.")
429
+ def lock_aspect_ratio(self) -> bool:
430
+ return bool(self.plot_item.vb.getState()["aspectLocked"])
431
+
432
+ @lock_aspect_ratio.setter
433
+ def lock_aspect_ratio(self, value: bool):
434
+ self.plot_item.setAspectLocked(value)
435
+
436
+ @SafeProperty(bool, doc="Set auto range for the x-axis.")
437
+ def auto_range_x(self) -> bool:
438
+ return bool(self.plot_item.vb.getState()["autoRange"][0])
439
+
440
+ @auto_range_x.setter
441
+ def auto_range_x(self, value: bool):
442
+ self.plot_item.enableAutoRange(x=value)
443
+
444
+ @SafeProperty(bool, doc="Set auto range for the y-axis.")
445
+ def auto_range_y(self) -> bool:
446
+ return bool(self.plot_item.vb.getState()["autoRange"][1])
447
+
448
+ @auto_range_y.setter
449
+ def auto_range_y(self, value: bool):
450
+ self.plot_item.enableAutoRange(y=value)
451
+
452
+ @SafeProperty(int, doc="The font size of the legend font.")
453
+ def legend_label_size(self) -> int:
454
+ if not self.plot_item.legend:
455
+ return
456
+ scale = self.plot_item.legend.scale() * 9
457
+ return scale
458
+
459
+ @legend_label_size.setter
460
+ def legend_label_size(self, value: int):
461
+ if not self.plot_item.legend:
462
+ return
463
+ scale = (
464
+ value / 9
465
+ ) # 9 is the default font size of the legend, so we always scale it against 9
466
+ self.plot_item.legend.setScale(scale)
467
+
468
+ ################################################################################
469
+ # FPS Counter
470
+ ################################################################################
471
+
472
+ def update_fps_label(self, fps: float) -> None:
473
+ """
474
+ Update the FPS label.
475
+
476
+ Args:
477
+ fps(float): The frames per second.
478
+ """
479
+ if self.fps_label:
480
+ self.fps_label.setText(f"FPS: {fps:.2f}")
481
+
482
+ def hook_fps_monitor(self):
483
+ """Hook the FPS monitor to the plot."""
484
+ if self.fps_monitor is None:
485
+ self.fps_monitor = FPSCounter(self.plot_item.vb)
486
+ self.fps_label.show()
487
+
488
+ self.fps_monitor.sigFpsUpdate.connect(self.update_fps_label)
489
+ self.update_fps_label(0)
490
+
491
+ def unhook_fps_monitor(self, delete_label=True):
492
+ """Unhook the FPS monitor from the plot."""
493
+ if self.fps_monitor is not None and delete_label:
494
+ # Remove Monitor
495
+ self.fps_monitor.cleanup()
496
+ self.fps_monitor.deleteLater()
497
+ self.fps_monitor = None
498
+ if self.fps_label is not None:
499
+ # Hide Label
500
+ self.fps_label.hide()
501
+
502
+ ################################################################################
503
+ # Crosshair
504
+ ################################################################################
505
+
506
+ def hook_crosshair(self) -> None:
507
+ """Hook the crosshair to all plots."""
508
+ if self.crosshair is None:
509
+ self.crosshair = Crosshair(self.plot_item, precision=3)
510
+ self.crosshair.crosshairChanged.connect(self.crosshair_position_changed)
511
+ self.crosshair.crosshairClicked.connect(self.crosshair_position_clicked)
512
+ self.crosshair.coordinatesChanged1D.connect(self.crosshair_coordinates_changed)
513
+ self.crosshair.coordinatesClicked1D.connect(self.crosshair_coordinates_clicked)
514
+ self.crosshair.coordinatesChanged2D.connect(self.crosshair_coordinates_changed)
515
+ self.crosshair.coordinatesClicked2D.connect(self.crosshair_coordinates_clicked)
516
+
517
+ def unhook_crosshair(self) -> None:
518
+ """Unhook the crosshair from all plots."""
519
+ if self.crosshair is not None:
520
+ self.crosshair.crosshairChanged.disconnect(self.crosshair_position_changed)
521
+ self.crosshair.crosshairClicked.disconnect(self.crosshair_position_clicked)
522
+ self.crosshair.coordinatesChanged1D.disconnect(self.crosshair_coordinates_changed)
523
+ self.crosshair.coordinatesClicked1D.disconnect(self.crosshair_coordinates_clicked)
524
+ self.crosshair.coordinatesChanged2D.disconnect(self.crosshair_coordinates_changed)
525
+ self.crosshair.coordinatesClicked2D.disconnect(self.crosshair_coordinates_clicked)
526
+ self.crosshair.cleanup()
527
+ self.crosshair.deleteLater()
528
+ self.crosshair = None
529
+
530
+ def toggle_crosshair(self) -> None:
531
+ """Toggle the crosshair on all plots."""
532
+ if self.crosshair is None:
533
+ return self.hook_crosshair()
534
+
535
+ self.unhook_crosshair()
536
+
537
+ @SafeSlot()
538
+ def reset(self) -> None:
539
+ """Reset the plot widget."""
540
+ if self.crosshair is not None:
541
+ self.crosshair.clear_markers()
542
+ self.crosshair.update_markers()
543
+
544
+ def cleanup(self):
545
+ self.unhook_crosshair()
546
+ self.unhook_fps_monitor(delete_label=True)
547
+ self.cleanup_pyqtgraph()
548
+
549
+ def cleanup_pyqtgraph(self):
550
+ """Cleanup pyqtgraph items."""
551
+ item = self.plot_item
552
+ item.vb.menu.close()
553
+ item.vb.menu.deleteLater()
554
+ item.ctrlMenu.close()
555
+ item.ctrlMenu.deleteLater()
556
+
557
+
558
+ if __name__ == "__main__": # pragma: no cover:
559
+ import sys
560
+
561
+ from qtpy.QtWidgets import QApplication
562
+
563
+ app = QApplication(sys.argv)
564
+ set_theme("dark")
565
+ widget = PlotBase()
566
+ widget.show()
567
+ # Just some example data and parameters to test
568
+ widget.y_grid = True
569
+ widget.plot_item.plot([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
570
+
571
+ sys.exit(app.exec_())
@@ -0,0 +1,95 @@
1
+ import os
2
+
3
+ from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout, QWidget
4
+
5
+ from bec_widgets.qt_utils.error_popups import SafeSlot
6
+ from bec_widgets.qt_utils.settings_dialog import SettingWidget
7
+ from bec_widgets.utils import UILoader
8
+ from bec_widgets.utils.widget_io import WidgetIO
9
+
10
+
11
+ class AxisSettings(SettingWidget):
12
+ def __init__(self, parent=None, target_widget=None, *args, **kwargs):
13
+ super().__init__(parent=parent, *args, **kwargs)
14
+
15
+ # This is a settings widget that depends on the target widget
16
+ # and should mirror what is in the target widget.
17
+ # Saving settings for this widget could result in recursively setting the target widget.
18
+ self.setProperty("skip_settings", True)
19
+ self.setObjectName("AxisSettings")
20
+ current_path = os.path.dirname(__file__)
21
+ form = UILoader().load_ui(os.path.join(current_path, "axis_settings_vertical.ui"), self)
22
+
23
+ self.target_widget = target_widget
24
+
25
+ # # Scroll area
26
+ self.scroll_area = QScrollArea(self)
27
+ self.scroll_area.setWidgetResizable(True)
28
+ self.scroll_area.setFrameShape(QFrame.NoFrame)
29
+ self.scroll_area.setWidget(form)
30
+
31
+ self.layout = QVBoxLayout(self)
32
+ self.layout.setContentsMargins(0, 0, 0, 0)
33
+ self.layout.addWidget(self.scroll_area)
34
+ # self.layout.addWidget(self.ui)
35
+ self.ui = form
36
+
37
+ self.connect_all_signals()
38
+ if self.target_widget is not None:
39
+ self.target_widget.property_changed.connect(self.update_property)
40
+
41
+ def connect_all_signals(self):
42
+ for widget in [
43
+ self.ui.title,
44
+ self.ui.inner_axes,
45
+ self.ui.outer_axes,
46
+ self.ui.x_label,
47
+ self.ui.x_min,
48
+ self.ui.x_max,
49
+ self.ui.x_log,
50
+ self.ui.x_grid,
51
+ self.ui.y_label,
52
+ self.ui.y_min,
53
+ self.ui.y_max,
54
+ self.ui.y_log,
55
+ self.ui.y_grid,
56
+ ]:
57
+ WidgetIO.connect_widget_change_signal(widget, self.set_property)
58
+
59
+ @SafeSlot()
60
+ def set_property(self, widget: QWidget, value):
61
+ """
62
+ Set property of the target widget based on the widget that emitted the signal.
63
+ The name of the property has to be the same as the objectName of the widget
64
+ and compatible with WidgetIO.
65
+
66
+ Args:
67
+ widget(QWidget): The widget that emitted the signal.
68
+ value(): The value to set the property to.
69
+ """
70
+
71
+ try: # to avoid crashing when the widget is not found in Designer
72
+ property_name = widget.objectName()
73
+ setattr(self.target_widget, property_name, value)
74
+ except RuntimeError:
75
+ return
76
+
77
+ @SafeSlot()
78
+ def update_property(self, property_name: str, value):
79
+ """
80
+ Update the value of the widget based on the property name and value.
81
+ The name of the property has to be the same as the objectName of the widget
82
+ and compatible with WidgetIO.
83
+
84
+ Args:
85
+ property_name(str): The name of the property to update.
86
+ value: The value to set the property to.
87
+ """
88
+ try: # to avoid crashing when the widget is not found in Designer
89
+ widget_to_set = self.ui.findChild(QWidget, property_name)
90
+ except RuntimeError:
91
+ return
92
+ # Block signals to avoid triggering set_property again
93
+ was_blocked = widget_to_set.blockSignals(True)
94
+ WidgetIO.set_value(widget_to_set, value)
95
+ widget_to_set.blockSignals(was_blocked)