0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21:50 +02:00

feat(progressbar): added bec progressbar

This commit is contained in:
2024-09-05 20:09:52 +02:00
parent a52182dca9
commit f6d1d0bbe3
7 changed files with 397 additions and 0 deletions

View File

@ -19,6 +19,7 @@ class Widgets(str, enum.Enum):
BECFigure = "BECFigure"
BECImageWidget = "BECImageWidget"
BECMotorMapWidget = "BECMotorMapWidget"
BECProgressBar = "BECProgressBar"
BECQueue = "BECQueue"
BECStatusBox = "BECStatusBox"
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):
@property
@rpc_call

View File

@ -0,0 +1 @@
{'files': ['bec_progressbar.py']}

View File

@ -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()

View 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())

View File

@ -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()

View 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"