bec-widgets 0.118.0__py3-none-any.whl → 0.119.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.
@@ -1,7 +1,8 @@
1
+ import time
1
2
  from types import SimpleNamespace
2
3
 
3
4
  from bec_qthemes import material_icon
4
- from qtpy.QtCore import Property, Qt
5
+ from qtpy.QtCore import Property, Qt, Signal
5
6
  from qtpy.QtGui import QColor
6
7
  from qtpy.QtWidgets import (
7
8
  QDialog,
@@ -9,6 +10,7 @@ from qtpy.QtWidgets import (
9
10
  QLabel,
10
11
  QPushButton,
11
12
  QSizePolicy,
13
+ QSpacerItem,
12
14
  QVBoxLayout,
13
15
  QWidget,
14
16
  )
@@ -98,6 +100,7 @@ class PopupDialog(QDialog):
98
100
  def closeEvent(self, event):
99
101
  self.content_widget.setVisible(False)
100
102
  self.content_widget.setParent(self.parent)
103
+ self.done(True)
101
104
 
102
105
 
103
106
  class CompactPopupWidget(QWidget):
@@ -107,29 +110,35 @@ class CompactPopupWidget(QWidget):
107
110
  In the compact form, a LED-like indicator shows a status indicator.
108
111
  """
109
112
 
113
+ expand = Signal(bool)
114
+
110
115
  def __init__(self, parent=None, layout=QVBoxLayout):
111
116
  super().__init__(parent)
112
117
 
113
118
  self._popup_window = None
119
+ self._expand_popup = True
114
120
 
115
121
  QVBoxLayout(self)
116
- self.compact_view = QWidget(self)
117
- self.compact_view.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
118
- QHBoxLayout(self.compact_view)
119
- self.compact_view.layout().setSpacing(0)
120
- self.compact_view.layout().setContentsMargins(0, 0, 0, 0)
121
- self.compact_label = QLabel(self.compact_view)
122
- self.compact_status = LedLabel(self.compact_view)
123
- self.compact_show_popup = QPushButton(self.compact_view)
122
+ self.compact_view_widget = QWidget(self)
123
+ self.compact_view_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
124
+ QHBoxLayout(self.compact_view_widget)
125
+ self.compact_view_widget.layout().setSpacing(0)
126
+ self.compact_view_widget.layout().setContentsMargins(0, 0, 0, 0)
127
+ self.compact_view_widget.layout().addSpacerItem(
128
+ QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Fixed)
129
+ )
130
+ self.compact_label = QLabel(self.compact_view_widget)
131
+ self.compact_status = LedLabel(self.compact_view_widget)
132
+ self.compact_show_popup = QPushButton(self.compact_view_widget)
124
133
  self.compact_show_popup.setFlat(True)
125
134
  self.compact_show_popup.setIcon(
126
- material_icon(icon_name="pan_zoom", size=(10, 10), convert_to_pixmap=False)
135
+ material_icon(icon_name="expand_content", size=(10, 10), convert_to_pixmap=False)
127
136
  )
128
- self.compact_view.layout().addWidget(self.compact_label)
129
- self.compact_view.layout().addWidget(self.compact_status)
130
- self.compact_view.layout().addWidget(self.compact_show_popup)
131
- self.compact_view.setVisible(False)
132
- self.layout().addWidget(self.compact_view)
137
+ self.compact_view_widget.layout().addWidget(self.compact_label)
138
+ self.compact_view_widget.layout().addWidget(self.compact_status)
139
+ self.compact_view_widget.layout().addWidget(self.compact_show_popup)
140
+ self.compact_view_widget.setVisible(False)
141
+ self.layout().addWidget(self.compact_view_widget)
133
142
  self.container = QWidget(self)
134
143
  self.layout().addWidget(self.container)
135
144
  self.container.setVisible(True)
@@ -148,8 +157,36 @@ class CompactPopupWidget(QWidget):
148
157
 
149
158
  def show_popup(self):
150
159
  """Display the contained widgets in a popup dialog"""
151
- self._popup_window = PopupDialog(self.container)
152
- self._popup_window.show()
160
+ if self._expand_popup:
161
+ # show popup
162
+ self._popup_window = PopupDialog(self.container)
163
+ self._popup_window.show()
164
+ self._popup_window.finished.connect(lambda: self.expand.emit(False))
165
+ self.expand.emit(True)
166
+ else:
167
+ if self.compact_view:
168
+ # expand in place
169
+ self.compact_view = False
170
+ self.compact_view_widget.setVisible(True)
171
+ self.compact_label.setVisible(False)
172
+ self.compact_status.setVisible(False)
173
+ self.compact_show_popup.setIcon(
174
+ material_icon(
175
+ icon_name="collapse_content", size=(10, 10), convert_to_pixmap=False
176
+ )
177
+ )
178
+ self.expand.emit(True)
179
+ else:
180
+ # back to compact form
181
+ self.compact_label.setVisible(True)
182
+ self.compact_status.setVisible(True)
183
+ self.compact_show_popup.setIcon(
184
+ material_icon(
185
+ icon_name="expand_content", size=(10, 10), convert_to_pixmap=False
186
+ )
187
+ )
188
+ self.compact_view = True
189
+ self.expand.emit(False)
153
190
 
154
191
  def setSizePolicy(self, size_policy1, size_policy2=None):
155
192
  # setting size policy on the compact popup widget will set
@@ -172,11 +209,11 @@ class CompactPopupWidget(QWidget):
172
209
  self.container.layout().addWidget(widget)
173
210
 
174
211
  @Property(bool)
175
- def compact(self):
176
- return self.compact_view.isVisible()
212
+ def compact_view(self):
213
+ return self.compact_label.isVisible()
177
214
 
178
- @compact.setter
179
- def compact(self, set_compact: bool):
215
+ @compact_view.setter
216
+ def compact_view(self, set_compact: bool):
180
217
  """Sets the compact form
181
218
 
182
219
  If set_compact is True, the compact view is displayed ; otherwise,
@@ -184,11 +221,11 @@ class CompactPopupWidget(QWidget):
184
221
  the container widget or the compact view widget.
185
222
  """
186
223
  if set_compact:
187
- self.compact_view.setVisible(True)
224
+ self.compact_view_widget.setVisible(True)
188
225
  self.container.setVisible(False)
189
226
  QWidget.setSizePolicy(self, QSizePolicy.Fixed, QSizePolicy.Fixed)
190
227
  else:
191
- self.compact_view.setVisible(False)
228
+ self.compact_view_widget.setVisible(False)
192
229
  self.container.setVisible(True)
193
230
  QWidget.setSizePolicy(self, self.container.sizePolicy())
194
231
  if self.parentWidget():
@@ -215,6 +252,14 @@ class CompactPopupWidget(QWidget):
215
252
  self.compact_label.setToolTip(tooltip)
216
253
  self.compact_status.setToolTip(tooltip)
217
254
 
255
+ @Property(bool)
256
+ def expand_popup(self):
257
+ return self._expand_popup
258
+
259
+ @expand_popup.setter
260
+ def expand_popup(self, popup: bool):
261
+ self._expand_popup = popup
262
+
218
263
  def closeEvent(self, event):
219
264
  # Called by Qt, on closing - since the children widgets can be
220
265
  # BECWidgets, it is good to explicitely call 'close' on them,
@@ -1,6 +1,6 @@
1
1
  from typing import TYPE_CHECKING
2
2
 
3
- from qtpy.QtCore import QSize
3
+ from qtpy.QtCore import QSize, Signal, Slot
4
4
  from qtpy.QtWidgets import QCompleter, QLineEdit, QSizePolicy
5
5
 
6
6
  from bec_widgets.utils.bec_widget import BECWidget
@@ -24,6 +24,8 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
24
24
  arg_name: Argument name, can be used for the other widgets which has to call some other function in bec using correct argument names.
25
25
  """
26
26
 
27
+ device_selected = Signal(str)
28
+
27
29
  ICON_NAME = "edit_note"
28
30
 
29
31
  def __init__(
@@ -54,6 +56,18 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
54
56
  self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
55
57
  self.setMinimumSize(QSize(100, 0))
56
58
 
59
+ self.editingFinished.connect(self.emit_device_selected)
60
+
61
+ @Slot()
62
+ def emit_device_selected(self):
63
+ """
64
+ Editing finished, let's see which device is selected and emit signal
65
+ """
66
+ device_name = self.text().lower()
67
+ device_obj = getattr(self.dev, device_name, None)
68
+ if device_obj is not None:
69
+ self.device_selected.emit(device_name)
70
+
57
71
  def set_device_filter(self, device_filter: str | list[str]):
58
72
  """
59
73
  Set the device filter.
@@ -14,6 +14,7 @@ from qtpy.QtCore import Property, Signal, Slot
14
14
  from qtpy.QtGui import QDoubleValidator
15
15
  from qtpy.QtWidgets import QDialog, QDoubleSpinBox, QPushButton, QVBoxLayout, QWidget
16
16
 
17
+ from bec_widgets.qt_utils.compact_popup import CompactPopupWidget
17
18
  from bec_widgets.utils import UILoader
18
19
  from bec_widgets.utils.bec_widget import BECWidget
19
20
  from bec_widgets.utils.colors import get_accent_colors, set_theme
@@ -24,7 +25,7 @@ logger = bec_logger.logger
24
25
  MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
25
26
 
26
27
 
27
- class PositionerBox(BECWidget, QWidget):
28
+ class PositionerBox(BECWidget, CompactPopupWidget):
28
29
  """Simple Widget to control a positioner in box form"""
29
30
 
30
31
  ui_file = "positioner_box.ui"
@@ -44,7 +45,7 @@ class PositionerBox(BECWidget, QWidget):
44
45
  device (Positioner): The device to control.
45
46
  """
46
47
  super().__init__(**kwargs)
47
- QWidget.__init__(self, parent=parent)
48
+ CompactPopupWidget.__init__(self, parent=parent, layout=QVBoxLayout)
48
49
  self.get_bec_shortcuts()
49
50
  self._device = ""
50
51
  self._limits = None
@@ -63,8 +64,7 @@ class PositionerBox(BECWidget, QWidget):
63
64
  current_path = os.path.dirname(__file__)
64
65
  self.ui = UILoader(self).loader(os.path.join(current_path, self.ui_file))
65
66
 
66
- self.layout = QVBoxLayout(self)
67
- self.layout.addWidget(self.ui)
67
+ self.addWidget(self.ui)
68
68
  self.layout.setSpacing(0)
69
69
  self.layout.setContentsMargins(0, 0, 0, 0)
70
70
 
@@ -135,8 +135,12 @@ class PositionerBox(BECWidget, QWidget):
135
135
  """Setter, checks if device is a string"""
136
136
  if not value or not isinstance(value, str):
137
137
  return
138
+ if not self._check_device_is_valid(value):
139
+ return
138
140
  old_device = self._device
139
141
  self._device = value
142
+ if not self.label:
143
+ self.label = value
140
144
  self.device_changed.emit(old_device, value)
141
145
 
142
146
  @Property(bool)
@@ -241,9 +245,11 @@ class PositionerBox(BECWidget, QWidget):
241
245
  if is_moving:
242
246
  self.ui.spinner_widget.start()
243
247
  self.ui.spinner_widget.setToolTip("Device is moving")
248
+ self.set_global_state("warning")
244
249
  else:
245
250
  self.ui.spinner_widget.stop()
246
251
  self.ui.spinner_widget.setToolTip("Device is idle")
252
+ self.set_global_state("success")
247
253
 
248
254
  if readback_val is not None:
249
255
  self.ui.readback.setText(f"{readback_val:.{precision}f}")
File without changes
@@ -0,0 +1,170 @@
1
+ """ Module for a PositionerGroup widget to control a positioner device."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from bec_lib.device import Positioner
6
+ from bec_lib.logger import bec_logger
7
+ from qtpy.QtCore import Property, QSize, Signal, Slot
8
+ from qtpy.QtWidgets import QGridLayout, QGroupBox, QSizePolicy, QVBoxLayout, QWidget
9
+
10
+ from bec_widgets.utils.bec_widget import BECWidget
11
+ from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox
12
+
13
+ logger = bec_logger.logger
14
+
15
+
16
+ class PositionerGroupBox(QGroupBox):
17
+
18
+ position_update = Signal(float)
19
+
20
+ def __init__(self, parent, dev_name):
21
+ super().__init__(parent)
22
+
23
+ self.device_name = dev_name
24
+
25
+ QVBoxLayout(self)
26
+ self.layout().setContentsMargins(0, 0, 0, 0)
27
+ self.layout().setSpacing(0)
28
+ self.widget = PositionerBox(self, dev_name)
29
+ self.widget.compact_view = True
30
+ self.widget.expand_popup = False
31
+ self.layout().addWidget(self.widget)
32
+ self.widget.position_update.connect(self._on_position_update)
33
+ self.widget.expand.connect(self._on_expand)
34
+ self.setTitle(self.device_name)
35
+ self.widget.init_device() # force readback
36
+
37
+ def _on_expand(self, expand):
38
+ if expand:
39
+ self.setTitle("")
40
+ self.setFlat(True)
41
+ else:
42
+ self.setTitle(self.device_name)
43
+ self.setFlat(False)
44
+
45
+ def _on_position_update(self, pos: float):
46
+ self.position_update.emit(pos)
47
+ self.widget.label = f"%.{self.widget.dev[self.widget.device].precision}f" % pos
48
+
49
+ def close(self):
50
+ self.widget.close()
51
+ super().close()
52
+
53
+
54
+ class PositionerGroup(BECWidget, QWidget):
55
+ """Simple Widget to control a positioner in box form"""
56
+
57
+ ICON_NAME = "grid_view"
58
+ USER_ACCESS = ["set_positioners"]
59
+
60
+ # Signal emitted to inform listeners about a position update of the first positioner
61
+ position_update = Signal(float)
62
+ # Signal emitted to inform listeners about (positioner, pos) updates
63
+ device_position_update = Signal(str, float)
64
+
65
+ def __init__(self, parent=None, **kwargs):
66
+ """Initialize the widget.
67
+
68
+ Args:
69
+ parent: The parent widget.
70
+ """
71
+ super().__init__(**kwargs)
72
+ QWidget.__init__(self, parent)
73
+
74
+ self.get_bec_shortcuts()
75
+
76
+ QGridLayout(self)
77
+ self.layout().setContentsMargins(0, 0, 0, 0)
78
+
79
+ self._device_widgets = {}
80
+ self._grid_ncols = 2
81
+
82
+ def minimumSizeHint(self):
83
+ return QSize(300, 30)
84
+
85
+ @Slot(str)
86
+ def set_positioners(self, device_names: str):
87
+ """Redraw grid with positioners from device_names string
88
+
89
+ Device names must be separated by space
90
+ """
91
+ devs = device_names.split()
92
+ for dev_name in devs:
93
+ if not self._check_device_is_valid(dev_name):
94
+ raise ValueError(f"{dev_name} is not a valid Positioner")
95
+ for i, existing_widget in enumerate(self._device_widgets.values()):
96
+ self.layout().removeWidget(existing_widget)
97
+ existing_widget.position_update.disconnect(self._on_position_update)
98
+ if i == 0:
99
+ existing_widget.position_update.disconnect(self.position_update)
100
+ for i, dev_name in enumerate(devs):
101
+ widget = self._device_widgets.get(dev_name)
102
+ if widget is None:
103
+ widget = PositionerGroupBox(self, dev_name)
104
+ self._device_widgets[dev_name] = widget
105
+ widget.position_update.connect(self._on_position_update)
106
+ if i == 0:
107
+ # only emit 'position_update' for the first positioner in grid
108
+ widget.position_update.connect(self.position_update)
109
+ self.layout().addWidget(widget, i // self._grid_ncols, i % self._grid_ncols)
110
+ to_remove = set(self._device_widgets) - set(devs)
111
+ for dev_name in to_remove:
112
+ self._device_widgets[dev_name].close()
113
+ del self._device_widgets[dev_name]
114
+
115
+ def _check_device_is_valid(self, device: str):
116
+ """Check if the device is a positioner
117
+
118
+ Args:
119
+ device (str): The device name
120
+ """
121
+ if device not in self.dev:
122
+ logger.info(f"Device {device} not found in the device list")
123
+ return False
124
+ if not isinstance(self.dev[device], Positioner):
125
+ logger.info(f"Device {device} is not a positioner")
126
+ return False
127
+ return True
128
+
129
+ def _on_position_update(self, pos: float):
130
+ widget = self.sender()
131
+ self.device_position_update.emit(widget.title(), pos)
132
+
133
+ @Property(str)
134
+ def devices_list(self):
135
+ """Device names string separated by space"""
136
+ return " ".join(self._device_widgets)
137
+
138
+ @devices_list.setter
139
+ def devices_list(self, device_names: str):
140
+ """Set devices list from device names string separated by space"""
141
+ devs = device_names.split()
142
+ for dev_name in devs:
143
+ if not self._check_device_is_valid(dev_name):
144
+ return
145
+ self.set_positioners(device_names)
146
+
147
+ @Property(int)
148
+ def grid_max_cols(self):
149
+ """Max number of columns for widgets grid"""
150
+ return self._grid_ncols
151
+
152
+ @grid_max_cols.setter
153
+ def grid_max_cols(self, ncols: int):
154
+ """Set max number of columns for widgets grid"""
155
+ self._grid_ncols = ncols
156
+ self.set_positioners(self.devices_list)
157
+
158
+
159
+ if __name__ == "__main__": # pragma: no cover
160
+ import sys
161
+
162
+ from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
163
+
164
+ app = QApplication(sys.argv)
165
+ widget = PositionerGroup()
166
+ widget.grid_max_cols = 3
167
+ widget.set_positioners("samx samy samz")
168
+
169
+ widget.show()
170
+ sys.exit(app.exec_())
@@ -0,0 +1 @@
1
+ {'files': ['positioner_group.py']}
@@ -0,0 +1,57 @@
1
+ # Copyright (C) 2022 The Qt Company Ltd.
2
+ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
3
+
4
+ import os
5
+
6
+ from qtpy.QtDesigner import QDesignerCustomWidgetInterface
7
+
8
+ from bec_widgets.utils.bec_designer import designer_material_icon
9
+ from bec_widgets.widgets.positioner_group.positioner_group import PositionerGroup
10
+
11
+ DOM_XML = """
12
+ <ui language='c++'>
13
+ <widget class='PositionerGroup' name='positioner_group'>
14
+ </widget>
15
+ </ui>
16
+ """
17
+ MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
18
+
19
+
20
+ class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
21
+ def __init__(self):
22
+ super().__init__()
23
+ self._form_editor = None
24
+
25
+ def createWidget(self, parent):
26
+ t = PositionerGroup(parent)
27
+ return t
28
+
29
+ def domXml(self):
30
+ return DOM_XML
31
+
32
+ def group(self):
33
+ return "Device Control"
34
+
35
+ def icon(self):
36
+ return designer_material_icon(PositionerGroup.ICON_NAME)
37
+
38
+ def includeFile(self):
39
+ return "positioner_group"
40
+
41
+ def initialize(self, form_editor):
42
+ self._form_editor = form_editor
43
+
44
+ def isContainer(self):
45
+ return False
46
+
47
+ def isInitialized(self):
48
+ return self._form_editor is not None
49
+
50
+ def name(self):
51
+ return "PositionerGroup"
52
+
53
+ def toolTip(self):
54
+ return "Container Widget to control positioners in compact form, in a grid"
55
+
56
+ def whatsThis(self):
57
+ return self.toolTip()
@@ -0,0 +1,15 @@
1
+ def main(): # pragma: no cover
2
+ from qtpy import PYSIDE6
3
+
4
+ if not PYSIDE6:
5
+ print("PYSIDE6 is not available in the environment. Cannot patch designer.")
6
+ return
7
+ from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
8
+
9
+ from bec_widgets.widgets.positioner_group.positioner_group_plugin import PositionerGroupPlugin
10
+
11
+ QPyDesignerCustomWidgetCollection.addCustomWidget(PositionerGroupPlugin())
12
+
13
+
14
+ if __name__ == "__main__": # pragma: no cover
15
+ main()