bec-widgets 1.17.2__py3-none-any.whl → 1.18.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.
- .gitlab-ci.yml +5 -6
- CHANGELOG.md +24 -0
- PKG-INFO +1 -4
- README.md +15 -19
- bec_widgets/cli/generate_cli.py +15 -6
- bec_widgets/examples/jupyter_console/jupyter_console_window.py +12 -0
- bec_widgets/qt_utils/round_frame.py +6 -4
- bec_widgets/qt_utils/side_panel.py +60 -79
- bec_widgets/widgets/plots_next_gen/plot_base.py +571 -0
- bec_widgets/widgets/plots_next_gen/setting_menus/__init__.py +0 -0
- bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings.py +95 -0
- bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings_horizontal.ui +256 -0
- bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings_vertical.ui +240 -0
- bec_widgets/widgets/plots_next_gen/toolbar_bundles/__init__.py +0 -0
- bec_widgets/widgets/plots_next_gen/toolbar_bundles/mouse_interactions.py +88 -0
- bec_widgets/widgets/plots_next_gen/toolbar_bundles/plot_export.py +63 -0
- bec_widgets/widgets/plots_next_gen/toolbar_bundles/save_state.py +48 -0
- {bec_widgets-1.17.2.dist-info → bec_widgets-1.18.0.dist-info}/METADATA +1 -4
- {bec_widgets-1.17.2.dist-info → bec_widgets-1.18.0.dist-info}/RECORD +23 -14
- pyproject.toml +1 -2
- {bec_widgets-1.17.2.dist-info → bec_widgets-1.18.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.17.2.dist-info → bec_widgets-1.18.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.17.2.dist-info → bec_widgets-1.18.0.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_())
|
File without changes
|
@@ -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)
|