bec-widgets 1.21.3__py3-none-any.whl → 1.22.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.
CHANGELOG.md CHANGED
@@ -1,6 +1,54 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v1.22.0 (2025-02-19)
5
+
6
+ ### Bug Fixes
7
+
8
+ - **modular_toolbar**: Add action to an already existing bundle
9
+ ([`4c4f159`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4c4f1592c29974bb095c3c8325e93a1383efa289))
10
+
11
+ - **toolbar**: Qmenu Icons are visible
12
+ ([`c2c0221`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c2c022154bddc15d81eb55aad912d8fe1e34c698))
13
+
14
+ - **toolbar**: Update_separators logic updated, there cannot be two separators next to each other
15
+ ([`facb8c3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/facb8c30ffa3b12a97c7c68f8594b0354372ca17))
16
+
17
+ - **toolbar**: Widget actions are more compact
18
+ ([`ef36a71`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ef36a7124d54319c2cd592433c95e4f7513e982e))
19
+
20
+ ### Features
21
+
22
+ - **toolbar**: Switchabletoolbarbutton
23
+ ([`333570b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/333570ba2fe67cb51fdbab17718003dfdb7f7b55))
24
+
25
+ ### Refactoring
26
+
27
+ - **toolbar**: Added dark mode button for testing appearance for the toolbar example
28
+ ([`6b08f7c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6b08f7cfb2115609a6dc6f681631ecfae23fa899))
29
+
30
+ ### Testing
31
+
32
+ - **toolbar**: Blocking tests fixed
33
+ ([`6ae33a2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6ae33a23a62eafb7c820e1fde9d6d91ec1796e55))
34
+
35
+
36
+ ## v1.21.4 (2025-02-19)
37
+
38
+ ### Bug Fixes
39
+
40
+ - **colors**: Pyqtgraph styling updated on the app level
41
+ ([`ae18279`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ae182796855719437bdf911c2e969e3f438d6982))
42
+
43
+ - **plot_base**: Mouse interactions default state fetch to toolbar
44
+ ([`97c0ed5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/97c0ed53df21053fef9811c3dea3b79020137030))
45
+
46
+ ### Refactoring
47
+
48
+ - **plot_base**: Change the PlotWidget to GraphicalLayoutWidget
49
+ ([`ff8e282`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ff8e282034f0970b69cf0447fc5f88b4f30bf470))
50
+
51
+
4
52
  ## v1.21.3 (2025-02-19)
5
53
 
6
54
  ### Bug Fixes
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 1.21.3
3
+ Version: 1.22.0
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
@@ -1,6 +1,6 @@
1
1
  import pyqtgraph as pg
2
2
  from qtpy.QtCore import Property
3
- from qtpy.QtWidgets import QApplication, QFrame, QVBoxLayout, QWidget
3
+ from qtpy.QtWidgets import QApplication, QFrame, QHBoxLayout, QVBoxLayout, QWidget
4
4
 
5
5
  from bec_widgets.utils.bec_widget import BECWidget
6
6
  from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
@@ -31,22 +31,20 @@ class RoundedFrame(BECWidget, QFrame):
31
31
  # Apply rounded frame styling
32
32
  self.setProperty("skip_settings", True)
33
33
  self.setObjectName("roundedFrame")
34
- self.update_style()
35
34
 
36
35
  # Create a layout for the frame
37
- layout = QVBoxLayout(self)
38
- layout.setContentsMargins(5, 5, 5, 5) # Set 5px margin
36
+ self.layout = QHBoxLayout(self)
37
+ self.layout.setContentsMargins(5, 5, 5, 5) # Set 5px margin
39
38
 
40
39
  # Add the content widget to the layout
41
40
  if content_widget:
42
- layout.addWidget(content_widget)
41
+ self.layout.addWidget(content_widget)
43
42
 
44
43
  # Store reference to the content widget
45
44
  self.content_widget = content_widget
46
45
 
47
- # Automatically apply initial styles to the PlotWidget if applicable
48
- if isinstance(content_widget, pg.PlotWidget):
49
- self.apply_plot_widget_style()
46
+ # Automatically apply initial styles to the GraphicalLayoutWidget if applicable
47
+ self.apply_plot_widget_style()
50
48
 
51
49
  self._connect_to_theme_change()
52
50
 
@@ -65,10 +63,6 @@ class RoundedFrame(BECWidget, QFrame):
65
63
 
66
64
  self.update_style()
67
65
 
68
- # Update PlotWidget's background color and axis styles if applicable
69
- if isinstance(self.content_widget, pg.PlotWidget):
70
- self.apply_plot_widget_style()
71
-
72
66
  @Property(int)
73
67
  def radius(self):
74
68
  """Radius of the rounded corners."""
@@ -92,6 +86,7 @@ class RoundedFrame(BECWidget, QFrame):
92
86
  }}
93
87
  """
94
88
  )
89
+ self.apply_plot_widget_style()
95
90
 
96
91
  def apply_plot_widget_style(self, border: str = "none"):
97
92
  """
@@ -100,35 +95,16 @@ class RoundedFrame(BECWidget, QFrame):
100
95
  Args:
101
96
  border (str): Border style (e.g., 'none', '1px solid red').
102
97
  """
103
- if isinstance(self.content_widget, pg.PlotWidget):
104
- # Sync PlotWidget's background color with the RoundedFrame's background color
105
- self.content_widget.setBackground(self.background_color)
106
-
107
- # Calculate contrast-optimized axis and label colors
108
- if self.background_color == "#e9ecef": # Light mode
109
- label_color = "#000000"
110
- axis_color = "#666666"
111
- else: # Dark mode
112
- label_color = "#FFFFFF"
113
- axis_color = "#CCCCCC"
114
-
115
- # Apply axis label and tick colors
116
- plot_item = self.content_widget.getPlotItem()
117
- for axis in ["left", "right", "top", "bottom"]:
118
- plot_item.getAxis(axis).setPen(pg.mkPen(color=axis_color))
119
- plot_item.getAxis(axis).setTextPen(pg.mkPen(color=label_color))
120
-
121
- # Change title color
122
- plot_item.titleLabel.setText(plot_item.titleLabel.text, color=label_color)
123
-
98
+ if isinstance(self.content_widget, pg.GraphicsLayoutWidget):
124
99
  # Apply border style via stylesheet
125
100
  self.content_widget.setStyleSheet(
126
101
  f"""
127
- PlotWidget {{
102
+ GraphicsLayoutWidget {{
128
103
  border: {border}; /* Explicitly set the border */
129
104
  }}
130
105
  """
131
106
  )
107
+ self.content_widget.setBackground(self.background_color)
132
108
 
133
109
 
134
110
  class ExampleApp(QWidget): # pragma: no cover
@@ -142,26 +118,27 @@ class ExampleApp(QWidget): # pragma: no cover
142
118
  dark_button = DarkModeButton()
143
119
 
144
120
  # Create PlotWidgets
145
- plot1 = pg.PlotWidget()
146
- plot1.plot([1, 3, 2, 4, 6, 5], pen="r")
121
+ plot1 = pg.GraphicsLayoutWidget()
122
+ plot_item_1 = pg.PlotItem()
123
+ plot_item_1.plot([1, 3, 2, 4, 6, 5], pen="r")
124
+ plot1.plot_item = plot_item_1
147
125
 
148
- plot2 = pg.PlotWidget()
149
- plot2.plot([1, 2, 4, 8, 16, 32], pen="r")
126
+ plot2 = pg.GraphicsLayoutWidget()
127
+ plot_item_2 = pg.PlotItem()
128
+ plot_item_2.plot([1, 2, 4, 8, 16, 32], pen="r")
129
+ plot2.plot_item = plot_item_2
150
130
 
151
131
  # Wrap PlotWidgets in RoundedFrame
152
132
  rounded_plot1 = RoundedFrame(content_widget=plot1, theme_update=True)
153
133
  rounded_plot2 = RoundedFrame(content_widget=plot2, theme_update=True)
154
- round = RoundedFrame()
155
134
 
156
135
  # Add to layout
157
136
  layout.addWidget(dark_button)
158
137
  layout.addWidget(rounded_plot1)
159
138
  layout.addWidget(rounded_plot2)
160
- layout.addWidget(round)
161
139
 
162
140
  self.setLayout(layout)
163
141
 
164
- # Simulate theme change after 2 seconds
165
142
  from qtpy.QtCore import QTimer
166
143
 
167
144
  def change_theme():
@@ -8,7 +8,7 @@ from collections import defaultdict
8
8
  from typing import Dict, List, Literal, Tuple
9
9
 
10
10
  from bec_qthemes._icon.material_icons import material_icon
11
- from qtpy.QtCore import QSize, Qt
11
+ from qtpy.QtCore import QSize, Qt, QTimer
12
12
  from qtpy.QtGui import QAction, QColor, QIcon
13
13
  from qtpy.QtWidgets import (
14
14
  QApplication,
@@ -18,15 +18,54 @@ from qtpy.QtWidgets import (
18
18
  QMainWindow,
19
19
  QMenu,
20
20
  QSizePolicy,
21
+ QStyle,
21
22
  QToolBar,
22
23
  QToolButton,
24
+ QVBoxLayout,
23
25
  QWidget,
24
26
  )
25
27
 
26
28
  import bec_widgets
29
+ from bec_widgets.utils.colors import set_theme
30
+ from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
27
31
 
28
32
  MODULE_PATH = os.path.dirname(bec_widgets.__file__)
29
33
 
34
+ # Ensure that icons are shown in menus (especially on macOS)
35
+ QApplication.setAttribute(Qt.AA_DontShowIconsInMenus, False)
36
+
37
+
38
+ class LongPressToolButton(QToolButton):
39
+ def __init__(self, *args, long_press_threshold=500, **kwargs):
40
+ super().__init__(*args, **kwargs)
41
+ self.long_press_threshold = long_press_threshold
42
+ self._long_press_timer = QTimer(self)
43
+ self._long_press_timer.setSingleShot(True)
44
+ self._long_press_timer.timeout.connect(self.handleLongPress)
45
+ self._pressed = False
46
+ self._longPressed = False
47
+
48
+ def mousePressEvent(self, event):
49
+ self._pressed = True
50
+ self._longPressed = False
51
+ self._long_press_timer.start(self.long_press_threshold)
52
+ super().mousePressEvent(event)
53
+
54
+ def mouseReleaseEvent(self, event):
55
+ self._pressed = False
56
+ if self._longPressed:
57
+ self._longPressed = False
58
+ self._long_press_timer.stop()
59
+ event.accept() # Prevent normal click action after a long press
60
+ return
61
+ self._long_press_timer.stop()
62
+ super().mouseReleaseEvent(event)
63
+
64
+ def handleLongPress(self):
65
+ if self._pressed:
66
+ self._longPressed = True
67
+ self.showMenu()
68
+
30
69
 
31
70
  class ToolBarAction(ABC):
32
71
  """
@@ -84,6 +123,21 @@ class IconAction(ToolBarAction):
84
123
  toolbar.addAction(self.action)
85
124
 
86
125
 
126
+ class QtIconAction(ToolBarAction):
127
+ def __init__(self, standard_icon, tooltip=None, checkable=False, parent=None):
128
+ super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable)
129
+ self.standard_icon = standard_icon
130
+ self.icon = QApplication.style().standardIcon(standard_icon)
131
+ self.action = QAction(self.icon, self.tooltip, parent)
132
+ self.action.setCheckable(self.checkable)
133
+
134
+ def add_to_toolbar(self, toolbar, target):
135
+ toolbar.addAction(self.action)
136
+
137
+ def get_icon(self):
138
+ return self.icon
139
+
140
+
87
141
  class MaterialIconAction(ToolBarAction):
88
142
  """
89
143
  Action with a Material icon for the toolbar.
@@ -111,7 +165,7 @@ class MaterialIconAction(ToolBarAction):
111
165
  self.icon_name = icon_name
112
166
  self.filled = filled
113
167
  self.color = color
114
- # Generate the icon
168
+ # Generate the icon using the material_icon helper
115
169
  self.icon = material_icon(
116
170
  self.icon_name,
117
171
  size=(20, 20),
@@ -119,7 +173,6 @@ class MaterialIconAction(ToolBarAction):
119
173
  filled=self.filled,
120
174
  color=self.color,
121
175
  )
122
- # Immediately create an QAction with the given parent
123
176
  self.action = QAction(self.icon, self.tooltip, parent=parent)
124
177
  self.action.setCheckable(self.checkable)
125
178
 
@@ -152,7 +205,7 @@ class DeviceSelectionAction(ToolBarAction):
152
205
  device_combobox (DeviceComboBox): The combobox for selecting the device.
153
206
  """
154
207
 
155
- def __init__(self, label: str, device_combobox):
208
+ def __init__(self, label: str | None = None, device_combobox=None):
156
209
  super().__init__()
157
210
  self.label = label
158
211
  self.device_combobox = device_combobox
@@ -161,15 +214,99 @@ class DeviceSelectionAction(ToolBarAction):
161
214
  def add_to_toolbar(self, toolbar, target):
162
215
  widget = QWidget()
163
216
  layout = QHBoxLayout(widget)
164
- label = QLabel(f"{self.label}")
165
- layout.addWidget(label)
166
- layout.addWidget(self.device_combobox)
167
- toolbar.addWidget(widget)
217
+ layout.setContentsMargins(0, 0, 0, 0)
218
+ layout.setSpacing(0)
219
+ if self.label is not None:
220
+ label = QLabel(f"{self.label}")
221
+ layout.addWidget(label)
222
+ if self.device_combobox is not None:
223
+ layout.addWidget(self.device_combobox)
224
+ toolbar.addWidget(widget)
168
225
 
169
226
  def set_combobox_style(self, color: str):
170
227
  self.device_combobox.setStyleSheet(f"QComboBox {{ background-color: {color}; }}")
171
228
 
172
229
 
230
+ class SwitchableToolBarAction(ToolBarAction):
231
+ """
232
+ A split toolbar action that combines a main action and a drop-down menu for additional actions.
233
+
234
+ The main button displays the currently selected action's icon and tooltip. Clicking on the main button
235
+ triggers that action. Clicking on the drop-down arrow displays a menu with alternative actions. When an
236
+ alternative action is selected, it becomes the new default and its callback is immediately executed.
237
+
238
+ This design mimics the behavior seen in Adobe Photoshop or Affinity Designer toolbars.
239
+
240
+ Args:
241
+ actions (dict): A dictionary mapping a unique key to a ToolBarAction instance.
242
+ initial_action (str, optional): The key of the initial default action. If not provided, the first action is used.
243
+ tooltip (str, optional): An optional tooltip for the split action; if provided, it overrides the default action's tooltip.
244
+ checkable (bool, optional): Whether the action is checkable. Defaults to True.
245
+ parent (QWidget, optional): Parent widget for the underlying QAction.
246
+ """
247
+
248
+ def __init__(
249
+ self,
250
+ actions: Dict[str, ToolBarAction],
251
+ initial_action: str = None,
252
+ tooltip: str = None,
253
+ checkable: bool = True,
254
+ parent=None,
255
+ ):
256
+ super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable)
257
+ self.actions = actions
258
+ self.current_key = initial_action if initial_action is not None else next(iter(actions))
259
+ self.parent = parent
260
+ self.checkable = checkable
261
+ self.main_button = None
262
+ self.menu_actions: Dict[str, QAction] = {}
263
+
264
+ def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
265
+ """
266
+ Adds the split action to the toolbar.
267
+
268
+ Args:
269
+ toolbar (QToolBar): The toolbar to add the action to.
270
+ target (QWidget): The target widget for the action.
271
+ """
272
+ self.main_button = LongPressToolButton(toolbar)
273
+ self.main_button.setPopupMode(QToolButton.MenuButtonPopup)
274
+ self.main_button.setCheckable(self.checkable)
275
+ default_action = self.actions[self.current_key]
276
+ self.main_button.setIcon(default_action.get_icon())
277
+ self.main_button.setToolTip(default_action.tooltip)
278
+ self.main_button.clicked.connect(self._trigger_current_action)
279
+ menu = QMenu(self.main_button)
280
+ self.menu_actions = {}
281
+ for key, action_obj in self.actions.items():
282
+ menu_action = QAction(action_obj.get_icon(), action_obj.tooltip, self.main_button)
283
+ menu_action.setIconVisibleInMenu(True)
284
+ menu_action.setCheckable(self.checkable)
285
+ menu_action.setChecked(key == self.current_key)
286
+ menu_action.triggered.connect(lambda checked, k=key: self._set_default_action(k))
287
+ menu.addAction(menu_action)
288
+ self.menu_actions[key] = menu_action
289
+ self.main_button.setMenu(menu)
290
+ toolbar.addWidget(self.main_button)
291
+
292
+ def _trigger_current_action(self):
293
+ action_obj = self.actions[self.current_key]
294
+ action_obj.action.trigger()
295
+
296
+ def _set_default_action(self, key: str):
297
+ self.current_key = key
298
+ new_action = self.actions[self.current_key]
299
+ self.main_button.setIcon(new_action.get_icon())
300
+ self.main_button.setToolTip(new_action.tooltip)
301
+ # Update check state of menu items
302
+ for k, menu_act in self.menu_actions.items():
303
+ menu_act.setChecked(k == key)
304
+ new_action.action.trigger()
305
+
306
+ def get_icon(self) -> QIcon:
307
+ return self.actions[self.current_key].get_icon()
308
+
309
+
173
310
  class WidgetAction(ToolBarAction):
174
311
  """
175
312
  Action for adding any widget to the toolbar.
@@ -180,15 +317,23 @@ class WidgetAction(ToolBarAction):
180
317
  """
181
318
 
182
319
  def __init__(self, label: str | None = None, widget: QWidget = None, parent=None):
183
- super().__init__(parent)
320
+ super().__init__(icon_path=None, tooltip=label, checkable=False)
184
321
  self.label = label
185
322
  self.widget = widget
323
+ self.container = None
186
324
 
187
325
  def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
188
- container = QWidget()
189
- layout = QHBoxLayout(container)
326
+ """
327
+ Adds the widget to the toolbar.
328
+
329
+ Args:
330
+ toolbar (QToolBar): The toolbar to add the widget to.
331
+ target (QWidget): The target widget for the action.
332
+ """
333
+ self.container = QWidget()
334
+ layout = QHBoxLayout(self.container)
190
335
  layout.setContentsMargins(0, 0, 0, 0)
191
- layout.setSpacing(5)
336
+ layout.setSpacing(0)
192
337
 
193
338
  if self.label is not None:
194
339
  label_widget = QLabel(f"{self.label}")
@@ -209,19 +354,12 @@ class WidgetAction(ToolBarAction):
209
354
 
210
355
  layout.addWidget(self.widget)
211
356
 
212
- toolbar.addWidget(container)
357
+ toolbar.addWidget(self.container)
358
+ # Store the container as the action to allow toggling visibility.
359
+ self.action = self.container
213
360
 
214
361
  @staticmethod
215
362
  def calculate_minimum_width(combo_box: QComboBox) -> int:
216
- """
217
- Calculate the minimum width required to display the longest item in the combo box.
218
-
219
- Args:
220
- combo_box (QComboBox): The combo box to calculate the width for.
221
-
222
- Returns:
223
- int: The calculated minimum width in pixels.
224
- """
225
363
  font_metrics = combo_box.fontMetrics()
226
364
  max_width = max(font_metrics.width(combo_box.itemText(i)) for i in range(combo_box.count()))
227
365
  return max_width + 60
@@ -261,12 +399,15 @@ class ExpandableMenuAction(ToolBarAction):
261
399
  menu = QMenu(button)
262
400
  for action_id, action in self.actions.items():
263
401
  sub_action = QAction(action.tooltip, target)
264
- if hasattr(action, "icon_path"):
402
+ sub_action.setIconVisibleInMenu(True)
403
+ if action.icon_path:
265
404
  icon = QIcon()
266
405
  icon.addFile(action.icon_path, size=QSize(20, 20))
267
406
  sub_action.setIcon(icon)
268
- elif hasattr(action, "get_icon"):
269
- sub_action.setIcon(action.get_icon())
407
+ elif hasattr(action, "get_icon") and callable(action.get_icon):
408
+ sub_icon = action.get_icon()
409
+ if sub_icon and not sub_icon.isNull():
410
+ sub_action.setIcon(sub_icon)
270
411
  sub_action.setCheckable(action.checkable)
271
412
  menu.addAction(sub_action)
272
413
  self.widgets[action_id] = sub_action
@@ -289,7 +430,6 @@ class ToolbarBundle:
289
430
  self.bundle_id = bundle_id
290
431
  self._actions: dict[str, ToolBarAction] = {}
291
432
 
292
- # If you passed in a list of tuples, load them into the dictionary
293
433
  if actions is not None:
294
434
  for action_id, action in actions:
295
435
  self._actions[action_id] = action
@@ -331,7 +471,7 @@ class ModularToolBar(QToolBar):
331
471
  actions (dict, optional): A dictionary of action creators to populate the toolbar. Defaults to None.
332
472
  target_widget (QWidget, optional): The widget that the actions will target. Defaults to None.
333
473
  orientation (Literal["horizontal", "vertical"], optional): The initial orientation of the toolbar. Defaults to "horizontal".
334
- background_color (str, optional): The background color of the toolbar. Defaults to "rgba(0, 0, 0, 0)" - transparent background.
474
+ background_color (str, optional): The background color of the toolbar. Defaults to "rgba(0, 0, 0, 0)".
335
475
  """
336
476
 
337
477
  def __init__(
@@ -378,7 +518,7 @@ class ModularToolBar(QToolBar):
378
518
  Sets the background color and other appearance settings.
379
519
 
380
520
  Args:
381
- color(str): The background color of the toolbar.
521
+ color (str): The background color of the toolbar.
382
522
  """
383
523
  self.setIconSize(QSize(20, 20))
384
524
  self.setMovable(False)
@@ -402,100 +542,133 @@ class ModularToolBar(QToolBar):
402
542
 
403
543
  def update_material_icon_colors(self, new_color: str | tuple | QColor):
404
544
  """
405
- Updates the color of all MaterialIconAction icons in the toolbar.
545
+ Updates the color of all MaterialIconAction icons.
406
546
 
407
547
  Args:
408
- new_color (str | tuple | QColor): The new color for the icons.
548
+ new_color (str | tuple | QColor): The new color.
409
549
  """
410
550
  for action in self.widgets.values():
411
551
  if isinstance(action, MaterialIconAction):
412
552
  action.color = new_color
413
- # Refresh the icon
414
553
  updated_icon = action.get_icon()
415
554
  action.action.setIcon(updated_icon)
416
555
 
417
556
  def add_action(self, action_id: str, action: ToolBarAction, target_widget: QWidget):
418
557
  """
419
- Adds a new standalone action to the toolbar dynamically.
558
+ Adds a new standalone action dynamically.
420
559
 
421
560
  Args:
422
- action_id (str): Unique identifier for the action.
423
- action (ToolBarAction): The action to add to the toolbar.
424
- target_widget (QWidget): The target widget for the action.
561
+ action_id (str): Unique identifier.
562
+ action (ToolBarAction): The action to add.
563
+ target_widget (QWidget): The target widget.
425
564
  """
426
565
  if action_id in self.widgets:
427
566
  raise ValueError(f"Action with ID '{action_id}' already exists.")
428
567
  action.add_to_toolbar(self, target_widget)
429
568
  self.widgets[action_id] = action
430
569
  self.toolbar_items.append(("action", action_id))
431
- self.update_separators() # Update separators after adding the action
570
+ self.update_separators()
432
571
 
433
572
  def hide_action(self, action_id: str):
434
573
  """
435
- Hides a specific action on the toolbar.
574
+ Hides a specific action.
436
575
 
437
576
  Args:
438
- action_id (str): Unique identifier for the action to hide.
577
+ action_id (str): Unique identifier.
439
578
  """
440
579
  if action_id not in self.widgets:
441
580
  raise ValueError(f"Action with ID '{action_id}' does not exist.")
442
581
  action = self.widgets[action_id]
443
- if hasattr(action, "action") and isinstance(action.action, QAction):
582
+ if hasattr(action, "action") and action.action is not None:
444
583
  action.action.setVisible(False)
445
- self.update_separators() # Update separators after hiding the action
584
+ self.update_separators()
446
585
 
447
586
  def show_action(self, action_id: str):
448
587
  """
449
- Shows a specific action on the toolbar.
588
+ Shows a specific action.
450
589
 
451
590
  Args:
452
- action_id (str): Unique identifier for the action to show.
591
+ action_id (str): Unique identifier.
453
592
  """
454
593
  if action_id not in self.widgets:
455
594
  raise ValueError(f"Action with ID '{action_id}' does not exist.")
456
595
  action = self.widgets[action_id]
457
- if hasattr(action, "action") and isinstance(action.action, QAction):
596
+ if hasattr(action, "action") and action.action is not None:
458
597
  action.action.setVisible(True)
459
- self.update_separators() # Update separators after showing the action
598
+ self.update_separators()
460
599
 
461
600
  def add_bundle(self, bundle: ToolbarBundle, target_widget: QWidget):
462
601
  """
463
- Adds a bundle of actions to the toolbar, separated by a separator.
602
+ Adds a bundle of actions, separated by a separator.
464
603
 
465
604
  Args:
466
- bundle (ToolbarBundle): The bundle to add.
467
- target_widget (QWidget): The target widget for the actions.
605
+ bundle (ToolbarBundle): The bundle.
606
+ target_widget (QWidget): The target widget.
468
607
  """
469
608
  if bundle.bundle_id in self.bundles:
470
609
  raise ValueError(f"ToolbarBundle with ID '{bundle.bundle_id}' already exists.")
471
610
 
472
- # Add a separator before the bundle (but not to first one)
473
611
  if self.toolbar_items:
474
612
  sep = SeparatorAction()
475
613
  sep.add_to_toolbar(self, target_widget)
476
614
  self.toolbar_items.append(("separator", None))
477
615
 
478
- # Add each action in the bundle
479
616
  for action_id, action_obj in bundle.actions.items():
480
617
  action_obj.add_to_toolbar(self, target_widget)
481
618
  self.widgets[action_id] = action_obj
482
619
 
483
- # Register the bundle
484
620
  self.bundles[bundle.bundle_id] = list(bundle.actions.keys())
485
621
  self.toolbar_items.append(("bundle", bundle.bundle_id))
622
+ self.update_separators()
623
+
624
+ def add_action_to_bundle(self, bundle_id: str, action_id: str, action, target_widget: QWidget):
625
+ """
626
+ Dynamically adds an action to an existing bundle.
627
+
628
+ Args:
629
+ bundle_id (str): The bundle ID.
630
+ action_id (str): Unique identifier.
631
+ action (ToolBarAction): The action to add.
632
+ target_widget (QWidget): The target widget.
633
+ """
634
+ if bundle_id not in self.bundles:
635
+ raise ValueError(f"Bundle '{bundle_id}' does not exist.")
636
+ if action_id in self.widgets:
637
+ raise ValueError(f"Action with ID '{action_id}' already exists.")
638
+
639
+ action.add_to_toolbar(self, target_widget)
640
+ new_qaction = action.action
641
+ self.removeAction(new_qaction)
642
+
643
+ bundle_action_ids = self.bundles[bundle_id]
644
+ if bundle_action_ids:
645
+ last_bundle_action = self.widgets[bundle_action_ids[-1]].action
646
+ actions_list = self.actions()
647
+ try:
648
+ index = actions_list.index(last_bundle_action)
649
+ except ValueError:
650
+ self.addAction(new_qaction)
651
+ else:
652
+ if index + 1 < len(actions_list):
653
+ before_action = actions_list[index + 1]
654
+ self.insertAction(before_action, new_qaction)
655
+ else:
656
+ self.addAction(new_qaction)
657
+ else:
658
+ self.addAction(new_qaction)
486
659
 
487
- self.update_separators() # Update separators after adding the bundle
660
+ self.widgets[action_id] = action
661
+ self.bundles[bundle_id].append(action_id)
662
+ self.update_separators()
488
663
 
489
664
  def contextMenuEvent(self, event):
490
665
  """
491
- Overrides the context menu event to show a list of toolbar actions with checkboxes and icons, including separators.
666
+ Overrides the context menu event to show toolbar actions with checkboxes and icons.
492
667
 
493
668
  Args:
494
- event(QContextMenuEvent): The context menu event.
669
+ event (QContextMenuEvent): The context menu event.
495
670
  """
496
671
  menu = QMenu(self)
497
-
498
- # Iterate through the toolbar items in order
499
672
  for item_type, identifier in self.toolbar_items:
500
673
  if item_type == "separator":
501
674
  menu.addSeparator()
@@ -503,18 +676,16 @@ class ModularToolBar(QToolBar):
503
676
  self.handle_bundle_context_menu(menu, identifier)
504
677
  elif item_type == "action":
505
678
  self.handle_action_context_menu(menu, identifier)
506
-
507
- # Connect the triggered signal after all actions are added
508
679
  menu.triggered.connect(self.handle_menu_triggered)
509
680
  menu.exec_(event.globalPos())
510
681
 
511
682
  def handle_bundle_context_menu(self, menu: QMenu, bundle_id: str):
512
683
  """
513
- Adds a set of bundle actions to the context menu.
684
+ Adds bundle actions to the context menu.
514
685
 
515
686
  Args:
516
- menu (QMenu): The context menu to which the actions are added.
517
- bundle_id (str): The identifier for the bundle.
687
+ menu (QMenu): The context menu.
688
+ bundle_id (str): The bundle identifier.
518
689
  """
519
690
  action_ids = self.bundles.get(bundle_id, [])
520
691
  for act_id in action_ids:
@@ -535,7 +706,6 @@ class ModularToolBar(QToolBar):
535
706
  # Set the icon if available
536
707
  if qaction.icon() and not qaction.icon().isNull():
537
708
  menu_action.setIcon(qaction.icon())
538
-
539
709
  menu.addAction(menu_action)
540
710
 
541
711
  def handle_action_context_menu(self, menu: QMenu, action_id: str):
@@ -565,73 +735,95 @@ class ModularToolBar(QToolBar):
565
735
  menu.addAction(menu_action)
566
736
 
567
737
  def handle_menu_triggered(self, action):
568
- """Handles the toggling of toolbar actions from the context menu."""
738
+ """
739
+ Handles the triggered signal from the context menu.
740
+
741
+ Args:
742
+ action: Action triggered.
743
+ """
569
744
  action_id = action.data()
570
745
  if action_id:
571
746
  self.toggle_action_visibility(action_id, action.isChecked())
572
747
 
573
748
  def toggle_action_visibility(self, action_id: str, visible: bool):
574
749
  """
575
- Toggles the visibility of a specific action on the toolbar.
750
+ Toggles the visibility of a specific action.
576
751
 
577
752
  Args:
578
- action_id(str): Unique identifier for the action to toggle.
579
- visible(bool): Whether the action should be visible.
753
+ action_id (str): Unique identifier.
754
+ visible (bool): Whether the action should be visible.
580
755
  """
581
756
  if action_id not in self.widgets:
582
757
  return
583
-
584
758
  tool_action = self.widgets[action_id]
585
- if hasattr(tool_action, "action") and isinstance(tool_action.action, QAction):
759
+ if hasattr(tool_action, "action") and tool_action.action is not None:
586
760
  tool_action.action.setVisible(visible)
587
761
  self.update_separators()
588
762
 
589
763
  def update_separators(self):
590
764
  """
591
- Hide separators that are adjacent to another separator or have no actions next to them.
765
+ Hide separators that are adjacent to another separator or have no non-separator actions between them.
592
766
  """
593
767
  toolbar_actions = self.actions()
594
-
768
+ # First pass: set visibility based on surrounding non-separator actions.
595
769
  for i, action in enumerate(toolbar_actions):
596
770
  if not action.isSeparator():
597
771
  continue
598
- # Find the previous visible action
599
772
  prev_visible = None
600
773
  for j in range(i - 1, -1, -1):
601
774
  if toolbar_actions[j].isVisible():
602
775
  prev_visible = toolbar_actions[j]
603
776
  break
604
-
605
- # Find the next visible action
606
777
  next_visible = None
607
778
  for j in range(i + 1, len(toolbar_actions)):
608
779
  if toolbar_actions[j].isVisible():
609
780
  next_visible = toolbar_actions[j]
610
781
  break
611
-
612
- # Determine if the separator should be hidden
613
- # Hide if both previous and next visible actions are separators or non-existent
614
782
  if (prev_visible is None or prev_visible.isSeparator()) and (
615
783
  next_visible is None or next_visible.isSeparator()
616
784
  ):
617
785
  action.setVisible(False)
618
786
  else:
619
787
  action.setVisible(True)
788
+ # Second pass: ensure no two visible separators are adjacent.
789
+ prev = None
790
+ for action in toolbar_actions:
791
+ if action.isVisible() and action.isSeparator():
792
+ if prev and prev.isSeparator():
793
+ action.setVisible(False)
794
+ else:
795
+ prev = action
796
+ else:
797
+ if action.isVisible():
798
+ prev = action
620
799
 
621
800
 
622
801
  class MainWindow(QMainWindow): # pragma: no cover
623
802
  def __init__(self):
624
803
  super().__init__()
625
804
  self.setWindowTitle("Toolbar / ToolbarBundle Demo")
626
-
627
805
  self.central_widget = QWidget()
628
806
  self.setCentralWidget(self.central_widget)
807
+ self.test_label = QLabel(text="This is a test label.")
808
+ self.central_widget.layout = QVBoxLayout(self.central_widget)
809
+ self.central_widget.layout.addWidget(self.test_label)
629
810
 
630
- # Create a modular toolbar
631
811
  self.toolbar = ModularToolBar(parent=self, target_widget=self)
632
812
  self.addToolBar(self.toolbar)
633
813
 
634
- # Example: Add a single bundle
814
+ self.add_switchable_button_checkable()
815
+ self.add_switchable_button_non_checkable()
816
+ self.add_widget_actions()
817
+ self.add_bundles()
818
+ self.add_menus()
819
+
820
+ # For theme testing
821
+
822
+ self.dark_button = DarkModeButton(toolbar=True)
823
+ dark_mode_action = WidgetAction(label=None, widget=self.dark_button)
824
+ self.toolbar.add_action("dark_mode", dark_mode_action, self)
825
+
826
+ def add_bundles(self):
635
827
  home_action = MaterialIconAction(
636
828
  icon_name="home", tooltip="Home", checkable=True, parent=self
637
829
  )
@@ -651,12 +843,11 @@ class MainWindow(QMainWindow): # pragma: no cover
651
843
  )
652
844
  self.toolbar.add_bundle(main_actions_bundle, target_widget=self)
653
845
 
654
- # Another bundle
655
846
  search_action = MaterialIconAction(
656
- icon_name="search", tooltip="Search", checkable=True, parent=self
847
+ icon_name="search", tooltip="Search", checkable=False, parent=self
657
848
  )
658
849
  help_action = MaterialIconAction(
659
- icon_name="help", tooltip="Help", checkable=True, parent=self
850
+ icon_name="help", tooltip="Help", checkable=False, parent=self
660
851
  )
661
852
  second_bundle = ToolbarBundle(
662
853
  bundle_id="secondary_actions",
@@ -664,9 +855,101 @@ class MainWindow(QMainWindow): # pragma: no cover
664
855
  )
665
856
  self.toolbar.add_bundle(second_bundle, target_widget=self)
666
857
 
858
+ new_action = MaterialIconAction(
859
+ icon_name="counter_1", tooltip="New Action", checkable=True, parent=self
860
+ )
861
+ self.toolbar.add_action_to_bundle(
862
+ "main_actions", "new_action", new_action, target_widget=self
863
+ )
864
+
865
+ def add_menus(self):
866
+ menu_material_actions = {
867
+ "mat1": MaterialIconAction(
868
+ icon_name="home", tooltip="Material Home", checkable=True, parent=self
869
+ ),
870
+ "mat2": MaterialIconAction(
871
+ icon_name="settings", tooltip="Material Settings", checkable=True, parent=self
872
+ ),
873
+ "mat3": MaterialIconAction(
874
+ icon_name="info", tooltip="Material Info", checkable=True, parent=self
875
+ ),
876
+ }
877
+ menu_qt_actions = {
878
+ "qt1": QtIconAction(
879
+ standard_icon=QStyle.SP_FileIcon, tooltip="Qt File", checkable=True, parent=self
880
+ ),
881
+ "qt2": QtIconAction(
882
+ standard_icon=QStyle.SP_DirIcon, tooltip="Qt Directory", checkable=True, parent=self
883
+ ),
884
+ "qt3": QtIconAction(
885
+ standard_icon=QStyle.SP_TrashIcon, tooltip="Qt Trash", checkable=True, parent=self
886
+ ),
887
+ }
888
+ expandable_menu_material = ExpandableMenuAction(
889
+ label="Material Menu", actions=menu_material_actions
890
+ )
891
+ expandable_menu_qt = ExpandableMenuAction(label="Qt Menu", actions=menu_qt_actions)
892
+
893
+ self.toolbar.add_action("material_menu", expandable_menu_material, self)
894
+ self.toolbar.add_action("qt_menu", expandable_menu_qt, self)
895
+
896
+ def add_switchable_button_checkable(self):
897
+ action1 = MaterialIconAction(
898
+ icon_name="counter_1", tooltip="Action 1", checkable=True, parent=self
899
+ )
900
+ action2 = MaterialIconAction(
901
+ icon_name="counter_2", tooltip="Action 2", checkable=True, parent=self
902
+ )
903
+
904
+ switchable_action = SwitchableToolBarAction(
905
+ actions={"action1": action1, "action2": action2},
906
+ initial_action="action1",
907
+ tooltip="Switchable Action",
908
+ checkable=True,
909
+ parent=self,
910
+ )
911
+ self.toolbar.add_action("switchable_action", switchable_action, self)
912
+
913
+ action1.action.toggled.connect(
914
+ lambda checked: self.test_label.setText(f"Action 1 triggered, checked = {checked}")
915
+ )
916
+ action2.action.toggled.connect(
917
+ lambda checked: self.test_label.setText(f"Action 2 triggered, checked = {checked}")
918
+ )
919
+
920
+ def add_switchable_button_non_checkable(self):
921
+ action1 = MaterialIconAction(
922
+ icon_name="counter_1", tooltip="Action 1", checkable=False, parent=self
923
+ )
924
+ action2 = MaterialIconAction(
925
+ icon_name="counter_2", tooltip="Action 2", checkable=False, parent=self
926
+ )
927
+
928
+ switchable_action = SwitchableToolBarAction(
929
+ actions={"action1": action1, "action2": action2},
930
+ initial_action="action1",
931
+ tooltip="Switchable Action",
932
+ checkable=False,
933
+ parent=self,
934
+ )
935
+ self.toolbar.add_action("switchable_action_no_toggle", switchable_action, self)
936
+
937
+ action1.action.triggered.connect(
938
+ lambda checked: self.test_label.setText(f"Action 1 triggered, checked = {checked}")
939
+ )
940
+ action2.action.triggered.connect(
941
+ lambda checked: self.test_label.setText(f"Action 2 triggered, checked = {checked}")
942
+ )
943
+
944
+ def add_widget_actions(self):
945
+ combo = QComboBox()
946
+ combo.addItems(["Option 1", "Option 2", "Option 3"])
947
+ self.toolbar.add_action("device_combo", WidgetAction(label="Device:", widget=combo), self)
948
+
667
949
 
668
950
  if __name__ == "__main__": # pragma: no cover
669
951
  app = QApplication(sys.argv)
952
+ set_theme("light")
670
953
  main_window = MainWindow()
671
954
  main_window.show()
672
955
  sys.exit(app.exec_())
@@ -66,7 +66,7 @@ class BECWidget(BECConnector):
66
66
  if hasattr(qapp, "theme_signal"):
67
67
  qapp.theme_signal.theme_updated.connect(self._update_theme)
68
68
 
69
- def _update_theme(self, theme: str):
69
+ def _update_theme(self, theme: str | None = None):
70
70
  """Update the theme."""
71
71
  if theme is None:
72
72
  qapp = QApplication.instance()
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import itertools
4
3
  import re
5
4
  from typing import TYPE_CHECKING, Literal
6
5
 
@@ -71,15 +70,64 @@ def apply_theme(theme: Literal["dark", "light"]):
71
70
  Apply the theme to all pyqtgraph widgets. Do not use this function directly. Use set_theme instead.
72
71
  """
73
72
  app = QApplication.instance()
74
- # go through all pyqtgraph widgets and set background
75
- children = itertools.chain.from_iterable(
76
- top.findChildren(pg.GraphicsLayoutWidget) for top in app.topLevelWidgets()
77
- )
78
- pg.setConfigOptions(
79
- foreground="d" if theme == "dark" else "k", background="k" if theme == "dark" else "w"
80
- )
81
- for pg_widget in children:
82
- pg_widget.setBackground("k" if theme == "dark" else "w")
73
+ graphic_layouts = [
74
+ child
75
+ for top in app.topLevelWidgets()
76
+ for child in top.findChildren(pg.GraphicsLayoutWidget)
77
+ ]
78
+
79
+ plot_items = [
80
+ item
81
+ for gl in graphic_layouts
82
+ for item in gl.ci.items.keys() # ci is internal pg.GraphicsLayout that hosts all items
83
+ if isinstance(item, pg.PlotItem)
84
+ ]
85
+
86
+ histograms = [
87
+ item
88
+ for gl in graphic_layouts
89
+ for item in gl.ci.items.keys() # ci is internal pg.GraphicsLayout that hosts all items
90
+ if isinstance(item, pg.HistogramLUTItem)
91
+ ]
92
+
93
+ # Update background color based on the theme
94
+ if theme == "light":
95
+ background_color = "#e9ecef" # Subtle contrast for light mode
96
+ foreground_color = "#141414"
97
+ label_color = "#000000"
98
+ axis_color = "#666666"
99
+ else:
100
+ background_color = "#141414" # Dark mode
101
+ foreground_color = "#e9ecef"
102
+ label_color = "#FFFFFF"
103
+ axis_color = "#CCCCCC"
104
+
105
+ # update GraphicsLayoutWidget
106
+ pg.setConfigOptions(foreground=foreground_color, background=background_color)
107
+ for pg_widget in graphic_layouts:
108
+ pg_widget.setBackground(background_color)
109
+
110
+ # update PlotItems
111
+ for plot_item in plot_items:
112
+ for axis in ["left", "right", "top", "bottom"]:
113
+ plot_item.getAxis(axis).setPen(pg.mkPen(color=axis_color))
114
+ plot_item.getAxis(axis).setTextPen(pg.mkPen(color=label_color))
115
+
116
+ # Change title color
117
+ plot_item.titleLabel.setText(plot_item.titleLabel.text, color=label_color)
118
+
119
+ # Change legend color
120
+ if hasattr(plot_item, "legend") and plot_item.legend is not None:
121
+ plot_item.legend.setLabelTextColor(label_color)
122
+ # if legend is in plot item and theme is changed, has to be like that because of pg opt logic
123
+ for sample, label in plot_item.legend.items:
124
+ label_text = label.text
125
+ label.setText(label_text, color=label_color)
126
+
127
+ # update HistogramLUTItem
128
+ for histogram in histograms:
129
+ histogram.axis.setPen(pg.mkPen(color=axis_color))
130
+ histogram.axis.setTextPen(pg.mkPen(color=label_color))
83
131
 
84
132
  # now define stylesheet according to theme and apply it
85
133
  style = bec_qthemes.load_stylesheet(theme)
@@ -11,7 +11,6 @@ from bec_widgets.qt_utils.side_panel import SidePanel
11
11
  from bec_widgets.qt_utils.toolbar import MaterialIconAction, ModularToolBar, SeparatorAction
12
12
  from bec_widgets.utils import ConnectionConfig, Crosshair, EntryValidator
13
13
  from bec_widgets.utils.bec_widget import BECWidget
14
- from bec_widgets.utils.colors import set_theme
15
14
  from bec_widgets.utils.fps_counter import FPSCounter
16
15
  from bec_widgets.utils.widget_state_manager import WidgetStateManager
17
16
  from bec_widgets.widgets.containers.layout_manager.layout_manager import LayoutManagerWidget
@@ -20,7 +19,7 @@ from bec_widgets.widgets.plots_next_gen.toolbar_bundles.mouse_interactions impor
20
19
  MouseInteractionToolbarBundle,
21
20
  )
22
21
  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
22
+ from bec_widgets.widgets.plots_next_gen.toolbar_bundles.roi_bundle import ROIBundle
24
23
  from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
25
24
 
26
25
  logger = bec_logger.logger
@@ -83,8 +82,9 @@ class PlotBase(BECWidget, QWidget):
83
82
  self.entry_validator = EntryValidator(self.dev)
84
83
 
85
84
  # Base widgets elements
85
+ self.plot_widget = pg.GraphicsLayoutWidget(parent=self)
86
86
  self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
87
- self.plot_widget = pg.PlotWidget(plotItem=self.plot_item)
87
+ self.plot_widget.addItem(self.plot_item)
88
88
  self.side_panel = SidePanel(self, orientation="left", panel_max_width=280)
89
89
  self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
90
90
  self.init_toolbar()
@@ -94,13 +94,20 @@ class PlotBase(BECWidget, QWidget):
94
94
  self.crosshair = None
95
95
  self.fps_monitor = None
96
96
  self.fps_label = QLabel(alignment=Qt.AlignmentFlag.AlignRight)
97
+ self._user_x_label = ""
98
+ self._x_label_suffix = ""
97
99
 
98
100
  self._init_ui()
99
101
 
102
+ self._connect_to_theme_change()
103
+ self._update_theme()
104
+
105
+ def apply_theme(self, theme: str):
106
+ self.round_plot_widget.apply_theme(theme)
107
+
100
108
  def _init_ui(self):
101
109
  self.layout.addWidget(self.layout_manager)
102
110
  self.round_plot_widget = RoundedFrame(content_widget=self.plot_widget, theme_update=True)
103
- self.round_plot_widget.apply_theme("dark")
104
111
 
105
112
  self.layout_manager.add_widget(self.round_plot_widget)
106
113
  self.layout_manager.add_widget_relative(self.fps_label, self.round_plot_widget, "top")
@@ -117,19 +124,15 @@ class PlotBase(BECWidget, QWidget):
117
124
 
118
125
  self.plot_export_bundle = PlotExportBundle("plot_export", target_widget=self)
119
126
  self.mouse_bundle = MouseInteractionToolbarBundle("mouse_interaction", target_widget=self)
120
- self.state_export_bundle = SaveStateBundle("state_export", target_widget=self)
127
+ # self.state_export_bundle = SaveStateBundle("state_export", target_widget=self) #TODO ATM disabled, cannot be used in DockArea, which is exposed to the user
128
+ self.roi_bundle = ROIBundle("roi", target_widget=self)
121
129
 
122
130
  # Add elements to toolbar
123
131
  self.toolbar.add_bundle(self.plot_export_bundle, target_widget=self)
124
- self.toolbar.add_bundle(self.state_export_bundle, target_widget=self)
132
+ # self.toolbar.add_bundle(self.state_export_bundle, target_widget=self) #TODO ATM disabled, cannot be used in DockArea, which is exposed to the user
125
133
  self.toolbar.add_bundle(self.mouse_bundle, target_widget=self)
134
+ self.toolbar.add_bundle(self.roi_bundle, target_widget=self)
126
135
 
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
136
  self.toolbar.add_action("separator_1", SeparatorAction(), target_widget=self)
134
137
  self.toolbar.add_action(
135
138
  "fps_monitor",
@@ -141,7 +144,6 @@ class PlotBase(BECWidget, QWidget):
141
144
  self.toolbar.widgets["fps_monitor"].action.toggled.connect(
142
145
  lambda checked: setattr(self, "enable_fps_monitor", checked)
143
146
  )
144
- self.toolbar.widgets["crosshair"].action.toggled.connect(self.toggle_crosshair)
145
147
 
146
148
  def add_side_menus(self):
147
149
  """Adds multiple menus to the side panel."""
@@ -256,12 +258,45 @@ class PlotBase(BECWidget, QWidget):
256
258
 
257
259
  @SafeProperty(str, doc="The text of the x label")
258
260
  def x_label(self) -> str:
259
- return self.plot_item.getAxis("bottom").labelText
261
+ return self._user_x_label
260
262
 
261
263
  @x_label.setter
262
264
  def x_label(self, value: str):
263
- self.plot_item.setLabel("bottom", text=value)
264
- self.property_changed.emit("x_label", value)
265
+ self._user_x_label = value
266
+ self._apply_x_label()
267
+ self.property_changed.emit("x_label", self._user_x_label)
268
+
269
+ @property
270
+ def x_label_suffix(self) -> str:
271
+ """
272
+ A read-only (or internal) suffix automatically appended to the user label.
273
+ Not settable by the user directly from the UI.
274
+ """
275
+ return self._x_label_suffix
276
+
277
+ def set_x_label_suffix(self, suffix: str):
278
+ """
279
+ Public or protected method to update the suffix.
280
+ The user code or subclass (Waveform) can call this
281
+ when x_mode changes, but the AxisSettings won't show it.
282
+ """
283
+ self._x_label_suffix = suffix
284
+ self._apply_x_label()
285
+
286
+ @property
287
+ def x_label_combined(self) -> str:
288
+ """
289
+ The final label shown on the axis = user portion + suffix.
290
+ """
291
+ return self._user_x_label + self._x_label_suffix
292
+
293
+ def _apply_x_label(self):
294
+ """
295
+ Actually updates the pyqtgraph axis label text to
296
+ the combined label. Called whenever user label or suffix changes.
297
+ """
298
+ final_label = self.x_label_combined
299
+ self.plot_item.setLabel("bottom", text=final_label)
265
300
 
266
301
  @SafeProperty(str, doc="The text of the y label")
267
302
  def y_label(self) -> str:
@@ -545,6 +580,7 @@ class PlotBase(BECWidget, QWidget):
545
580
  self.unhook_crosshair()
546
581
  self.unhook_fps_monitor(delete_label=True)
547
582
  self.cleanup_pyqtgraph()
583
+ self.rpc_register.remove_rpc(self)
548
584
 
549
585
  def cleanup_pyqtgraph(self):
550
586
  """Cleanup pyqtgraph items."""
@@ -561,7 +597,6 @@ if __name__ == "__main__": # pragma: no cover:
561
597
  from qtpy.QtWidgets import QApplication
562
598
 
563
599
  app = QApplication(sys.argv)
564
- set_theme("dark")
565
600
  widget = PlotBase()
566
601
  widget.show()
567
602
  # Just some example data and parameters to test
@@ -55,6 +55,27 @@ class MouseInteractionToolbarBundle(ToolbarBundle):
55
55
  auto.action.triggered.connect(self.autorange_plot)
56
56
  aspect_ratio.action.toggled.connect(self.lock_aspect_ratio)
57
57
 
58
+ mode = self.get_viewbox_mode()
59
+ if mode == "PanMode":
60
+ drag.action.setChecked(True)
61
+ elif mode == "RectMode":
62
+ rect.action.setChecked(True)
63
+
64
+ def get_viewbox_mode(self) -> str:
65
+ """
66
+ Returns the current interaction mode of a PyQtGraph ViewBox.
67
+
68
+ Returns:
69
+ str: "PanMode" if pan is enabled, "RectMode" if zoom is enabled, "Unknown" otherwise.
70
+ """
71
+ if self.target_widget:
72
+ viewbox = self.target_widget.plot_item.getViewBox()
73
+ if viewbox.getState()["mouseMode"] == 3:
74
+ return "PanMode"
75
+ elif viewbox.getState()["mouseMode"] == 1:
76
+ return "RectMode"
77
+ return "Unknown"
78
+
58
79
  @SafeSlot(bool)
59
80
  def enable_mouse_rectangle_mode(self, checked: bool):
60
81
  """
@@ -0,0 +1,26 @@
1
+ from bec_widgets.qt_utils.toolbar import MaterialIconAction, ToolbarBundle
2
+
3
+
4
+ class ROIBundle(ToolbarBundle):
5
+ """
6
+ A bundle of actions that are hooked in this constructor itself,
7
+ so that you can immediately connect the signals and toggle states.
8
+
9
+ This bundle is for a toolbar that controls crosshair and ROI interaction.
10
+ """
11
+
12
+ def __init__(self, bundle_id="roi", target_widget=None, **kwargs):
13
+ super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
14
+ self.target_widget = target_widget
15
+
16
+ # Create each MaterialIconAction with a parent
17
+ # so the signals can fire even if the toolbar isn't added yet.
18
+ crosshair = MaterialIconAction(
19
+ icon_name="point_scan", tooltip="Show Crosshair", checkable=True
20
+ )
21
+
22
+ # Add them to the bundle
23
+ self.add_action("crosshair", crosshair)
24
+
25
+ # Immediately connect signals
26
+ crosshair.action.toggled.connect(self.target_widget.toggle_crosshair)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 1.21.3
3
+ Version: 1.22.0
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
@@ -2,11 +2,11 @@
2
2
  .gitlab-ci.yml,sha256=PuL-FmkTHm7qs467Mh9D8quWcEj4tgEA-UUGDieMuWk,8774
3
3
  .pylintrc,sha256=eeY8YwSI74oFfq6IYIbCqnx3Vk8ZncKaatv96n_Y8Rs,18544
4
4
  .readthedocs.yaml,sha256=aSOc277LqXcsTI6lgvm_JY80lMlr69GbPKgivua2cS0,603
5
- CHANGELOG.md,sha256=wa_NuBhhF8FiLGPc83aoatKrjIOevuQGe9cHp8bEXlU,228561
5
+ CHANGELOG.md,sha256=I0VXV76hN6hH8JX7l9F5OXVqbuenWClNEslZF43a6yU,230360
6
6
  LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
7
- PKG-INFO,sha256=HQ3U74E2VE4lMPxWO5PJv-MeMP9fqpzYZHlsUBxa9Ms,1173
7
+ PKG-INFO,sha256=AoFfQtO1XAwk8aXaYDJs2ZBxHxPbxVKUbwOVXpQAkJw,1173
8
8
  README.md,sha256=KgdKusjlvEvFtdNZCeDMO91y77MWK2iDcYMDziksOr4,2553
9
- pyproject.toml,sha256=uh7Ew520igoYuhlY3qjUeV7ieYJ0n_Dp2IA6kq1ZvzI,2540
9
+ pyproject.toml,sha256=HVH5bV4UEM4smaSNq_bv0FAjv14NavzQ0xGE3K1r_sk,2540
10
10
  .git_hooks/pre-commit,sha256=n3RofIZHJl8zfJJIUomcMyYGFi_rwq4CC19z0snz3FI,286
11
11
  .gitlab/issue_templates/bug_report_template.md,sha256=gAuyEwl7XlnebBrkiJ9AqffSNOywmr8vygUFWKTuQeI,386
12
12
  .gitlab/issue_templates/documentation_update_template.md,sha256=FHLdb3TS_D9aL4CYZCjyXSulbaW5mrN2CmwTaeLPbNw,860
@@ -52,10 +52,10 @@ bec_widgets/qt_utils/compact_popup.py,sha256=3yeb-GJ1PUla5Q_hT0XDKqvyIEH9yV_eGid
52
52
  bec_widgets/qt_utils/error_popups.py,sha256=7bL-Lil1G8reQPaRZo8NfG_7Cq2Y0HkF3KREJUE0ZlQ,11545
53
53
  bec_widgets/qt_utils/palette_viewer.py,sha256=--B0x7aE7bniHIeuuLY_pH8yBDrTTXaE0IDrC_AM1mo,6326
54
54
  bec_widgets/qt_utils/redis_message_waiter.py,sha256=fvL_QgC0cTDv_FPJdRyp5AKjf401EJU4z3r38p47ydY,1745
55
- bec_widgets/qt_utils/round_frame.py,sha256=JKVEcPY5TAEQcj2XeCyvlMzE41gUWgLaIEDOSQjTC3Y,5812
55
+ bec_widgets/qt_utils/round_frame.py,sha256=ldLQ4BaWygFRrRv1s0w--5qZ4sj8MAqhdXuzho50xMg,4860
56
56
  bec_widgets/qt_utils/settings_dialog.py,sha256=NhtzTer_xzlB2lLLrGklkI1QYLJEWQpJoZbCz4o5daI,3645
57
57
  bec_widgets/qt_utils/side_panel.py,sha256=H2Ko7FPYwQ2Nemrud-q-rmqzHGO8vly_pzE_ySEfoGQ,12569
58
- bec_widgets/qt_utils/toolbar.py,sha256=YY_-UGc7uZhahYn7xnTvBGbalmTkpTa4WLikpsHwnMw,24433
58
+ bec_widgets/qt_utils/toolbar.py,sha256=AizwnTiY2RkRJfouJu4nCiFoWd0wEgS_nDF36EiUQjk,35779
59
59
  bec_widgets/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
60
  bec_widgets/tests/utils.py,sha256=GbQtN7qf9n-8FoAfNddZ4aAqA7oBo_hGAlnKELd6Xzw,6943
61
61
  bec_widgets/utils/__init__.py,sha256=1930ji1Jj6dVuY81Wd2kYBhHYNV-2R0bN_L4o9zBj1U,533
@@ -64,8 +64,8 @@ bec_widgets/utils/bec_designer.py,sha256=XBy38NbNMoRDpvRx5lGP2XnJNG34YKZ7I-ARFkn
64
64
  bec_widgets/utils/bec_dispatcher.py,sha256=OFmkx9vOz4pA4Sdc14QreyDZ870QYskJ4B5daVVeYg4,6325
65
65
  bec_widgets/utils/bec_signal_proxy.py,sha256=56lgrHmKEDxzvhLw5DJcyBVJJUcrPSqQIU1EFQmwutw,3268
66
66
  bec_widgets/utils/bec_table.py,sha256=nA2b8ukSeUfquFMAxGrUVOqdrzMoDYD6O_4EYbOG2zk,717
67
- bec_widgets/utils/bec_widget.py,sha256=1lrHNuvW6uOuPpr-cJBYJNbFekTsqpnQdfTo3P5tbWI,3330
68
- bec_widgets/utils/colors.py,sha256=zL9ieD_Bsb2ehd6tPpgfkhu1u5qrQRIE4mvoqM2iqko,16546
67
+ bec_widgets/utils/bec_widget.py,sha256=O3JSaKXL3l62EU9sbpsRL8lgnE-aZHkwccuWTSiP7zk,3344
68
+ bec_widgets/utils/colors.py,sha256=8HKIIwQX4P7ND7ITtGWcN8ozG2xT3IBTeYMkneaPBC8,18288
69
69
  bec_widgets/utils/container_utils.py,sha256=0wr3ZfuMiAFKCrQHVjxjw-Vuk8wsHdridqcjy2eY840,1531
70
70
  bec_widgets/utils/crosshair.py,sha256=baT68rFZjAOoQDC9V3p7vfYhIy1FIMnPNtbnppvjYiM,18141
71
71
  bec_widgets/utils/entry_validator.py,sha256=3skJIsUwTYicT76AMHm_M78RiWtUgyD2zb-Rxo2HdHQ,1313
@@ -276,14 +276,15 @@ bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.py,
276
276
  bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.ui,sha256=vaOfrygcQp3-H82AkMUHgV2v0Y_TmRO5KLui-bWoado,11056
277
277
  bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
278
278
  bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py,sha256=78vAXX_x5vi1XMtncOHvTvfgGnZJNSkRjOAnCOBE26U,1047
279
- bec_widgets/widgets/plots_next_gen/plot_base.py,sha256=WwRp1bKhnFpMkZNDSxNvn2v5YxOxcFH0ISaxeK3KLTg,20941
279
+ bec_widgets/widgets/plots_next_gen/plot_base.py,sha256=epwxSv6vH_RutDPV8jRlA9kq7tc4qUYOsLoeSS9iKAo,22113
280
280
  bec_widgets/widgets/plots_next_gen/setting_menus/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
281
281
  bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings.py,sha256=GpgTIYa7t4q9G8hF0rg_qJW2FgbrkvnsMMuqFKJR1fA,3567
282
282
  bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings_horizontal.ui,sha256=ye-guaRU_jhu7sHZS-9AjBjLrCtA1msOD0dszu4o9x8,11785
283
283
  bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings_vertical.ui,sha256=VR0BgkNAZWRXZPCvX3kMQwM0GLahgM_k2XYmSNbHM64,11235
284
284
  bec_widgets/widgets/plots_next_gen/toolbar_bundles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
285
- bec_widgets/widgets/plots_next_gen/toolbar_bundles/mouse_interactions.py,sha256=q8Ree9UA6ZPC_Z197aNQOFhNU6cELUdJn_218VoacmM,3195
285
+ bec_widgets/widgets/plots_next_gen/toolbar_bundles/mouse_interactions.py,sha256=tc7v50yS_BYJ_uBFm5lTYJX7gIOZnAr4DRC0wIWXDSk,3924
286
286
  bec_widgets/widgets/plots_next_gen/toolbar_bundles/plot_export.py,sha256=9xbpMtaBuY8kvzTSkEeFF52qJloEexHtPbGzU2f2n3s,2361
287
+ bec_widgets/widgets/plots_next_gen/toolbar_bundles/roi_bundle.py,sha256=kgEPk1MYJ0ighSm5T1ri8Hw1t82J4SKMvJLj_witnoQ,979
287
288
  bec_widgets/widgets/plots_next_gen/toolbar_bundles/save_state.py,sha256=a7yCJQ_8HVAYN2dHcbxAjo3nJOSUUQZ7RJuUuGCXDBU,1740
288
289
  bec_widgets/widgets/progress/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
289
290
  bec_widgets/widgets/progress/bec_progressbar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -355,8 +356,8 @@ bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py,sha256=Z
355
356
  bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.pyproject,sha256=Lbi9zb6HNlIq14k6hlzR-oz6PIFShBuF7QxE6d87d64,34
356
357
  bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button_plugin.py,sha256=CzChz2SSETYsR8-36meqWnsXCT-FIy_J_xeU5coWDY8,1350
357
358
  bec_widgets/widgets/utility/visual/dark_mode_button/register_dark_mode_button.py,sha256=rMpZ1CaoucwobgPj1FuKTnt07W82bV1GaSYdoqcdMb8,521
358
- bec_widgets-1.21.3.dist-info/METADATA,sha256=HQ3U74E2VE4lMPxWO5PJv-MeMP9fqpzYZHlsUBxa9Ms,1173
359
- bec_widgets-1.21.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
360
- bec_widgets-1.21.3.dist-info/entry_points.txt,sha256=dItMzmwA1wizJ1Itx15qnfJ0ZzKVYFLVJ1voxT7K7D4,214
361
- bec_widgets-1.21.3.dist-info/licenses/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
362
- bec_widgets-1.21.3.dist-info/RECORD,,
359
+ bec_widgets-1.22.0.dist-info/METADATA,sha256=AoFfQtO1XAwk8aXaYDJs2ZBxHxPbxVKUbwOVXpQAkJw,1173
360
+ bec_widgets-1.22.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
361
+ bec_widgets-1.22.0.dist-info/entry_points.txt,sha256=dItMzmwA1wizJ1Itx15qnfJ0ZzKVYFLVJ1voxT7K7D4,214
362
+ bec_widgets-1.22.0.dist-info/licenses/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
363
+ bec_widgets-1.22.0.dist-info/RECORD,,
pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bec_widgets"
7
- version = "1.21.3"
7
+ version = "1.22.0"
8
8
  description = "BEC Widgets"
9
9
  requires-python = ">=3.10"
10
10
  classifiers = [