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 +71 -0
- PKG-INFO +3 -3
- bec_widgets/applications/launch_window.py +1 -1
- bec_widgets/cli/client.py +24 -0
- bec_widgets/tests/utils.py +33 -0
- bec_widgets/widgets/containers/main_window/main_window.py +122 -7
- bec_widgets/widgets/plots/waveform/waveform.py +46 -9
- bec_widgets/widgets/progress/bec_progressbar/bec_progressbar.py +135 -21
- bec_widgets/widgets/progress/scan_progressbar/__init__.py +0 -0
- bec_widgets/widgets/progress/scan_progressbar/register_scan_progress_bar.py +17 -0
- bec_widgets/widgets/progress/scan_progressbar/scan_progress_bar.pyproject +1 -0
- bec_widgets/widgets/progress/scan_progressbar/scan_progress_bar_plugin.py +54 -0
- bec_widgets/widgets/progress/scan_progressbar/scan_progressbar.py +320 -0
- bec_widgets/widgets/progress/scan_progressbar/scan_progressbar.ui +141 -0
- bec_widgets/widgets/progress/scan_progressbar/scan_progressbar_one_line.ui +124 -0
- {bec_widgets-2.16.1.dist-info → bec_widgets-2.17.0.dist-info}/METADATA +3 -3
- {bec_widgets-2.16.1.dist-info → bec_widgets-2.17.0.dist-info}/RECORD +21 -14
- pyproject.toml +6 -6
- {bec_widgets-2.16.1.dist-info → bec_widgets-2.17.0.dist-info}/WHEEL +0 -0
- {bec_widgets-2.16.1.dist-info → bec_widgets-2.17.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-2.16.1.dist-info → bec_widgets-2.17.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,12 +1,46 @@
|
|
1
1
|
import sys
|
2
|
+
from enum import Enum
|
2
3
|
from string import Template
|
3
4
|
|
4
|
-
from qtpy.QtCore import
|
5
|
+
from qtpy.QtCore import QEasingCurve, QPropertyAnimation, QRectF, Qt, QTimer
|
5
6
|
from qtpy.QtGui import QColor, QPainter, QPainterPath
|
7
|
+
|
8
|
+
|
9
|
+
class ProgressState(Enum):
|
10
|
+
NORMAL = "normal"
|
11
|
+
PAUSED = "paused"
|
12
|
+
INTERRUPTED = "interrupted"
|
13
|
+
COMPLETED = "completed"
|
14
|
+
|
15
|
+
@classmethod
|
16
|
+
def from_bec_status(cls, status: str) -> "ProgressState":
|
17
|
+
"""
|
18
|
+
Map a BEC status string (open, paused, aborted, halted, closed)
|
19
|
+
to the corresponding ProgressState.
|
20
|
+
Any unknown status falls back to NORMAL.
|
21
|
+
"""
|
22
|
+
mapping = {
|
23
|
+
"open": cls.NORMAL,
|
24
|
+
"paused": cls.PAUSED,
|
25
|
+
"aborted": cls.INTERRUPTED,
|
26
|
+
"halted": cls.PAUSED,
|
27
|
+
"closed": cls.COMPLETED,
|
28
|
+
}
|
29
|
+
return mapping.get(status.lower(), cls.NORMAL)
|
30
|
+
|
31
|
+
|
32
|
+
PROGRESS_STATE_COLORS = {
|
33
|
+
ProgressState.NORMAL: QColor("#2979ff"), # blue – normal progress
|
34
|
+
ProgressState.PAUSED: QColor("#ffca28"), # orange/amber – paused
|
35
|
+
ProgressState.INTERRUPTED: QColor("#ff5252"), # red – interrupted
|
36
|
+
ProgressState.COMPLETED: QColor("#00e676"), # green – finished
|
37
|
+
}
|
38
|
+
|
6
39
|
from qtpy.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
|
7
40
|
|
8
41
|
from bec_widgets.utils.bec_widget import BECWidget
|
9
42
|
from bec_widgets.utils.colors import get_accent_colors
|
43
|
+
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
10
44
|
|
11
45
|
|
12
46
|
class BECProgressBar(BECWidget, QWidget):
|
@@ -21,6 +55,8 @@ class BECProgressBar(BECWidget, QWidget):
|
|
21
55
|
"set_minimum",
|
22
56
|
"label_template",
|
23
57
|
"label_template.setter",
|
58
|
+
"state",
|
59
|
+
"state.setter",
|
24
60
|
"_get_label",
|
25
61
|
]
|
26
62
|
ICON_NAME = "page_control"
|
@@ -48,27 +84,38 @@ class BECProgressBar(BECWidget, QWidget):
|
|
48
84
|
|
49
85
|
self._completed_color = accent_colors.success
|
50
86
|
self._border_color = QColor(50, 50, 50)
|
87
|
+
# Corner‑rounding: base radius in pixels (auto‑reduced if bar is small)
|
88
|
+
self._corner_radius = 10
|
89
|
+
|
90
|
+
# Progress‑bar state handling
|
91
|
+
self._state = ProgressState.NORMAL
|
92
|
+
self._state_colors = dict(PROGRESS_STATE_COLORS)
|
51
93
|
|
52
94
|
# layout settings
|
95
|
+
self._padding_left_right = 10
|
53
96
|
self._value_animation = QPropertyAnimation(self, b"_progressbar_value")
|
54
97
|
self._value_animation.setDuration(200)
|
55
98
|
self._value_animation.setEasingCurve(QEasingCurve.Type.OutCubic)
|
56
99
|
|
57
100
|
# label on top of the progress bar
|
58
101
|
self.center_label = QLabel(self)
|
59
|
-
self.center_label.setAlignment(Qt.
|
102
|
+
self.center_label.setAlignment(Qt.AlignHCenter)
|
60
103
|
self.center_label.setStyleSheet("color: white;")
|
61
104
|
self.center_label.setMinimumSize(0, 0)
|
62
105
|
|
63
106
|
layout = QVBoxLayout(self)
|
64
|
-
layout.setContentsMargins(
|
107
|
+
layout.setContentsMargins(10, 0, 10, 0)
|
65
108
|
layout.setSpacing(0)
|
66
109
|
layout.addWidget(self.center_label)
|
110
|
+
layout.setAlignment(self.center_label, Qt.AlignCenter)
|
67
111
|
self.setLayout(layout)
|
68
112
|
|
69
113
|
self.update()
|
114
|
+
self._adjust_label_width()
|
70
115
|
|
71
|
-
@
|
116
|
+
@SafeProperty(
|
117
|
+
str, doc="The template for the center label. Use $value, $maximum, and $percentage."
|
118
|
+
)
|
72
119
|
def label_template(self):
|
73
120
|
"""
|
74
121
|
The template for the center label. Use $value, $maximum, and $percentage to insert the values.
|
@@ -83,10 +130,11 @@ class BECProgressBar(BECWidget, QWidget):
|
|
83
130
|
@label_template.setter
|
84
131
|
def label_template(self, template):
|
85
132
|
self._label_template = template
|
133
|
+
self._adjust_label_width()
|
86
134
|
self.set_value(self._user_value)
|
87
135
|
self.update()
|
88
136
|
|
89
|
-
@
|
137
|
+
@SafeProperty(float, designable=False)
|
90
138
|
def _progressbar_value(self):
|
91
139
|
"""
|
92
140
|
The current value of the progress bar.
|
@@ -106,8 +154,20 @@ class BECProgressBar(BECWidget, QWidget):
|
|
106
154
|
percentage=int((self.map_value(self._user_value) / self._maximum) * 100),
|
107
155
|
)
|
108
156
|
|
109
|
-
|
110
|
-
|
157
|
+
def _adjust_label_width(self):
|
158
|
+
"""
|
159
|
+
Reserve enough horizontal space for the center label so the widget
|
160
|
+
doesn't resize as the text grows during progress.
|
161
|
+
"""
|
162
|
+
template = Template(self._label_template)
|
163
|
+
sample_text = template.safe_substitute(
|
164
|
+
value=self._user_maximum, maximum=self._user_maximum, percentage=100
|
165
|
+
)
|
166
|
+
width = self.center_label.fontMetrics().horizontalAdvance(sample_text)
|
167
|
+
self.center_label.setFixedWidth(width)
|
168
|
+
|
169
|
+
@SafeSlot(float)
|
170
|
+
@SafeSlot(int)
|
111
171
|
def set_value(self, value):
|
112
172
|
"""
|
113
173
|
Set the value of the progress bar.
|
@@ -122,35 +182,88 @@ class BECProgressBar(BECWidget, QWidget):
|
|
122
182
|
self._target_value = self.map_value(value)
|
123
183
|
self._user_value = value
|
124
184
|
self.center_label.setText(self._update_template())
|
185
|
+
# Update state automatically unless paused or interrupted
|
186
|
+
if self._state not in (ProgressState.PAUSED, ProgressState.INTERRUPTED):
|
187
|
+
self._state = (
|
188
|
+
ProgressState.COMPLETED
|
189
|
+
if self._user_value >= self._user_maximum
|
190
|
+
else ProgressState.NORMAL
|
191
|
+
)
|
125
192
|
self.animate_progress()
|
126
193
|
|
194
|
+
@SafeProperty(object, doc="Current visual state of the progress bar.")
|
195
|
+
def state(self):
|
196
|
+
return self._state
|
197
|
+
|
198
|
+
@state.setter
|
199
|
+
def state(self, state):
|
200
|
+
"""
|
201
|
+
Set the visual state of the progress bar.
|
202
|
+
|
203
|
+
Args:
|
204
|
+
state(ProgressState | str): The state to set. Can be one of the
|
205
|
+
"""
|
206
|
+
if isinstance(state, str):
|
207
|
+
state = ProgressState(state.lower())
|
208
|
+
if not isinstance(state, ProgressState):
|
209
|
+
raise ValueError("state must be a ProgressState or its value")
|
210
|
+
self._state = state
|
211
|
+
self.update()
|
212
|
+
|
213
|
+
@SafeProperty(float, doc="Base corner radius in pixels (auto‑scaled down on small bars).")
|
214
|
+
def corner_radius(self) -> float:
|
215
|
+
return self._corner_radius
|
216
|
+
|
217
|
+
@corner_radius.setter
|
218
|
+
def corner_radius(self, radius: float):
|
219
|
+
self._corner_radius = max(0.0, radius)
|
220
|
+
self.update()
|
221
|
+
|
222
|
+
@SafeProperty(float)
|
223
|
+
def padding_left_right(self) -> float:
|
224
|
+
return self._padding_left_right
|
225
|
+
|
226
|
+
@padding_left_right.setter
|
227
|
+
def padding_left_right(self, padding: float):
|
228
|
+
self._padding_left_right = padding
|
229
|
+
self.update()
|
230
|
+
|
127
231
|
def paintEvent(self, event):
|
128
232
|
painter = QPainter(self)
|
129
233
|
painter.setRenderHint(QPainter.Antialiasing)
|
130
|
-
rect = self.rect().adjusted(
|
234
|
+
rect = self.rect().adjusted(self._padding_left_right, 0, -self._padding_left_right, -1)
|
235
|
+
|
236
|
+
# Corner radius adapts to widget height so it never exceeds half the bar’s thickness
|
237
|
+
radius = min(self._corner_radius, rect.height() / 2)
|
131
238
|
|
132
239
|
# Draw background
|
133
240
|
painter.setBrush(self._background_color)
|
134
241
|
painter.setPen(Qt.NoPen)
|
135
|
-
painter.drawRoundedRect(rect,
|
242
|
+
painter.drawRoundedRect(rect, radius, radius) # Rounded corners
|
136
243
|
|
137
244
|
# Draw border
|
138
245
|
painter.setBrush(Qt.NoBrush)
|
139
246
|
painter.setPen(self._border_color)
|
140
|
-
painter.drawRoundedRect(rect,
|
141
|
-
|
142
|
-
# Determine progress
|
143
|
-
if self.
|
144
|
-
current_color = self.
|
247
|
+
painter.drawRoundedRect(rect, radius, radius)
|
248
|
+
|
249
|
+
# Determine progress colour based on current state
|
250
|
+
if self._state == ProgressState.PAUSED:
|
251
|
+
current_color = self._state_colors[ProgressState.PAUSED]
|
252
|
+
elif self._state == ProgressState.INTERRUPTED:
|
253
|
+
current_color = self._state_colors[ProgressState.INTERRUPTED]
|
254
|
+
elif self._state == ProgressState.COMPLETED or self._value >= self._maximum:
|
255
|
+
current_color = self._state_colors[ProgressState.COMPLETED]
|
145
256
|
else:
|
146
|
-
current_color = self.
|
257
|
+
current_color = self._state_colors[ProgressState.NORMAL]
|
147
258
|
|
148
259
|
# Set clipping region to preserve the background's rounded corners
|
149
260
|
progress_rect = rect.adjusted(
|
150
261
|
0, 0, int(-rect.width() + (self._value / self._maximum) * rect.width()), 0
|
151
262
|
)
|
152
263
|
clip_path = QPainterPath()
|
153
|
-
clip_path.addRoundedRect(
|
264
|
+
clip_path.addRoundedRect(
|
265
|
+
QRectF(rect), radius, radius
|
266
|
+
) # Clip to the background's rounded corners
|
154
267
|
painter.setClipPath(clip_path)
|
155
268
|
|
156
269
|
# Draw progress bar
|
@@ -168,7 +281,7 @@ class BECProgressBar(BECWidget, QWidget):
|
|
168
281
|
self._value_animation.setEndValue(self._target_value)
|
169
282
|
self._value_animation.start()
|
170
283
|
|
171
|
-
@
|
284
|
+
@SafeProperty(float)
|
172
285
|
def maximum(self):
|
173
286
|
"""
|
174
287
|
The maximum value of the progress bar.
|
@@ -182,7 +295,7 @@ class BECProgressBar(BECWidget, QWidget):
|
|
182
295
|
"""
|
183
296
|
self.set_maximum(maximum)
|
184
297
|
|
185
|
-
@
|
298
|
+
@SafeProperty(float)
|
186
299
|
def minimum(self):
|
187
300
|
"""
|
188
301
|
The minimum value of the progress bar.
|
@@ -193,7 +306,7 @@ class BECProgressBar(BECWidget, QWidget):
|
|
193
306
|
def minimum(self, minimum: float):
|
194
307
|
self.set_minimum(minimum)
|
195
308
|
|
196
|
-
@
|
309
|
+
@SafeProperty(float)
|
197
310
|
def initial_value(self):
|
198
311
|
"""
|
199
312
|
The initial value of the progress bar.
|
@@ -204,7 +317,7 @@ class BECProgressBar(BECWidget, QWidget):
|
|
204
317
|
def initial_value(self, value: float):
|
205
318
|
self.set_value(value)
|
206
319
|
|
207
|
-
@
|
320
|
+
@SafeSlot(float)
|
208
321
|
def set_maximum(self, maximum: float):
|
209
322
|
"""
|
210
323
|
Set the maximum value of the progress bar.
|
@@ -213,10 +326,11 @@ class BECProgressBar(BECWidget, QWidget):
|
|
213
326
|
maximum (float): The maximum value.
|
214
327
|
"""
|
215
328
|
self._user_maximum = maximum
|
329
|
+
self._adjust_label_width()
|
216
330
|
self.set_value(self._user_value) # Update the value to fit the new range
|
217
331
|
self.update()
|
218
332
|
|
219
|
-
@
|
333
|
+
@SafeSlot(float)
|
220
334
|
def set_minimum(self, minimum: float):
|
221
335
|
"""
|
222
336
|
Set the minimum value of the progress bar.
|
File without changes
|
@@ -0,0 +1,17 @@
|
|
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.progress.scan_progressbar.scan_progress_bar_plugin import (
|
10
|
+
ScanProgressBarPlugin,
|
11
|
+
)
|
12
|
+
|
13
|
+
QPyDesignerCustomWidgetCollection.addCustomWidget(ScanProgressBarPlugin())
|
14
|
+
|
15
|
+
|
16
|
+
if __name__ == "__main__": # pragma: no cover
|
17
|
+
main()
|
@@ -0,0 +1 @@
|
|
1
|
+
{'files': ['scan_progressbar.py']}
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Copyright (C) 2022 The Qt Company Ltd.
|
2
|
+
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
3
|
+
|
4
|
+
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
5
|
+
|
6
|
+
from bec_widgets.utils.bec_designer import designer_material_icon
|
7
|
+
from bec_widgets.widgets.progress.scan_progressbar.scan_progressbar import ScanProgressBar
|
8
|
+
|
9
|
+
DOM_XML = """
|
10
|
+
<ui language='c++'>
|
11
|
+
<widget class='ScanProgressBar' name='scan_progress_bar'>
|
12
|
+
</widget>
|
13
|
+
</ui>
|
14
|
+
"""
|
15
|
+
|
16
|
+
|
17
|
+
class ScanProgressBarPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
18
|
+
def __init__(self):
|
19
|
+
super().__init__()
|
20
|
+
self._form_editor = None
|
21
|
+
|
22
|
+
def createWidget(self, parent):
|
23
|
+
t = ScanProgressBar(parent)
|
24
|
+
return t
|
25
|
+
|
26
|
+
def domXml(self):
|
27
|
+
return DOM_XML
|
28
|
+
|
29
|
+
def group(self):
|
30
|
+
return "BEC Utils"
|
31
|
+
|
32
|
+
def icon(self):
|
33
|
+
return designer_material_icon(ScanProgressBar.ICON_NAME)
|
34
|
+
|
35
|
+
def includeFile(self):
|
36
|
+
return "scan_progress_bar"
|
37
|
+
|
38
|
+
def initialize(self, form_editor):
|
39
|
+
self._form_editor = form_editor
|
40
|
+
|
41
|
+
def isContainer(self):
|
42
|
+
return False
|
43
|
+
|
44
|
+
def isInitialized(self):
|
45
|
+
return self._form_editor is not None
|
46
|
+
|
47
|
+
def name(self):
|
48
|
+
return "ScanProgressBar"
|
49
|
+
|
50
|
+
def toolTip(self):
|
51
|
+
return "A progress bar that is hooked up to the scan progress of a scan."
|
52
|
+
|
53
|
+
def whatsThis(self):
|
54
|
+
return self.toolTip()
|
@@ -0,0 +1,320 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import enum
|
4
|
+
import os
|
5
|
+
import time
|
6
|
+
from typing import Literal
|
7
|
+
|
8
|
+
import numpy as np
|
9
|
+
from bec_lib.endpoints import MessageEndpoints
|
10
|
+
from bec_lib.logger import bec_logger
|
11
|
+
from qtpy.QtCore import QObject, QTimer, Signal
|
12
|
+
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
13
|
+
|
14
|
+
from bec_widgets.utils.bec_widget import BECWidget
|
15
|
+
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
16
|
+
from bec_widgets.utils.ui_loader import UILoader
|
17
|
+
from bec_widgets.widgets.progress.bec_progressbar.bec_progressbar import ProgressState
|
18
|
+
|
19
|
+
logger = bec_logger.logger
|
20
|
+
|
21
|
+
|
22
|
+
class ProgressSource(enum.Enum):
|
23
|
+
"""
|
24
|
+
Enum to define the source of the progress.
|
25
|
+
"""
|
26
|
+
|
27
|
+
SCAN_PROGRESS = "scan_progress"
|
28
|
+
DEVICE_PROGRESS = "device_progress"
|
29
|
+
|
30
|
+
|
31
|
+
class ProgressTask(QObject):
|
32
|
+
"""
|
33
|
+
Class to store progress information.
|
34
|
+
Inspired by https://github.com/Textualize/rich/blob/master/rich/progress.py
|
35
|
+
"""
|
36
|
+
|
37
|
+
def __init__(self, parent: QWidget, value: float = 0, max_value: float = 0, done: bool = False):
|
38
|
+
super().__init__(parent=parent)
|
39
|
+
self.start_time = time.time()
|
40
|
+
self.done = done
|
41
|
+
self.value = value
|
42
|
+
self.max_value = max_value
|
43
|
+
self._elapsed_time = 0
|
44
|
+
|
45
|
+
self.timer = QTimer(self)
|
46
|
+
self.timer.timeout.connect(self.update_elapsed_time)
|
47
|
+
self.timer.start(100) # update the elapsed time every 100 ms
|
48
|
+
|
49
|
+
def update(self, value: float, max_value: float, done: bool = False):
|
50
|
+
"""
|
51
|
+
Update the progress.
|
52
|
+
"""
|
53
|
+
self.max_value = max_value
|
54
|
+
self.done = done
|
55
|
+
self.value = value
|
56
|
+
if done:
|
57
|
+
self.timer.stop()
|
58
|
+
|
59
|
+
def update_elapsed_time(self):
|
60
|
+
"""
|
61
|
+
Update the time estimates. This is called every 100 ms by a QTimer.
|
62
|
+
"""
|
63
|
+
self._elapsed_time += 0.1
|
64
|
+
|
65
|
+
@property
|
66
|
+
def percentage(self) -> float:
|
67
|
+
"""float: Get progress of task as a percentage. If a None total was set, returns 0"""
|
68
|
+
if not self.max_value:
|
69
|
+
return 0.0
|
70
|
+
completed = (self.value / self.max_value) * 100.0
|
71
|
+
completed = min(100.0, max(0.0, completed))
|
72
|
+
return completed
|
73
|
+
|
74
|
+
@property
|
75
|
+
def speed(self) -> float:
|
76
|
+
"""Get the estimated speed in steps per second."""
|
77
|
+
if self._elapsed_time == 0:
|
78
|
+
return 0.0
|
79
|
+
|
80
|
+
return self.value / self._elapsed_time
|
81
|
+
|
82
|
+
@property
|
83
|
+
def frequency(self) -> float:
|
84
|
+
"""Get the estimated frequency in steps per second."""
|
85
|
+
if self.speed == 0:
|
86
|
+
return 0.0
|
87
|
+
return 1 / self.speed
|
88
|
+
|
89
|
+
@property
|
90
|
+
def time_elapsed(self) -> str:
|
91
|
+
# format the elapsed time to a string in the format HH:MM:SS
|
92
|
+
return self._format_time(int(self._elapsed_time))
|
93
|
+
|
94
|
+
@property
|
95
|
+
def remaining(self) -> float:
|
96
|
+
"""Get the estimated remaining steps."""
|
97
|
+
if self.done:
|
98
|
+
return 0.0
|
99
|
+
remaining = self.max_value - self.value
|
100
|
+
return remaining
|
101
|
+
|
102
|
+
@property
|
103
|
+
def time_remaining(self) -> str:
|
104
|
+
"""
|
105
|
+
Get the estimated remaining time in the format HH:MM:SS.
|
106
|
+
"""
|
107
|
+
if self.done or not self.speed or not self.remaining:
|
108
|
+
return self._format_time(0)
|
109
|
+
estimate = int(np.round(self.remaining / self.speed))
|
110
|
+
|
111
|
+
return self._format_time(estimate)
|
112
|
+
|
113
|
+
def _format_time(self, seconds: float) -> str:
|
114
|
+
"""
|
115
|
+
Format the time in seconds to a string in the format HH:MM:SS.
|
116
|
+
"""
|
117
|
+
return f"{seconds // 3600:02}:{(seconds // 60) % 60:02}:{seconds % 60:02}"
|
118
|
+
|
119
|
+
|
120
|
+
class ScanProgressBar(BECWidget, QWidget):
|
121
|
+
"""
|
122
|
+
Widget to display a progress bar that is hooked up to the scan progress of a scan.
|
123
|
+
If you want to manually set the progress, it is recommended to use the BECProgressbar or QProgressbar directly.
|
124
|
+
"""
|
125
|
+
|
126
|
+
ICON_NAME = "timelapse"
|
127
|
+
progress_started = Signal()
|
128
|
+
progress_finished = Signal()
|
129
|
+
|
130
|
+
def __init__(self, parent=None, client=None, config=None, gui_id=None, one_line_design=False):
|
131
|
+
super().__init__(parent=parent, client=client, config=config, gui_id=gui_id)
|
132
|
+
|
133
|
+
self.get_bec_shortcuts()
|
134
|
+
ui_file = os.path.join(
|
135
|
+
os.path.dirname(__file__),
|
136
|
+
"scan_progressbar_one_line.ui" if one_line_design else "scan_progressbar.ui",
|
137
|
+
)
|
138
|
+
self.ui = UILoader(self).loader(ui_file)
|
139
|
+
self.layout = QVBoxLayout(self)
|
140
|
+
self.layout.setSpacing(0)
|
141
|
+
self.layout.setContentsMargins(0, 0, 0, 0)
|
142
|
+
self.layout.addWidget(self.ui)
|
143
|
+
self.setLayout(self.layout)
|
144
|
+
self.progressbar = self.ui.progressbar
|
145
|
+
|
146
|
+
self.connect_to_queue()
|
147
|
+
self._progress_source = None
|
148
|
+
self.task = None
|
149
|
+
self.scan_number = None
|
150
|
+
self.progress_started.connect(lambda: print("Scan progress started"))
|
151
|
+
|
152
|
+
def connect_to_queue(self):
|
153
|
+
"""
|
154
|
+
Connect to the queue status signal.
|
155
|
+
"""
|
156
|
+
self.bec_dispatcher.connect_slot(self.on_queue_update, MessageEndpoints.scan_queue_status())
|
157
|
+
|
158
|
+
def set_progress_source(self, source: ProgressSource, device=None):
|
159
|
+
"""
|
160
|
+
Set the source of the progress.
|
161
|
+
"""
|
162
|
+
if self._progress_source == source:
|
163
|
+
self.update_source_label(source, device=device)
|
164
|
+
return
|
165
|
+
if self._progress_source is not None:
|
166
|
+
self.bec_dispatcher.disconnect_slot(
|
167
|
+
self.on_progress_update,
|
168
|
+
(
|
169
|
+
MessageEndpoints.scan_progress()
|
170
|
+
if self._progress_source == ProgressSource.SCAN_PROGRESS
|
171
|
+
else MessageEndpoints.device_progress(device=device)
|
172
|
+
),
|
173
|
+
)
|
174
|
+
self._progress_source = source
|
175
|
+
self.bec_dispatcher.connect_slot(
|
176
|
+
self.on_progress_update,
|
177
|
+
(
|
178
|
+
MessageEndpoints.scan_progress()
|
179
|
+
if source == ProgressSource.SCAN_PROGRESS
|
180
|
+
else MessageEndpoints.device_progress(device=device)
|
181
|
+
),
|
182
|
+
)
|
183
|
+
self.update_source_label(source, device=device)
|
184
|
+
# self.progress_started.emit()
|
185
|
+
|
186
|
+
def update_source_label(self, source: ProgressSource, device=None):
|
187
|
+
scan_text = f"Scan {self.scan_number}" if self.scan_number is not None else "Scan"
|
188
|
+
text = scan_text if source == ProgressSource.SCAN_PROGRESS else f"Device {device}"
|
189
|
+
logger.info(f"Set progress source to {text}")
|
190
|
+
self.ui.source_label.setText(text)
|
191
|
+
|
192
|
+
@SafeSlot(dict, dict)
|
193
|
+
def on_progress_update(self, msg_content: dict, metadata: dict):
|
194
|
+
"""
|
195
|
+
Update the progress bar based on the progress message.
|
196
|
+
"""
|
197
|
+
value = msg_content["value"]
|
198
|
+
max_value = msg_content.get("max_value", 100)
|
199
|
+
done = msg_content.get("done", False)
|
200
|
+
status: Literal["open", "paused", "aborted", "halted", "closed"] = metadata.get(
|
201
|
+
"status", "open"
|
202
|
+
)
|
203
|
+
|
204
|
+
if self.task is None:
|
205
|
+
return
|
206
|
+
self.task.update(value, max_value, done)
|
207
|
+
|
208
|
+
self.update_labels()
|
209
|
+
|
210
|
+
self.progressbar.set_maximum(self.task.max_value)
|
211
|
+
self.progressbar.state = ProgressState.from_bec_status(status)
|
212
|
+
self.progressbar.set_value(self.task.value)
|
213
|
+
|
214
|
+
if done:
|
215
|
+
self.task = None
|
216
|
+
self.progress_finished.emit()
|
217
|
+
return
|
218
|
+
|
219
|
+
@SafeProperty(bool)
|
220
|
+
def show_elapsed_time(self):
|
221
|
+
return self.ui.elapsed_time_label.isVisible()
|
222
|
+
|
223
|
+
@show_elapsed_time.setter
|
224
|
+
def show_elapsed_time(self, value):
|
225
|
+
self.ui.elapsed_time_label.setVisible(value)
|
226
|
+
if hasattr(self.ui, "dash"):
|
227
|
+
self.ui.dash.setVisible(value)
|
228
|
+
|
229
|
+
@SafeProperty(bool)
|
230
|
+
def show_remaining_time(self):
|
231
|
+
return self.ui.remaining_time_label.isVisible()
|
232
|
+
|
233
|
+
@show_remaining_time.setter
|
234
|
+
def show_remaining_time(self, value):
|
235
|
+
self.ui.remaining_time_label.setVisible(value)
|
236
|
+
if hasattr(self.ui, "dash"):
|
237
|
+
self.ui.dash.setVisible(value)
|
238
|
+
|
239
|
+
@SafeProperty(bool)
|
240
|
+
def show_source_label(self):
|
241
|
+
return self.ui.source_label.isVisible()
|
242
|
+
|
243
|
+
@show_source_label.setter
|
244
|
+
def show_source_label(self, value):
|
245
|
+
self.ui.source_label.setVisible(value)
|
246
|
+
|
247
|
+
def update_labels(self):
|
248
|
+
"""
|
249
|
+
Update the labels based on the progress task.
|
250
|
+
"""
|
251
|
+
if self.task is None:
|
252
|
+
return
|
253
|
+
|
254
|
+
self.ui.elapsed_time_label.setText(self.task.time_elapsed)
|
255
|
+
self.ui.remaining_time_label.setText(self.task.time_remaining)
|
256
|
+
|
257
|
+
@SafeSlot(dict, dict, verify_sender=True)
|
258
|
+
def on_queue_update(self, msg_content, metadata):
|
259
|
+
"""
|
260
|
+
Update the progress bar based on the queue status.
|
261
|
+
"""
|
262
|
+
if not "queue" in msg_content:
|
263
|
+
return
|
264
|
+
primary_queue_info = msg_content["queue"].get("primary", {}).get("info", [])
|
265
|
+
if len(primary_queue_info) == 0:
|
266
|
+
return
|
267
|
+
scan_info = primary_queue_info[0]
|
268
|
+
if scan_info is None:
|
269
|
+
return
|
270
|
+
if scan_info.get("status").lower() == "running" and self.task is None:
|
271
|
+
self.task = ProgressTask(parent=self)
|
272
|
+
self.progress_started.emit()
|
273
|
+
|
274
|
+
active_request_block = scan_info.get("active_request_block", {})
|
275
|
+
if active_request_block is None:
|
276
|
+
return
|
277
|
+
|
278
|
+
self.scan_number = active_request_block.get("scan_number")
|
279
|
+
report_instructions = active_request_block.get("report_instructions", [])
|
280
|
+
if not report_instructions:
|
281
|
+
return
|
282
|
+
|
283
|
+
# for now, let's just use the first instruction
|
284
|
+
instruction = report_instructions[0]
|
285
|
+
|
286
|
+
if "scan_progress" in instruction:
|
287
|
+
self.set_progress_source(ProgressSource.SCAN_PROGRESS)
|
288
|
+
elif "device_progress" in instruction:
|
289
|
+
device = instruction["device_progress"][0]
|
290
|
+
self.set_progress_source(ProgressSource.DEVICE_PROGRESS, device=device)
|
291
|
+
|
292
|
+
def cleanup(self):
|
293
|
+
if self.task is not None:
|
294
|
+
self.task.timer.stop()
|
295
|
+
self.close()
|
296
|
+
self.deleteLater()
|
297
|
+
if self._progress_source is not None:
|
298
|
+
self.bec_dispatcher.disconnect_slot(
|
299
|
+
self.on_progress_update,
|
300
|
+
(
|
301
|
+
MessageEndpoints.scan_progress()
|
302
|
+
if self._progress_source == ProgressSource.SCAN_PROGRESS
|
303
|
+
else MessageEndpoints.device_progress(device=self._progress_source.value)
|
304
|
+
),
|
305
|
+
)
|
306
|
+
self.progressbar.close()
|
307
|
+
self.progressbar.deleteLater()
|
308
|
+
super().cleanup()
|
309
|
+
|
310
|
+
|
311
|
+
if __name__ == "__main__": # pragma: no cover
|
312
|
+
from qtpy.QtWidgets import QApplication
|
313
|
+
|
314
|
+
bec_logger.disabled_modules = ["bec_lib"]
|
315
|
+
app = QApplication([])
|
316
|
+
|
317
|
+
widget = ScanProgressBar()
|
318
|
+
widget.show()
|
319
|
+
|
320
|
+
app.exec_()
|