bec-widgets 2.16.1__py3-none-any.whl → 2.17.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,77 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v2.17.0 (2025-06-22)
5
+
6
+ ### Bug Fixes
7
+
8
+ - **bec_progressbar**: Layout and sizing adjustments
9
+ ([`b02c870`](https://github.com/bec-project/bec_widgets/commit/b02c870dbfecb4bc6921ec4c915dac0e67beb9b4))
10
+
11
+ - **launch_window**: Number of remaining connections increase to 2 to include the ScanProgressBar
12
+ ([`3bbb8da`](https://github.com/bec-project/bec_widgets/commit/3bbb8daa24348613f62bde667a446d37dcec8fb0))
13
+
14
+ - **main_window**: Labels and sizing of scan progress adopted
15
+ ([`aca6efb`](https://github.com/bec-project/bec_widgets/commit/aca6efb567528eb3c68521a59b4f9903a5616c6f))
16
+
17
+ - **scan_progressbar**: Cleanup adjusted
18
+ ([`e8ae972`](https://github.com/bec-project/bec_widgets/commit/e8ae9725fa86b7db52a147ca5a2acc62fa2ccf43))
19
+
20
+ - **scan_progressbar**: Mapping of bec progress states to the progressbar enums
21
+ ([`88b42e4`](https://github.com/bec-project/bec_widgets/commit/88b42e49e30a0aa0edc2de4d970408f4be5bde6b))
22
+
23
+ ### Build System
24
+
25
+ - Update min dependency of bec to 3.42.4
26
+ ([`a4274ff`](https://github.com/bec-project/bec_widgets/commit/a4274ff8cd9f3e73a61b2eaf902c172c028d21b0))
27
+
28
+ ### Features
29
+
30
+ - **main_window**: Added scan progress bar to BECMainWindow status bar
31
+ ([`497e394`](https://github.com/bec-project/bec_widgets/commit/497e394deb5cfe36c8fc4f769fef26f109fd1c1f))
32
+
33
+ - **main_window**: Timer to show hide scan progress when it is relevant only
34
+ ([`9ff1706`](https://github.com/bec-project/bec_widgets/commit/9ff170660edd9e03f99eccee60b5e20fc1cf5a8d))
35
+
36
+ - **progressbar**: Added padding as designer property
37
+ ([`a451625`](https://github.com/bec-project/bec_widgets/commit/a451625a5ab804ca8259f9c9f83c4f9ebbea4a5b))
38
+
39
+ - **progressbar**: State setting and dynamic corner radius
40
+ ([`d3a9e09`](https://github.com/bec-project/bec_widgets/commit/d3a9e0903a263d735ecab3a2ad9319c9d5e86092))
41
+
42
+ - **scan_progressbar**: Added oneline design for compact applications
43
+ ([`d5ca7b8`](https://github.com/bec-project/bec_widgets/commit/d5ca7b84337cf60aa66f961d357ae66994f53c7a))
44
+
45
+ - **scan_progressbar**: Added progressbar with hooks to scan progress and device progress
46
+ ([`c4b8538`](https://github.com/bec-project/bec_widgets/commit/c4b85381a41e4742567680864668ee83d498b1d1))
47
+
48
+ ### Refactoring
49
+
50
+ - **progressbar**: Change slot / property to safeslot / safeproperty
51
+ ([`92d0ffe`](https://github.com/bec-project/bec_widgets/commit/92d0ffee65babc718fafd60131d0a4f291e5ca2b))
52
+
53
+ ### Testing
54
+
55
+ - **scan progress**: Add test for queue update logic
56
+ ([`b2a46e2`](https://github.com/bec-project/bec_widgets/commit/b2a46e284d45e97dd9853d1a3c8e95de7e530267))
57
+
58
+ - **scan_progress**: Tests extended
59
+ ([`6c04eac`](https://github.com/bec-project/bec_widgets/commit/6c04eac18c887526b333f58fc1118c3b4029abd8))
60
+
61
+
62
+ ## v2.16.2 (2025-06-20)
63
+
64
+ ### Bug Fixes
65
+
66
+ - **waveform**: Asyncsignal are handled with the same update mechanism as async readback
67
+ ([`a3ffcef`](https://github.com/bec-project/bec_widgets/commit/a3ffcefe8085fa1a88d679f8ef6adfdff786492e))
68
+
69
+ ### Testing
70
+
71
+ - **utils**: Dmmock can fetch get_bec_signals method
72
+ ([`3146d98`](https://github.com/bec-project/bec_widgets/commit/3146d98c572ff2bb8ab77f71b75d9612e364ffe0))
73
+
74
+
4
75
  ## v2.16.1 (2025-06-20)
5
76
 
6
77
  ### Bug Fixes
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 2.16.1
3
+ Version: 2.17.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
@@ -9,8 +9,8 @@ Classifier: Development Status :: 3 - Alpha
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Topic :: Scientific/Engineering
11
11
  Requires-Python: >=3.10
12
- Requires-Dist: bec-ipython-client<=4.0,>=3.38
13
- Requires-Dist: bec-lib<=4.0,>=3.38
12
+ Requires-Dist: bec-ipython-client<=4.0,>=3.42.4
13
+ Requires-Dist: bec-lib<=4.0,>=3.42.4
14
14
  Requires-Dist: bec-qthemes>=0.7,~=0.7
15
15
  Requires-Dist: black~=25.0
16
16
  Requires-Dist: isort>=5.13.2,~=5.13
@@ -542,7 +542,7 @@ class LaunchWindow(BECMainWindow):
542
542
  remaining_connections = [
543
543
  connection for connection in connections.values() if connection.parent_id != self.gui_id
544
544
  ]
545
- return len(remaining_connections) <= 1
545
+ return len(remaining_connections) <= 2
546
546
 
547
547
  def _turn_off_the_lights(self, connections: dict):
548
548
  """
bec_widgets/cli/client.py CHANGED
@@ -474,6 +474,20 @@ class BECProgressBar(RPCBase):
474
474
  >>> progressbar.label_template = "$value / $percentage %"
475
475
  """
476
476
 
477
+ @property
478
+ @rpc_call
479
+ def state(self):
480
+ """
481
+ None
482
+ """
483
+
484
+ @state.setter
485
+ @rpc_call
486
+ def state(self):
487
+ """
488
+ None
489
+ """
490
+
477
491
  @rpc_call
478
492
  def _get_label(self) -> str:
479
493
  """
@@ -3245,6 +3259,16 @@ class ScanControl(RPCBase):
3245
3259
  """
3246
3260
 
3247
3261
 
3262
+ class ScanProgressBar(RPCBase):
3263
+ """Widget to display a progress bar that is hooked up to the scan progress of a scan."""
3264
+
3265
+ @rpc_call
3266
+ def remove(self):
3267
+ """
3268
+ Cleanup the BECConnector
3269
+ """
3270
+
3271
+
3248
3272
  class ScatterCurve(RPCBase):
3249
3273
  """Scatter curve item for the scatter waveform widget."""
3250
3274
 
@@ -210,6 +210,39 @@ class DMMock:
210
210
  for device in devices:
211
211
  self.devices[device.name] = device
212
212
 
213
+ def get_bec_signals(self, signal_class_name: str):
214
+ """
215
+ Emulate DeviceManager.get_bec_signals for unit-tests.
216
+
217
+ For “AsyncSignal” we list every device whose readout_priority is
218
+ ReadoutPriority.ASYNC and build a minimal tuple
219
+ (device_name, signal_name, signal_info_dict) that matches the real
220
+ API shape used by Waveform._check_async_signal_found.
221
+ """
222
+ signals: list[tuple[str, str, dict]] = []
223
+ if signal_class_name != "AsyncSignal":
224
+ return signals
225
+
226
+ for device in self.devices.values():
227
+ if getattr(device, "readout_priority", None) == ReadoutPriority.ASYNC:
228
+ device_name = device.name
229
+ signal_name = device.name # primary signal in our mocks
230
+ signal_info = {
231
+ "component_name": signal_name,
232
+ "obj_name": signal_name,
233
+ "kind_str": "hinted",
234
+ "signal_class": signal_class_name,
235
+ "metadata": {
236
+ "connected": True,
237
+ "precision": None,
238
+ "read_access": True,
239
+ "timestamp": 0.0,
240
+ "write_access": True,
241
+ },
242
+ }
243
+ signals.append((device_name, signal_name, signal_info))
244
+ return signals
245
+
213
246
 
214
247
  DEVICES = [
215
248
  FakePositioner("samx", limits=[-10, 10], read_value=2.0),
@@ -1,9 +1,28 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
 
3
5
  from bec_lib.endpoints import MessageEndpoints
4
- from qtpy.QtCore import QEvent, QSize, Qt, QTimer
6
+ from qtpy.QtCore import (
7
+ QAbstractAnimation,
8
+ QEasingCurve,
9
+ QEvent,
10
+ QPropertyAnimation,
11
+ QSize,
12
+ Qt,
13
+ QTimer,
14
+ )
5
15
  from qtpy.QtGui import QAction, QActionGroup, QIcon
6
- from qtpy.QtWidgets import QApplication, QFrame, QLabel, QMainWindow, QStyle, QVBoxLayout, QWidget
16
+ from qtpy.QtWidgets import (
17
+ QApplication,
18
+ QFrame,
19
+ QHBoxLayout,
20
+ QLabel,
21
+ QMainWindow,
22
+ QStyle,
23
+ QVBoxLayout,
24
+ QWidget,
25
+ )
7
26
 
8
27
  import bec_widgets
9
28
  from bec_widgets.utils import UILoader
@@ -13,6 +32,7 @@ from bec_widgets.utils.error_popups import SafeSlot
13
32
  from bec_widgets.utils.widget_io import WidgetHierarchy
14
33
  from bec_widgets.widgets.containers.main_window.addons.scroll_label import ScrollLabel
15
34
  from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
35
+ from bec_widgets.widgets.progress.scan_progressbar.scan_progressbar import ScanProgressBar
16
36
 
17
37
  MODULE_PATH = os.path.dirname(bec_widgets.__file__)
18
38
 
@@ -20,6 +40,8 @@ MODULE_PATH = os.path.dirname(bec_widgets.__file__)
20
40
  class BECMainWindow(BECWidget, QMainWindow):
21
41
  RPC = False
22
42
  PLUGIN = False
43
+ SCAN_PROGRESS_WIDTH = 100 # px
44
+ STATUS_BAR_WIDGETS_EXPIRE_TIME = 60_000 # milliseconds
23
45
 
24
46
  def __init__(
25
47
  self,
@@ -33,6 +55,7 @@ class BECMainWindow(BECWidget, QMainWindow):
33
55
  super().__init__(parent=parent, gui_id=gui_id, **kwargs)
34
56
 
35
57
  self.app = QApplication.instance()
58
+ self.status_bar = self.statusBar()
36
59
  self.setWindowTitle(window_title)
37
60
  self._init_ui()
38
61
  self._connect_to_theme_change()
@@ -61,14 +84,13 @@ class BECMainWindow(BECWidget, QMainWindow):
61
84
  """
62
85
  Prepare the BEC specific widgets in the status bar.
63
86
  """
64
- status_bar = self.statusBar()
65
87
 
66
88
  # Left: App‑ID label
67
89
  self._app_id_label = QLabel()
68
90
  self._app_id_label.setAlignment(
69
91
  Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
70
92
  )
71
- status_bar.addWidget(self._app_id_label)
93
+ self.status_bar.addWidget(self._app_id_label)
72
94
 
73
95
  # Add a separator after the app ID label
74
96
  self._add_separator()
@@ -78,16 +100,100 @@ class BECMainWindow(BECWidget, QMainWindow):
78
100
  self._client_info_label.setAlignment(
79
101
  Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
80
102
  )
81
- status_bar.addWidget(self._client_info_label, 1)
103
+ self.status_bar.addWidget(self._client_info_label, 1)
82
104
 
83
105
  # Timer to automatically clear client messages once they expire
84
106
  self._client_info_expire_timer = QTimer(self)
85
107
  self._client_info_expire_timer.setSingleShot(True)
86
108
  self._client_info_expire_timer.timeout.connect(lambda: self._client_info_label.setText(""))
87
109
 
88
- def _add_separator(self):
110
+ # Add scan_progress bar with display logic
111
+ self._add_scan_progress_bar()
112
+
113
+ ################################################################################
114
+ # Progress‑bar helpers
115
+ def _add_scan_progress_bar(self):
116
+
117
+ # --- Progress bar -------------------------------------------------
118
+ # Scan progress bar minimalistic design setup
119
+ self._scan_progress_bar = ScanProgressBar(self, one_line_design=True)
120
+ self._scan_progress_bar.show_elapsed_time = False
121
+ self._scan_progress_bar.show_remaining_time = False
122
+ self._scan_progress_bar.show_source_label = False
123
+ self._scan_progress_bar.progressbar.label_template = ""
124
+ self._scan_progress_bar.progressbar.setFixedHeight(8)
125
+ self._scan_progress_bar.progressbar.setFixedWidth(80)
126
+
127
+ # Bundle the progress bar with a separator
128
+ separator = self._add_separator(separate_object=True)
129
+ self._scan_progress_bar_with_separator = QWidget()
130
+ self._scan_progress_bar_with_separator.layout = QHBoxLayout(
131
+ self._scan_progress_bar_with_separator
132
+ )
133
+ self._scan_progress_bar_with_separator.layout.setContentsMargins(0, 0, 0, 0)
134
+ self._scan_progress_bar_with_separator.layout.setSpacing(0)
135
+ self._scan_progress_bar_with_separator.layout.addWidget(separator)
136
+ self._scan_progress_bar_with_separator.layout.addWidget(self._scan_progress_bar)
137
+
138
+ # Set Size
139
+ self._scan_progress_bar_target_width = self.SCAN_PROGRESS_WIDTH
140
+ self._scan_progress_bar_with_separator.setMaximumWidth(self._scan_progress_bar_target_width)
141
+
142
+ self.status_bar.addWidget(self._scan_progress_bar_with_separator)
143
+
144
+ # Visibility logic
145
+ self._scan_progress_bar_with_separator.hide()
146
+ self._scan_progress_bar_with_separator.setMaximumWidth(0)
147
+
148
+ # Timer for hiding logic
149
+ self._scan_progress_hide_timer = QTimer(self)
150
+ self._scan_progress_hide_timer.setSingleShot(True)
151
+ self._scan_progress_hide_timer.setInterval(self.STATUS_BAR_WIDGETS_EXPIRE_TIME)
152
+ self._scan_progress_hide_timer.timeout.connect(self._animate_hide_scan_progress_bar)
153
+
154
+ # Show / hide behaviour
155
+ self._scan_progress_bar.progress_started.connect(self._show_scan_progress_bar)
156
+ self._scan_progress_bar.progress_finished.connect(self._delay_hide_scan_progress_bar)
157
+
158
+ def _show_scan_progress_bar(self):
159
+ if self._scan_progress_hide_timer.isActive():
160
+ self._scan_progress_hide_timer.stop()
161
+ if self._scan_progress_bar_with_separator.isVisible():
162
+ return
163
+
164
+ # Make visible and reset width
165
+ self._scan_progress_bar_with_separator.show()
166
+ self._scan_progress_bar_with_separator.setMaximumWidth(0)
167
+
168
+ self._show_container_anim = QPropertyAnimation(
169
+ self._scan_progress_bar_with_separator, b"maximumWidth", self
170
+ )
171
+ self._show_container_anim.setDuration(300)
172
+ self._show_container_anim.setStartValue(0)
173
+ self._show_container_anim.setEndValue(self._scan_progress_bar_target_width)
174
+ self._show_container_anim.setEasingCurve(QEasingCurve.OutCubic)
175
+ self._show_container_anim.start()
176
+
177
+ def _delay_hide_scan_progress_bar(self):
178
+ """Start the countdown to hide the scan progress bar."""
179
+ if hasattr(self, "_scan_progress_hide_timer"):
180
+ self._scan_progress_hide_timer.start()
181
+
182
+ def _animate_hide_scan_progress_bar(self):
183
+ """Shrink container to the right, then hide."""
184
+ self._hide_container_anim = QPropertyAnimation(
185
+ self._scan_progress_bar_with_separator, b"maximumWidth", self
186
+ )
187
+ self._hide_container_anim.setDuration(300)
188
+ self._hide_container_anim.setStartValue(self._scan_progress_bar_with_separator.width())
189
+ self._hide_container_anim.setEndValue(0)
190
+ self._hide_container_anim.setEasingCurve(QEasingCurve.InCubic)
191
+ self._hide_container_anim.finished.connect(self._scan_progress_bar_with_separator.hide)
192
+ self._hide_container_anim.start()
193
+
194
+ def _add_separator(self, separate_object: bool = False) -> QWidget | None:
89
195
  """
90
- Add a vertically centred separator to the status bar.
196
+ Add a vertically centred separator to the status bar or just return it as a separate object.
91
197
  """
92
198
  status_bar = self.statusBar()
93
199
 
@@ -106,6 +212,8 @@ class BECMainWindow(BECWidget, QMainWindow):
106
212
  vbox.addStretch()
107
213
  wrapper.setFixedWidth(line.sizeHint().width())
108
214
 
215
+ if separate_object:
216
+ return wrapper
109
217
  status_bar.addWidget(wrapper)
110
218
 
111
219
  def _init_bec_icon(self):
@@ -279,10 +387,16 @@ class BECMainWindow(BECWidget, QMainWindow):
279
387
  child.close()
280
388
  child.deleteLater()
281
389
 
390
+ # Timer cleanup
282
391
  if hasattr(self, "_client_info_expire_timer") and self._client_info_expire_timer.isActive():
283
392
  self._client_info_expire_timer.stop()
393
+ if hasattr(self, "_scan_progress_hide_timer") and self._scan_progress_hide_timer.isActive():
394
+ self._scan_progress_hide_timer.stop()
395
+
284
396
  # Status bar widgets cleanup
285
397
  self._client_info_label.cleanup()
398
+ self._scan_progress_bar.close()
399
+ self._scan_progress_bar.deleteLater()
286
400
  super().cleanup()
287
401
 
288
402
 
@@ -296,4 +410,5 @@ if __name__ == "__main__":
296
410
  app = QApplication(sys.argv)
297
411
  main_window = UILaunchWindow()
298
412
  main_window.show()
413
+ main_window.resize(800, 600)
299
414
  sys.exit(app.exec())
@@ -1246,6 +1246,23 @@ class Waveform(PlotBase):
1246
1246
 
1247
1247
  self.request_dap_update.emit()
1248
1248
 
1249
+ def _check_async_signal_found(self, name: str, signal: str) -> bool:
1250
+ """
1251
+ Check if the async signal is found in the BEC device manager.
1252
+
1253
+ Args:
1254
+ name(str): The name of the async signal.
1255
+ signal(str): The entry of the async signal.
1256
+
1257
+ Returns:
1258
+ bool: True if the async signal is found, False otherwise.
1259
+ """
1260
+ bec_async_signals = self.client.device_manager.get_bec_signals("AsyncSignal")
1261
+ for entry_name, _, entry_data in bec_async_signals:
1262
+ if entry_name == name and entry_data.get("obj_name") == signal:
1263
+ return True
1264
+ return False
1265
+
1249
1266
  def _setup_async_curve(self, curve: Curve):
1250
1267
  """
1251
1268
  Setup async curve.
@@ -1254,20 +1271,40 @@ class Waveform(PlotBase):
1254
1271
  curve(Curve): The curve to set up.
1255
1272
  """
1256
1273
  name = curve.config.signal.name
1257
- self.bec_dispatcher.disconnect_slot(
1258
- self.on_async_readback, MessageEndpoints.device_async_readback(self.old_scan_id, name)
1259
- )
1274
+ signal = curve.config.signal.entry
1275
+ async_signal_found = self._check_async_signal_found(name, signal)
1276
+
1260
1277
  try:
1261
1278
  curve.clear_data()
1262
1279
  except KeyError:
1263
1280
  logger.warning(f"Curve {name} not found in plot item.")
1264
1281
  pass
1265
- self.bec_dispatcher.connect_slot(
1266
- self.on_async_readback,
1267
- MessageEndpoints.device_async_readback(self.scan_id, name),
1268
- from_start=True,
1269
- cb_info={"scan_id": self.scan_id},
1270
- )
1282
+
1283
+ # New endpoint for async signals
1284
+ if async_signal_found:
1285
+ self.bec_dispatcher.disconnect_slot(
1286
+ self.on_async_readback,
1287
+ MessageEndpoints.device_async_signal(self.old_scan_id, name, signal),
1288
+ )
1289
+ self.bec_dispatcher.connect_slot(
1290
+ self.on_async_readback,
1291
+ MessageEndpoints.device_async_signal(self.scan_id, name, signal),
1292
+ from_start=True,
1293
+ cb_info={"scan_id": self.scan_id},
1294
+ )
1295
+
1296
+ # old endpoint
1297
+ else:
1298
+ self.bec_dispatcher.disconnect_slot(
1299
+ self.on_async_readback,
1300
+ MessageEndpoints.device_async_readback(self.old_scan_id, name),
1301
+ )
1302
+ self.bec_dispatcher.connect_slot(
1303
+ self.on_async_readback,
1304
+ MessageEndpoints.device_async_readback(self.scan_id, name),
1305
+ from_start=True,
1306
+ cb_info={"scan_id": self.scan_id},
1307
+ )
1271
1308
  logger.info(f"Setup async curve {name}")
1272
1309
 
1273
1310
  @SafeSlot(dict, dict, verify_sender=True)