bec-widgets 1.23.1__py3-none-any.whl → 1.24.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.
- CHANGELOG.md +49 -0
- PKG-INFO +1 -1
- bec_widgets/qt_utils/expandable_frame.py +72 -0
- bec_widgets/qt_utils/settings_dialog.py +7 -6
- bec_widgets/qt_utils/toolbar.py +6 -3
- bec_widgets/widgets/control/scan_control/scan_control.py +76 -18
- bec_widgets/widgets/editors/scan_metadata/additional_metadata_table.py +7 -4
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +46 -9
- bec_widgets/widgets/plots_next_gen/plot_base.py +170 -33
- bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings.py +59 -4
- bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings_horizontal.ui +112 -156
- bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings_vertical.ui +17 -52
- bec_widgets/widgets/plots_next_gen/toolbar_bundles/mouse_interactions.py +31 -18
- bec_widgets/widgets/plots_next_gen/toolbar_bundles/plot_export.py +10 -3
- {bec_widgets-1.23.1.dist-info → bec_widgets-1.24.1.dist-info}/METADATA +1 -1
- {bec_widgets-1.23.1.dist-info → bec_widgets-1.24.1.dist-info}/RECORD +20 -19
- pyproject.toml +1 -1
- {bec_widgets-1.23.1.dist-info → bec_widgets-1.24.1.dist-info}/WHEEL +0 -0
- {bec_widgets-1.23.1.dist-info → bec_widgets-1.24.1.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.23.1.dist-info → bec_widgets-1.24.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,14 +1,18 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
from enum import Enum
|
4
|
+
|
5
|
+
import numpy as np
|
3
6
|
import pyqtgraph as pg
|
4
7
|
from bec_lib import bec_logger
|
5
8
|
from qtpy.QtCore import QPoint, QPointF, Qt, Signal
|
6
|
-
from qtpy.QtWidgets import QLabel, QVBoxLayout, QWidget
|
9
|
+
from qtpy.QtWidgets import QHBoxLayout, QLabel, QMainWindow, QVBoxLayout, QWidget
|
7
10
|
|
8
11
|
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
|
9
12
|
from bec_widgets.qt_utils.round_frame import RoundedFrame
|
13
|
+
from bec_widgets.qt_utils.settings_dialog import SettingsDialog
|
10
14
|
from bec_widgets.qt_utils.side_panel import SidePanel
|
11
|
-
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ModularToolBar,
|
15
|
+
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ModularToolBar, ToolbarBundle
|
12
16
|
from bec_widgets.utils import ConnectionConfig, Crosshair, EntryValidator
|
13
17
|
from bec_widgets.utils.bec_widget import BECWidget
|
14
18
|
from bec_widgets.utils.fps_counter import FPSCounter
|
@@ -20,7 +24,6 @@ from bec_widgets.widgets.plots_next_gen.toolbar_bundles.mouse_interactions impor
|
|
20
24
|
)
|
21
25
|
from bec_widgets.widgets.plots_next_gen.toolbar_bundles.plot_export import PlotExportBundle
|
22
26
|
from bec_widgets.widgets.plots_next_gen.toolbar_bundles.roi_bundle import ROIBundle
|
23
|
-
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
24
27
|
|
25
28
|
logger = bec_logger.logger
|
26
29
|
|
@@ -42,6 +45,12 @@ class BECViewBox(pg.ViewBox):
|
|
42
45
|
self.update()
|
43
46
|
|
44
47
|
|
48
|
+
class UIMode(Enum):
|
49
|
+
NONE = 0
|
50
|
+
POPUP = 1
|
51
|
+
SIDE = 2
|
52
|
+
|
53
|
+
|
45
54
|
class PlotBase(BECWidget, QWidget):
|
46
55
|
PLUGIN = False
|
47
56
|
RPC = False
|
@@ -59,6 +68,7 @@ class PlotBase(BECWidget, QWidget):
|
|
59
68
|
config: ConnectionConfig | None = None,
|
60
69
|
client=None,
|
61
70
|
gui_id: str | None = None,
|
71
|
+
popups: bool = False,
|
62
72
|
) -> None:
|
63
73
|
if config is None:
|
64
74
|
config = ConnectionConfig(widget_class=self.__class__.__name__)
|
@@ -74,6 +84,8 @@ class PlotBase(BECWidget, QWidget):
|
|
74
84
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
75
85
|
self.layout.setSpacing(0)
|
76
86
|
self.layout_manager = LayoutManagerWidget(parent=self)
|
87
|
+
self.layout_manager.layout.setContentsMargins(0, 0, 0, 0)
|
88
|
+
self.layout_manager.layout.setSpacing(0)
|
77
89
|
|
78
90
|
# Property Manager
|
79
91
|
self.state_manager = WidgetStateManager(self)
|
@@ -82,12 +94,14 @@ class PlotBase(BECWidget, QWidget):
|
|
82
94
|
self.entry_validator = EntryValidator(self.dev)
|
83
95
|
|
84
96
|
# Base widgets elements
|
97
|
+
self._ui_mode = UIMode.POPUP if popups else UIMode.SIDE
|
98
|
+
self.axis_settings_dialog = None
|
85
99
|
self.plot_widget = pg.GraphicsLayoutWidget(parent=self)
|
86
100
|
self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
|
87
101
|
self.plot_widget.addItem(self.plot_item)
|
88
102
|
self.side_panel = SidePanel(self, orientation="left", panel_max_width=280)
|
89
103
|
self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
|
90
|
-
self.
|
104
|
+
self._init_toolbar()
|
91
105
|
|
92
106
|
# PlotItem Addons
|
93
107
|
self.plot_item.addLegend()
|
@@ -115,13 +129,14 @@ class PlotBase(BECWidget, QWidget):
|
|
115
129
|
self.layout_manager.add_widget_relative(self.side_panel, self.round_plot_widget, "left")
|
116
130
|
self.layout_manager.add_widget_relative(self.toolbar, self.fps_label, "top")
|
117
131
|
|
118
|
-
self.
|
132
|
+
self.ui_mode = self._ui_mode # to initiate the first time
|
119
133
|
|
120
134
|
# PlotItem ViewBox Signals
|
121
135
|
self.plot_item.vb.sigStateChanged.connect(self.viewbox_state_changed)
|
122
136
|
|
123
|
-
def
|
124
|
-
|
137
|
+
def _init_toolbar(self):
|
138
|
+
self.popup_bundle = None
|
139
|
+
self.performance_bundle = ToolbarBundle("performance")
|
125
140
|
self.plot_export_bundle = PlotExportBundle("plot_export", target_widget=self)
|
126
141
|
self.mouse_bundle = MouseInteractionToolbarBundle("mouse_interaction", target_widget=self)
|
127
142
|
# self.state_export_bundle = SaveStateBundle("state_export", target_widget=self) #TODO ATM disabled, cannot be used in DockArea, which is exposed to the user
|
@@ -133,33 +148,130 @@ class PlotBase(BECWidget, QWidget):
|
|
133
148
|
self.toolbar.add_bundle(self.mouse_bundle, target_widget=self)
|
134
149
|
self.toolbar.add_bundle(self.roi_bundle, target_widget=self)
|
135
150
|
|
136
|
-
self.
|
137
|
-
self.toolbar.add_action(
|
151
|
+
self.performance_bundle.add_action(
|
138
152
|
"fps_monitor",
|
139
|
-
MaterialIconAction(
|
140
|
-
|
153
|
+
MaterialIconAction(
|
154
|
+
icon_name="speed", tooltip="Show FPS Monitor", checkable=True, parent=self
|
155
|
+
),
|
141
156
|
)
|
142
|
-
self.toolbar.
|
157
|
+
self.toolbar.add_bundle(self.performance_bundle, target_widget=self)
|
143
158
|
|
144
159
|
self.toolbar.widgets["fps_monitor"].action.toggled.connect(
|
145
160
|
lambda checked: setattr(self, "enable_fps_monitor", checked)
|
146
161
|
)
|
147
162
|
|
163
|
+
# hide some options by default
|
164
|
+
self.toolbar.toggle_action_visibility("fps_monitor", False)
|
165
|
+
|
148
166
|
def add_side_menus(self):
|
149
167
|
"""Adds multiple menus to the side panel."""
|
150
168
|
# Setting Axis Widget
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
169
|
+
try:
|
170
|
+
axis_setting = AxisSettings(target_widget=self)
|
171
|
+
self.side_panel.add_menu(
|
172
|
+
action_id="axis",
|
173
|
+
icon_name="settings",
|
174
|
+
tooltip="Show Axis Settings",
|
175
|
+
widget=axis_setting,
|
176
|
+
title="Axis Settings",
|
177
|
+
)
|
178
|
+
except ValueError:
|
179
|
+
return
|
180
|
+
|
181
|
+
def add_popups(self):
|
182
|
+
"""
|
183
|
+
Add popups to the toolbar.
|
184
|
+
"""
|
185
|
+
self.popup_bundle = ToolbarBundle("popup_bundle")
|
186
|
+
settings = MaterialIconAction(
|
187
|
+
icon_name="settings", tooltip="Show Axis Settings", checkable=True, parent=self
|
158
188
|
)
|
189
|
+
self.popup_bundle.add_action("axis", settings)
|
190
|
+
self.toolbar.add_bundle(self.popup_bundle, target_widget=self)
|
191
|
+
self.toolbar.widgets["axis"].action.triggered.connect(self.show_axis_settings_popup)
|
192
|
+
|
193
|
+
def show_axis_settings_popup(self):
|
194
|
+
"""
|
195
|
+
Show the axis settings dialog.
|
196
|
+
"""
|
197
|
+
settings_action = self.toolbar.widgets["axis"].action
|
198
|
+
if self.axis_settings_dialog is None or not self.axis_settings_dialog.isVisible():
|
199
|
+
axis_setting = AxisSettings(target_widget=self, popup=True)
|
200
|
+
self.axis_settings_dialog = SettingsDialog(
|
201
|
+
self, settings_widget=axis_setting, window_title="Axis Settings", modal=False
|
202
|
+
)
|
203
|
+
# When the dialog is closed, update the toolbar icon and clear the reference
|
204
|
+
self.axis_settings_dialog.finished.connect(self._axis_settings_closed)
|
205
|
+
self.axis_settings_dialog.show()
|
206
|
+
settings_action.setChecked(True)
|
207
|
+
else:
|
208
|
+
# If already open, bring it to the front
|
209
|
+
self.axis_settings_dialog.raise_()
|
210
|
+
self.axis_settings_dialog.activateWindow()
|
211
|
+
settings_action.setChecked(True) # keep it toggled
|
212
|
+
|
213
|
+
def _axis_settings_closed(self):
|
214
|
+
"""
|
215
|
+
Slot for when the axis settings dialog is closed.
|
216
|
+
"""
|
217
|
+
self.axis_settings_dialog = None
|
218
|
+
self.toolbar.widgets["axis"].action.setChecked(False)
|
159
219
|
|
160
220
|
################################################################################
|
161
221
|
# Toggle UI Elements
|
162
222
|
################################################################################
|
223
|
+
@property
|
224
|
+
def ui_mode(self) -> UIMode:
|
225
|
+
return self._ui_mode
|
226
|
+
|
227
|
+
@ui_mode.setter
|
228
|
+
def ui_mode(self, mode: UIMode):
|
229
|
+
if not isinstance(mode, UIMode):
|
230
|
+
raise ValueError("ui_mode must be an instance of UIMode")
|
231
|
+
self._ui_mode = mode
|
232
|
+
|
233
|
+
# First, clear both UI elements:
|
234
|
+
if self.popup_bundle is not None:
|
235
|
+
for action_id in self.toolbar.bundles["popup_bundle"]:
|
236
|
+
self.toolbar.widgets[action_id].action.setVisible(False)
|
237
|
+
if self.axis_settings_dialog is not None and self.axis_settings_dialog.isVisible():
|
238
|
+
self.axis_settings_dialog.close()
|
239
|
+
self.side_panel.hide()
|
240
|
+
|
241
|
+
# Now, apply the new mode:
|
242
|
+
if mode == UIMode.POPUP:
|
243
|
+
if self.popup_bundle is None:
|
244
|
+
self.add_popups()
|
245
|
+
else:
|
246
|
+
for action_id in self.toolbar.bundles["popup_bundle"]:
|
247
|
+
self.toolbar.widgets[action_id].action.setVisible(True)
|
248
|
+
elif mode == UIMode.SIDE:
|
249
|
+
self.add_side_menus()
|
250
|
+
self.side_panel.show()
|
251
|
+
|
252
|
+
@SafeProperty(bool, doc="Enable popups setting dialogs for the plot widget.")
|
253
|
+
def enable_popups(self):
|
254
|
+
return self.ui_mode == UIMode.POPUP
|
255
|
+
|
256
|
+
@enable_popups.setter
|
257
|
+
def enable_popups(self, value: bool):
|
258
|
+
if value:
|
259
|
+
self.ui_mode = UIMode.POPUP
|
260
|
+
else:
|
261
|
+
if self.ui_mode == UIMode.POPUP:
|
262
|
+
self.ui_mode = UIMode.NONE
|
263
|
+
|
264
|
+
@SafeProperty(bool, doc="Show Side Panel")
|
265
|
+
def enable_side_panel(self) -> bool:
|
266
|
+
return self.ui_mode == UIMode.SIDE
|
267
|
+
|
268
|
+
@enable_side_panel.setter
|
269
|
+
def enable_side_panel(self, value: bool):
|
270
|
+
if value:
|
271
|
+
self.ui_mode = UIMode.SIDE
|
272
|
+
else:
|
273
|
+
if self.ui_mode == UIMode.SIDE:
|
274
|
+
self.ui_mode = UIMode.NONE
|
163
275
|
|
164
276
|
@SafeProperty(bool, doc="Show Toolbar")
|
165
277
|
def enable_toolbar(self) -> bool:
|
@@ -167,15 +279,22 @@ class PlotBase(BECWidget, QWidget):
|
|
167
279
|
|
168
280
|
@enable_toolbar.setter
|
169
281
|
def enable_toolbar(self, value: bool):
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
282
|
+
if value:
|
283
|
+
# Disable popup mode
|
284
|
+
if self._popups:
|
285
|
+
# Directly update the internal flag to avoid recursion
|
286
|
+
self._popups = False
|
287
|
+
# Hide the popup bundle if it exists and close any open dialogs
|
288
|
+
if self.popup_bundle is not None:
|
289
|
+
for action in self.toolbar.bundles["popup_bundle"].actions:
|
290
|
+
action.setVisible(False)
|
291
|
+
if self.axis_settings_dialog is not None and self.axis_settings_dialog.isVisible():
|
292
|
+
self.axis_settings_dialog.close()
|
293
|
+
self.side_panel.show()
|
294
|
+
# Add side menus if not already added
|
295
|
+
self.add_side_menus()
|
296
|
+
else:
|
297
|
+
self.side_panel.hide()
|
179
298
|
|
180
299
|
@SafeProperty(bool, doc="Enable the FPS monitor.")
|
181
300
|
def enable_fps_monitor(self) -> bool:
|
@@ -591,16 +710,34 @@ class PlotBase(BECWidget, QWidget):
|
|
591
710
|
item.ctrlMenu.deleteLater()
|
592
711
|
|
593
712
|
|
713
|
+
class DemoPlotBase(QMainWindow): # pragma: no cover:
|
714
|
+
def __init__(self):
|
715
|
+
super().__init__()
|
716
|
+
self.main_widget = QWidget()
|
717
|
+
self.setCentralWidget(self.main_widget)
|
718
|
+
self.main_widget.layout = QHBoxLayout(self.main_widget)
|
719
|
+
|
720
|
+
self.plot_popup = PlotBase(popups=True)
|
721
|
+
self.plot_popup.title = "PlotBase with popups"
|
722
|
+
self.plot_side_panels = PlotBase(popups=False)
|
723
|
+
self.plot_side_panels.title = "PlotBase with side panels"
|
724
|
+
|
725
|
+
self.plot_popup.plot_item.plot(np.random.rand(100), pen=(255, 0, 0))
|
726
|
+
self.plot_side_panels.plot_item.plot(np.random.rand(100), pen=(0, 255, 0))
|
727
|
+
|
728
|
+
self.main_widget.layout.addWidget(self.plot_side_panels)
|
729
|
+
self.main_widget.layout.addWidget(self.plot_popup)
|
730
|
+
|
731
|
+
self.resize(1400, 600)
|
732
|
+
|
733
|
+
|
594
734
|
if __name__ == "__main__": # pragma: no cover:
|
595
735
|
import sys
|
596
736
|
|
597
737
|
from qtpy.QtWidgets import QApplication
|
598
738
|
|
599
739
|
app = QApplication(sys.argv)
|
600
|
-
|
601
|
-
|
602
|
-
# Just some example data and parameters to test
|
603
|
-
widget.y_grid = True
|
604
|
-
widget.plot_item.plot([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
|
740
|
+
window = DemoPlotBase()
|
741
|
+
window.show()
|
605
742
|
|
606
743
|
sys.exit(app.exec_())
|
@@ -9,7 +9,7 @@ from bec_widgets.utils.widget_io import WidgetIO
|
|
9
9
|
|
10
10
|
|
11
11
|
class AxisSettings(SettingWidget):
|
12
|
-
def __init__(self, parent=None, target_widget=None, *args, **kwargs):
|
12
|
+
def __init__(self, parent=None, target_widget=None, popup=False, *args, **kwargs):
|
13
13
|
super().__init__(parent=parent, *args, **kwargs)
|
14
14
|
|
15
15
|
# This is a settings widget that depends on the target widget
|
@@ -18,9 +18,15 @@ class AxisSettings(SettingWidget):
|
|
18
18
|
self.setProperty("skip_settings", True)
|
19
19
|
self.setObjectName("AxisSettings")
|
20
20
|
current_path = os.path.dirname(__file__)
|
21
|
-
|
21
|
+
if popup:
|
22
|
+
form = UILoader().load_ui(
|
23
|
+
os.path.join(current_path, "axis_settings_horizontal.ui"), self
|
24
|
+
)
|
25
|
+
else:
|
26
|
+
form = UILoader().load_ui(os.path.join(current_path, "axis_settings_vertical.ui"), self)
|
22
27
|
|
23
28
|
self.target_widget = target_widget
|
29
|
+
self.popup = popup
|
24
30
|
|
25
31
|
# # Scroll area
|
26
32
|
self.scroll_area = QScrollArea(self)
|
@@ -34,10 +40,13 @@ class AxisSettings(SettingWidget):
|
|
34
40
|
# self.layout.addWidget(self.ui)
|
35
41
|
self.ui = form
|
36
42
|
|
37
|
-
self.
|
38
|
-
|
43
|
+
if self.target_widget is not None and self.popup is False:
|
44
|
+
self.connect_all_signals()
|
39
45
|
self.target_widget.property_changed.connect(self.update_property)
|
40
46
|
|
47
|
+
if self.popup is True:
|
48
|
+
self.fetch_all_properties()
|
49
|
+
|
41
50
|
def connect_all_signals(self):
|
42
51
|
for widget in [
|
43
52
|
self.ui.title,
|
@@ -93,3 +102,49 @@ class AxisSettings(SettingWidget):
|
|
93
102
|
was_blocked = widget_to_set.blockSignals(True)
|
94
103
|
WidgetIO.set_value(widget_to_set, value)
|
95
104
|
widget_to_set.blockSignals(was_blocked)
|
105
|
+
|
106
|
+
def fetch_all_properties(self):
|
107
|
+
"""
|
108
|
+
Fetch all properties from the target widget and update the settings widget.
|
109
|
+
"""
|
110
|
+
for widget in [
|
111
|
+
self.ui.title,
|
112
|
+
self.ui.inner_axes,
|
113
|
+
self.ui.outer_axes,
|
114
|
+
self.ui.x_label,
|
115
|
+
self.ui.x_min,
|
116
|
+
self.ui.x_max,
|
117
|
+
self.ui.x_log,
|
118
|
+
self.ui.x_grid,
|
119
|
+
self.ui.y_label,
|
120
|
+
self.ui.y_min,
|
121
|
+
self.ui.y_max,
|
122
|
+
self.ui.y_log,
|
123
|
+
self.ui.y_grid,
|
124
|
+
]:
|
125
|
+
property_name = widget.objectName()
|
126
|
+
value = getattr(self.target_widget, property_name)
|
127
|
+
WidgetIO.set_value(widget, value)
|
128
|
+
|
129
|
+
def accept_changes(self):
|
130
|
+
"""
|
131
|
+
Apply all properties from the settings widget to the target widget.
|
132
|
+
"""
|
133
|
+
for widget in [
|
134
|
+
self.ui.title,
|
135
|
+
self.ui.inner_axes,
|
136
|
+
self.ui.outer_axes,
|
137
|
+
self.ui.x_label,
|
138
|
+
self.ui.x_min,
|
139
|
+
self.ui.x_max,
|
140
|
+
self.ui.x_log,
|
141
|
+
self.ui.x_grid,
|
142
|
+
self.ui.y_label,
|
143
|
+
self.ui.y_min,
|
144
|
+
self.ui.y_max,
|
145
|
+
self.ui.y_log,
|
146
|
+
self.ui.y_grid,
|
147
|
+
]:
|
148
|
+
property_name = widget.objectName()
|
149
|
+
value = WidgetIO.get_value(widget)
|
150
|
+
setattr(self.target_widget, property_name, value)
|