mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
feat(progressbar): added bec progressbar
This commit is contained in:
@ -19,6 +19,7 @@ class Widgets(str, enum.Enum):
|
|||||||
BECFigure = "BECFigure"
|
BECFigure = "BECFigure"
|
||||||
BECImageWidget = "BECImageWidget"
|
BECImageWidget = "BECImageWidget"
|
||||||
BECMotorMapWidget = "BECMotorMapWidget"
|
BECMotorMapWidget = "BECMotorMapWidget"
|
||||||
|
BECProgressBar = "BECProgressBar"
|
||||||
BECQueue = "BECQueue"
|
BECQueue = "BECQueue"
|
||||||
BECStatusBox = "BECStatusBox"
|
BECStatusBox = "BECStatusBox"
|
||||||
BECWaveformWidget = "BECWaveformWidget"
|
BECWaveformWidget = "BECWaveformWidget"
|
||||||
@ -1693,6 +1694,48 @@ class BECPlotBase(RPCBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BECProgressBar(RPCBase):
|
||||||
|
@rpc_call
|
||||||
|
def set_value(self, value):
|
||||||
|
"""
|
||||||
|
Smoothly transition the progress bar to the new value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def set_maximum(self, maximum: float):
|
||||||
|
"""
|
||||||
|
Set the maximum value of the progress bar.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def set_minimum(self, minimum: float):
|
||||||
|
"""
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def label_template(self):
|
||||||
|
"""
|
||||||
|
The template for the center label. Use $value, $maximum, and $percentage to insert the values.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> progressbar.label_template = "$value / $maximum - $percentage %"
|
||||||
|
>>> progressbar.label_template = "$value / $percentage %"
|
||||||
|
"""
|
||||||
|
|
||||||
|
@label_template.setter
|
||||||
|
@rpc_call
|
||||||
|
def label_template(self):
|
||||||
|
"""
|
||||||
|
The template for the center label. Use $value, $maximum, and $percentage to insert the values.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> progressbar.label_template = "$value / $maximum - $percentage %"
|
||||||
|
>>> progressbar.label_template = "$value / $percentage %"
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class BECQueue(RPCBase):
|
class BECQueue(RPCBase):
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
|
0
bec_widgets/widgets/bec_progressbar/__init__.py
Normal file
0
bec_widgets/widgets/bec_progressbar/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
{'files': ['bec_progressbar.py']}
|
@ -0,0 +1,54 @@
|
|||||||
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
|
from bec_widgets.widgets.bec_progressbar.bec_progressbar import BECProgressBar
|
||||||
|
|
||||||
|
DOM_XML = """
|
||||||
|
<ui language='c++'>
|
||||||
|
<widget class='BECProgressBar' name='bec_progress_bar'>
|
||||||
|
</widget>
|
||||||
|
</ui>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BECProgressBarPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._form_editor = None
|
||||||
|
|
||||||
|
def createWidget(self, parent):
|
||||||
|
t = BECProgressBar(parent)
|
||||||
|
return t
|
||||||
|
|
||||||
|
def domXml(self):
|
||||||
|
return DOM_XML
|
||||||
|
|
||||||
|
def group(self):
|
||||||
|
return "BEC Utils"
|
||||||
|
|
||||||
|
def icon(self):
|
||||||
|
return designer_material_icon(BECProgressBar.ICON_NAME)
|
||||||
|
|
||||||
|
def includeFile(self):
|
||||||
|
return "bec_progress_bar"
|
||||||
|
|
||||||
|
def initialize(self, form_editor):
|
||||||
|
self._form_editor = form_editor
|
||||||
|
|
||||||
|
def isContainer(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isInitialized(self):
|
||||||
|
return self._form_editor is not None
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return "BECProgressBar"
|
||||||
|
|
||||||
|
def toolTip(self):
|
||||||
|
return "A custom progress bar with smooth transitions and a modern design."
|
||||||
|
|
||||||
|
def whatsThis(self):
|
||||||
|
return self.toolTip()
|
249
bec_widgets/widgets/bec_progressbar/bec_progressbar.py
Normal file
249
bec_widgets/widgets/bec_progressbar/bec_progressbar.py
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import sys
|
||||||
|
from string import Template
|
||||||
|
|
||||||
|
from qtpy.QtCore import Property, QEasingCurve, QPropertyAnimation, QRectF, Qt, QTimer, Slot
|
||||||
|
from qtpy.QtGui import QColor, QPainter, QPainterPath
|
||||||
|
from qtpy.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
|
||||||
|
|
||||||
|
class BECProgressBar(BECWidget, QWidget):
|
||||||
|
"""
|
||||||
|
A custom progress bar with smooth transitions. The displayed text can be customized using a template.
|
||||||
|
"""
|
||||||
|
|
||||||
|
USER_ACCESS = [
|
||||||
|
"set_value",
|
||||||
|
"set_maximum",
|
||||||
|
"set_minimum",
|
||||||
|
"label_template",
|
||||||
|
"label_template.setter",
|
||||||
|
]
|
||||||
|
ICON_NAME = "page_control"
|
||||||
|
|
||||||
|
def __init__(self, parent=None, client=None, config=None, gui_id=None):
|
||||||
|
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||||
|
QWidget.__init__(self, parent=parent)
|
||||||
|
|
||||||
|
accent_colors = QApplication.instance().theme.accent_colors
|
||||||
|
|
||||||
|
# internal values
|
||||||
|
self._oversampling_factor = 50
|
||||||
|
self._value = 0
|
||||||
|
self._target_value = 0
|
||||||
|
self._maximum = 100 * self._oversampling_factor
|
||||||
|
|
||||||
|
# User values
|
||||||
|
self._user_value = 0
|
||||||
|
self._user_minimum = 0
|
||||||
|
self._user_maximum = 100
|
||||||
|
self._label_template = "$value / $maximum - $percentage %"
|
||||||
|
|
||||||
|
# Color settings
|
||||||
|
self._background_color = QColor(30, 30, 30)
|
||||||
|
self._progress_color = accent_colors.highlight # QColor(210, 55, 130)
|
||||||
|
|
||||||
|
self._completed_color = accent_colors.success
|
||||||
|
self._border_color = QColor(50, 50, 50)
|
||||||
|
|
||||||
|
# layout settings
|
||||||
|
self._value_animation = QPropertyAnimation(self, b"_progressbar_value")
|
||||||
|
self._value_animation.setDuration(200)
|
||||||
|
self._value_animation.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||||
|
|
||||||
|
# label on top of the progress bar
|
||||||
|
self.center_label = QLabel(self)
|
||||||
|
self.center_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.center_label.setStyleSheet("color: white;")
|
||||||
|
self.center_label.setMinimumSize(0, 0)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
layout.addWidget(self.center_label)
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@Property(str, doc="The template for the center label. Use $value, $maximum, and $percentage.")
|
||||||
|
def label_template(self):
|
||||||
|
"""
|
||||||
|
The template for the center label. Use $value, $maximum, and $percentage to insert the values.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> progressbar.label_template = "$value / $maximum - $percentage %"
|
||||||
|
>>> progressbar.label_template = "$value / $percentage %"
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self._label_template
|
||||||
|
|
||||||
|
@label_template.setter
|
||||||
|
def label_template(self, template):
|
||||||
|
self._label_template = template
|
||||||
|
self.set_value(self._user_value)
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@Property(float, designable=False)
|
||||||
|
def _progressbar_value(self):
|
||||||
|
"""
|
||||||
|
The current value of the progress bar.
|
||||||
|
"""
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@_progressbar_value.setter
|
||||||
|
def _progressbar_value(self, val):
|
||||||
|
self._value = val
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def _update_template(self):
|
||||||
|
template = Template(self._label_template)
|
||||||
|
return template.safe_substitute(
|
||||||
|
value=self._user_value,
|
||||||
|
maximum=self._user_maximum,
|
||||||
|
percentage=int((self.map_value(self._user_value) / self._maximum) * 100),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Slot(float)
|
||||||
|
@Slot(int)
|
||||||
|
def set_value(self, value):
|
||||||
|
"""Smoothly transition the progress bar to the new value."""
|
||||||
|
if value > self._user_maximum:
|
||||||
|
value = self._user_maximum
|
||||||
|
elif value < self._user_minimum:
|
||||||
|
value = self._user_minimum
|
||||||
|
self._target_value = self.map_value(value)
|
||||||
|
self._user_value = value
|
||||||
|
self.center_label.setText(self._update_template())
|
||||||
|
self.animate_progress()
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
painter = QPainter(self)
|
||||||
|
painter.setRenderHint(QPainter.Antialiasing)
|
||||||
|
rect = self.rect().adjusted(10, 0, -10, -1)
|
||||||
|
|
||||||
|
# Draw background
|
||||||
|
painter.setBrush(self._background_color)
|
||||||
|
painter.setPen(Qt.NoPen)
|
||||||
|
painter.drawRoundedRect(rect, 10, 10) # Rounded corners
|
||||||
|
|
||||||
|
# Draw border
|
||||||
|
painter.setBrush(Qt.NoBrush)
|
||||||
|
painter.setPen(self._border_color)
|
||||||
|
painter.drawRoundedRect(rect, 10, 10)
|
||||||
|
|
||||||
|
# Determine progress color based on completion
|
||||||
|
if self._value >= self._maximum:
|
||||||
|
current_color = self._completed_color
|
||||||
|
else:
|
||||||
|
current_color = self._progress_color
|
||||||
|
|
||||||
|
# Set clipping region to preserve the background's rounded corners
|
||||||
|
progress_rect = rect.adjusted(
|
||||||
|
0, 0, int(-rect.width() + (self._value / self._maximum) * rect.width()), 0
|
||||||
|
)
|
||||||
|
clip_path = QPainterPath()
|
||||||
|
clip_path.addRoundedRect(QRectF(rect), 10, 10) # Clip to the background's rounded corners
|
||||||
|
painter.setClipPath(clip_path)
|
||||||
|
|
||||||
|
# Draw progress bar
|
||||||
|
painter.setBrush(current_color)
|
||||||
|
painter.drawRect(progress_rect) # Less rounded, no additional rounding
|
||||||
|
|
||||||
|
painter.end()
|
||||||
|
|
||||||
|
def animate_progress(self):
|
||||||
|
"""
|
||||||
|
Animate the progress bar from the current value to the target value.
|
||||||
|
"""
|
||||||
|
self._value_animation.stop()
|
||||||
|
self._value_animation.setStartValue(self._value)
|
||||||
|
self._value_animation.setEndValue(self._target_value)
|
||||||
|
self._value_animation.start()
|
||||||
|
|
||||||
|
@Property(float)
|
||||||
|
def maximum(self):
|
||||||
|
"""
|
||||||
|
The maximum value of the progress bar.
|
||||||
|
"""
|
||||||
|
return self._user_maximum
|
||||||
|
|
||||||
|
@maximum.setter
|
||||||
|
def maximum(self, maximum: float):
|
||||||
|
"""
|
||||||
|
Set the maximum value of the progress bar.
|
||||||
|
"""
|
||||||
|
self.set_maximum(maximum)
|
||||||
|
|
||||||
|
@Property(float)
|
||||||
|
def minimum(self):
|
||||||
|
"""
|
||||||
|
The minimum value of the progress bar.
|
||||||
|
"""
|
||||||
|
return self._user_minimum
|
||||||
|
|
||||||
|
@minimum.setter
|
||||||
|
def minimum(self, minimum: float):
|
||||||
|
self.set_minimum(minimum)
|
||||||
|
|
||||||
|
@Property(float)
|
||||||
|
def initial_value(self):
|
||||||
|
"""
|
||||||
|
The initial value of the progress bar.
|
||||||
|
"""
|
||||||
|
return self._user_value
|
||||||
|
|
||||||
|
@initial_value.setter
|
||||||
|
def initial_value(self, value: float):
|
||||||
|
self.set_value(value)
|
||||||
|
|
||||||
|
@Slot(float)
|
||||||
|
def set_maximum(self, maximum: float):
|
||||||
|
"""
|
||||||
|
Set the maximum value of the progress bar.
|
||||||
|
"""
|
||||||
|
self._user_maximum = maximum
|
||||||
|
self.set_value(self._user_value) # Update the value to fit the new range
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@Slot(float)
|
||||||
|
def set_minimum(self, minimum: float):
|
||||||
|
self._user_minimum = minimum
|
||||||
|
self.set_value(self._user_value) # Update the value to fit the new range
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def map_value(self, value: float):
|
||||||
|
"""
|
||||||
|
Map the user value to the range [0, 100*self._oversampling_factor] for the progress
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
(value - self._user_minimum) / (self._user_maximum - self._user_minimum) * self._maximum
|
||||||
|
)
|
||||||
|
|
||||||
|
def sizeHint(self):
|
||||||
|
return self.minimumSizeHint()
|
||||||
|
|
||||||
|
def minimumSizeHint(self):
|
||||||
|
return self.size()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
progressBar = BECProgressBar()
|
||||||
|
progressBar.show()
|
||||||
|
progressBar.set_minimum(-100)
|
||||||
|
progressBar.set_maximum(0)
|
||||||
|
|
||||||
|
# Example of setting values
|
||||||
|
def update_progress():
|
||||||
|
value = progressBar._user_value + 2.5
|
||||||
|
if value > progressBar._user_maximum:
|
||||||
|
value = -100 # progressBar._maximum / progressBar._upsampling_factor
|
||||||
|
progressBar.set_value(value)
|
||||||
|
|
||||||
|
timer = QTimer()
|
||||||
|
timer.timeout.connect(update_progress)
|
||||||
|
timer.start(200) # Update every half second
|
||||||
|
|
||||||
|
sys.exit(app.exec())
|
@ -0,0 +1,15 @@
|
|||||||
|
def main(): # pragma: no cover
|
||||||
|
from qtpy import PYSIDE6
|
||||||
|
|
||||||
|
if not PYSIDE6:
|
||||||
|
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||||
|
return
|
||||||
|
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||||
|
|
||||||
|
from bec_widgets.widgets.bec_progressbar.bec_progress_bar_plugin import BECProgressBarPlugin
|
||||||
|
|
||||||
|
QPyDesignerCustomWidgetCollection.addCustomWidget(BECProgressBarPlugin())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
main()
|
35
tests/unit_tests/test_bec_progressbar.py
Normal file
35
tests/unit_tests/test_bec_progressbar.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from bec_widgets.widgets.bec_progressbar.bec_progressbar import BECProgressBar
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def progressbar(qtbot):
|
||||||
|
widget = BECProgressBar()
|
||||||
|
qtbot.addWidget(widget)
|
||||||
|
qtbot.waitExposed(widget)
|
||||||
|
yield widget
|
||||||
|
|
||||||
|
|
||||||
|
def test_progressbar(progressbar):
|
||||||
|
progressbar.update()
|
||||||
|
|
||||||
|
|
||||||
|
def test_progressbar_set_value(qtbot, progressbar):
|
||||||
|
progressbar.set_minimum(0)
|
||||||
|
progressbar.set_maximum(100)
|
||||||
|
progressbar.set_value(50)
|
||||||
|
progressbar.paintEvent(None)
|
||||||
|
|
||||||
|
qtbot.waitUntil(
|
||||||
|
lambda: np.isclose(
|
||||||
|
progressbar._value, progressbar._user_value * progressbar._oversampling_factor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_progressbar_label(progressbar):
|
||||||
|
progressbar.label_template = "Test: $value"
|
||||||
|
progressbar.set_value(50)
|
||||||
|
assert progressbar.center_label.text() == "Test: 50"
|
Reference in New Issue
Block a user