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.
@@ -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, SeparatorAction
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.init_toolbar()
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.add_side_menus()
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 init_toolbar(self):
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.toolbar.add_action("separator_1", SeparatorAction(), target_widget=self)
137
- self.toolbar.add_action(
151
+ self.performance_bundle.add_action(
138
152
  "fps_monitor",
139
- MaterialIconAction(icon_name="speed", tooltip="Show FPS Monitor", checkable=True),
140
- target_widget=self,
153
+ MaterialIconAction(
154
+ icon_name="speed", tooltip="Show FPS Monitor", checkable=True, parent=self
155
+ ),
141
156
  )
142
- self.toolbar.addWidget(DarkModeButton(toolbar=True))
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
- axis_setting = AxisSettings(target_widget=self)
152
- self.side_panel.add_menu(
153
- action_id="axis",
154
- icon_name="settings",
155
- tooltip="Show Axis Settings",
156
- widget=axis_setting,
157
- title="Axis Settings",
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
- self.toolbar.setVisible(value)
171
-
172
- @SafeProperty(bool, doc="Show Side Panel")
173
- def enable_side_panel(self) -> bool:
174
- return self.side_panel.isVisible()
175
-
176
- @enable_side_panel.setter
177
- def enable_side_panel(self, value: bool):
178
- self.side_panel.setVisible(value)
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
- widget = PlotBase()
601
- widget.show()
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
- form = UILoader().load_ui(os.path.join(current_path, "axis_settings_vertical.ui"), self)
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.connect_all_signals()
38
- if self.target_widget is not None:
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)