Skip to content

Commit 6bbb751

Browse files
committed
Added stacked progress bar
1 parent 12dc315 commit 6bbb751

File tree

2 files changed

+369
-0
lines changed

2 files changed

+369
-0
lines changed
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# This file is part of the bliss project
4+
#
5+
# Copyright (c) 2015-2023 Beamline Control Unit, ESRF
6+
# Distributed under the GNU LGPLv3. See LICENSE for more info.
7+
8+
from __future__ import annotations
9+
10+
from typing import NamedTuple, Any, Final, ValuesView
11+
from silx.gui import qt
12+
13+
14+
class ProgressItem(NamedTuple):
15+
"""Item storing the state of a stacked progress item"""
16+
17+
value: int
18+
"""Progression of the item"""
19+
20+
visible: bool
21+
"""Is the item displayed"""
22+
23+
color: qt.QColor
24+
"""Color of the progress"""
25+
26+
striped: bool
27+
"""If true, apply a stripe color to the gradiant"""
28+
29+
animated: bool
30+
"""If true, the stripe is animated"""
31+
32+
toolTip: str
33+
"""Tool tip of this item"""
34+
35+
userData: Any
36+
"""Any user data"""
37+
38+
39+
class _UndefinedType:
40+
pass
41+
42+
43+
_Undefined: Final = _UndefinedType()
44+
45+
46+
class StackedProgressBar(qt.QProgressBar):
47+
"""
48+
Multiple stacked progress bar in single component
49+
"""
50+
51+
def __init__(self, parent: qt.Qwidget | None = None):
52+
super().__init__(parent=parent)
53+
self.__stacks: dict[str, ProgressItem] = {}
54+
self._animated: int = 0
55+
self._timer = qt.QTimer(self)
56+
self._timer.setInterval(80)
57+
self._timer.timeout.connect(self._tick)
58+
self._spacing: int = 0
59+
self._spacingCollapsible: bool = True
60+
61+
def _tick(self):
62+
self._animated += 2
63+
self.update()
64+
65+
def setSpacing(self, spacing: int):
66+
"""Spacing between items, in pixels"""
67+
if self._spacing == spacing:
68+
return
69+
self._spacing = spacing
70+
self.update()
71+
72+
def spacing(self) -> int:
73+
return self._spacing
74+
75+
def setSpacingCollapsible(self, collapse: bool):
76+
"""
77+
Set whether consecutive spacing should be collapsed.
78+
79+
It can be usedul to disable that to ensure pixel perfect
80+
rendering is some use cases.
81+
82+
By default, this property is true.
83+
"""
84+
if self._spacingCollapsible == collapse:
85+
return
86+
self._spacingCollapsible = collapse
87+
self.update()
88+
89+
def spacingCollapsible(self) -> bool:
90+
return self._spacingCollapsible
91+
92+
def clear(self):
93+
"""Remove every stacked items from the widget"""
94+
if len(self.__stacks) == 0:
95+
return
96+
self.__stacks.clear()
97+
self.update()
98+
99+
def setProgressItem(
100+
self,
101+
name: str,
102+
value: int | None | _UndefinedType = _Undefined,
103+
visible: bool | _UndefinedType = _Undefined,
104+
color: qt.QColor | None | _UndefinedType = _Undefined,
105+
striped: bool | _UndefinedType = _Undefined,
106+
animated: bool | _UndefinedType = _Undefined,
107+
toolTip: str | None | _UndefinedType = _Undefined,
108+
userData: Any = _Undefined,
109+
):
110+
"""Add or update a stacked items by its name"""
111+
112+
previousItem = self.__stacks.get(name)
113+
114+
if previousItem is not None:
115+
if value is _Undefined:
116+
value = previousItem.value
117+
if visible is _Undefined:
118+
visible = previousItem.visible
119+
if striped is _Undefined:
120+
striped = previousItem.striped
121+
if color is _Undefined:
122+
color = previousItem.color
123+
if toolTip is _Undefined:
124+
toolTip = previousItem.toolTip
125+
if animated is _Undefined:
126+
animated = previousItem.animated
127+
if userData is _Undefined:
128+
userData = previousItem.userData
129+
else:
130+
if value is _Undefined:
131+
value = 0
132+
if visible is _Undefined:
133+
visible = True
134+
if striped is _Undefined:
135+
striped = False
136+
if color is _Undefined:
137+
color = qt.QColor()
138+
if toolTip is _Undefined:
139+
toolTip = ""
140+
if animated is _Undefined:
141+
animated = False
142+
if userData is _Undefined:
143+
userData = None
144+
145+
newItem = ProgressItem(
146+
value=value,
147+
visible=visible,
148+
color=color,
149+
striped=striped,
150+
animated=animated,
151+
toolTip=toolTip,
152+
userData=userData,
153+
)
154+
if previousItem == newItem:
155+
return
156+
self.__stacks[name] = newItem
157+
animated = any([s.animated for s in self.__stacks.values()])
158+
self._setAnimated(animated)
159+
self.update()
160+
161+
def _setAnimated(self, animated: bool):
162+
if animated == self._timer.isActive():
163+
return
164+
if animated:
165+
self._timer.start()
166+
else:
167+
self._timer.stop()
168+
169+
def removeProgressItem(self, name: str):
170+
"""Remove a stacked item by its name"""
171+
s = self.__stacks.pop(name, None)
172+
if s is None:
173+
return
174+
self.update()
175+
176+
def _brushFromProgressItem(self, item: ProgressItem) -> qt.QPalette | None:
177+
if item.color is None:
178+
return None
179+
180+
palette = qt.QPalette()
181+
color = qt.QColor(item.color)
182+
183+
if item.striped:
184+
if item.animated:
185+
delta = self._animated
186+
else:
187+
delta = 0
188+
color2 = color.lighter(120)
189+
shadowGradient = qt.QLinearGradient()
190+
shadowGradient.setSpread(qt.QGradient.RepeatSpread)
191+
shadowGradient.setStart(-delta, 0)
192+
shadowGradient.setFinalStop(8 - delta, -8)
193+
shadowGradient.setColorAt(0.0, color)
194+
shadowGradient.setColorAt(0.5, color)
195+
shadowGradient.setColorAt(0.50001, color2)
196+
shadowGradient.setColorAt(1.0, color2)
197+
brush = qt.QBrush(shadowGradient)
198+
palette.setBrush(qt.QPalette.Highlight, brush)
199+
palette.setBrush(qt.QPalette.Window, color2)
200+
else:
201+
palette.setColor(qt.QPalette.Highlight, color)
202+
203+
return palette
204+
205+
def paintEvent(self, event):
206+
painter = qt.QStylePainter(self)
207+
opt = qt.QStyleOptionProgressBar()
208+
self.initStyleOption(opt)
209+
painter.drawControl(qt.QStyle.CE_ProgressBarGroove, opt)
210+
self._drawProgressItems(painter, self.__stacks.values())
211+
212+
def _drawProgressItems(self, painter: qt.QPainter, items: ValuesView[ProgressItem]):
213+
opt = qt.QStyleOptionProgressBar()
214+
self.initStyleOption(opt)
215+
216+
visibleItems = [i for i in items if i.value and i.visible]
217+
xpos: int = 0
218+
w = opt.rect.width()
219+
if self._spacingCollapsible:
220+
cumspacing = max(0, len(visibleItems) - 1) * self._spacing
221+
w -= cumspacing
222+
vw = opt.maximum - opt.minimum
223+
opt.minimum = 0
224+
opt.maximum = w
225+
226+
for item in visibleItems:
227+
xwidth = int(item.value * w / vw)
228+
opt.progress = xwidth * 2
229+
palette = self._brushFromProgressItem(item)
230+
if palette is not None:
231+
opt.palette = palette
232+
self._drawProgressItem(painter, opt, xpos, xwidth)
233+
xpos += xwidth + self._spacing
234+
235+
def _drawProgressItem(
236+
self,
237+
painter: qt.QPainter,
238+
option: qt.QStyleOptionProgressBar,
239+
xpos: int,
240+
xwidth: int,
241+
):
242+
if xwidth == 0:
243+
return
244+
rect: qt.QRect = option.rect
245+
style = self.style()
246+
247+
if option.minimum == 0 and option.maximum == 0:
248+
return
249+
x0 = rect.x() + 3
250+
y0 = rect.y()
251+
252+
h = rect.height()
253+
w = rect.width()
254+
xmaxwith = min(x0 + xpos + xwidth, w - 1) - x0 - xpos
255+
if xmaxwith < 0:
256+
return
257+
rect = qt.QRect(x0 + xpos, y0, xmaxwith, h)
258+
opt = qt.QStyleOptionProgressBar()
259+
opt.state = qt.QStyle.State_None
260+
margin = 1
261+
opt.rect = rect.marginsAdded(qt.QMargins(margin, margin, margin, margin))
262+
opt.palette = option.palette
263+
style.drawPrimitive(qt.QStyle.PE_IndicatorProgressChunk, opt, painter, self)
264+
265+
def getProgressItemByPosition(self, pos: qt.QPoint) -> ProgressItem | None:
266+
"""Returns the stacked item at a position of the component."""
267+
minimum = self.minimum()
268+
maximum = self.maximum()
269+
vRange = maximum - minimum
270+
w = self.width()
271+
v = pos.x() * vRange / w
272+
current = 0
273+
for item in self.__stacks.values():
274+
if not item.visible:
275+
continue
276+
current += item.value
277+
if v < current:
278+
return item
279+
return None
280+
281+
def tooltipFromProgressItem(self, item: ProgressItem) -> str | None:
282+
"""Returns the tooltip to display over an item.
283+
284+
It is triggered when the tooltip have to be displayed.
285+
"""
286+
return item.toolTip
287+
288+
def event(self, event: qt.QEvent):
289+
if event.type() == qt.QEvent.ToolTip:
290+
item = self.getProgressItemByPosition(event.pos())
291+
if item is not None:
292+
toolTip = self.tooltipFromProgressItem(item)
293+
if toolTip:
294+
qt.QToolTip.showText(event.globalPos(), toolTip, self)
295+
return True
296+
return super().event(event)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# /*##########################################################################
2+
#
3+
# Copyright (c) 2018 European Synchrotron Radiation Facility
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in
13+
# all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
# THE SOFTWARE.
22+
#
23+
# ###########################################################################*/
24+
"""Tests for StackedProgressBar"""
25+
26+
__license__ = "MIT"
27+
28+
import pytest
29+
import weakref
30+
from silx.gui import qt
31+
from silx.gui.widgets.StackedProgressBar import StackedProgressBar
32+
33+
34+
@pytest.fixture
35+
def stackedProgressBar(qapp, qapp_utils):
36+
widget = StackedProgressBar()
37+
widget.setAttribute(qt.Qt.WA_DeleteOnClose)
38+
yield widget
39+
widget.close()
40+
ref = weakref.ref(widget)
41+
widget = None
42+
qapp_utils.qWaitForDestroy(ref)
43+
44+
45+
def test_show(qapp_utils, stackedProgressBar: StackedProgressBar):
46+
qapp_utils.qWaitForWindowExposed(stackedProgressBar)
47+
48+
49+
def test_value(qapp_utils, stackedProgressBar: StackedProgressBar):
50+
stackedProgressBar.setValue(1.5)
51+
stackedProgressBar.setRange(0, 100)
52+
stackedProgressBar.setProgressItem("foo", value=0)
53+
stackedProgressBar.setProgressItem("foo", value=50)
54+
stackedProgressBar.setProgressItem("foo", value=100)
55+
qapp_utils.qWaitForWindowExposed(stackedProgressBar)
56+
57+
58+
def test_animation(qapp_utils, stackedProgressBar: StackedProgressBar):
59+
stackedProgressBar.setValue(1.5)
60+
stackedProgressBar.setRange(0, 100)
61+
stackedProgressBar.setProgressItem("foo", value=0, striped=True, animated=True)
62+
stackedProgressBar.setProgressItem("foo", value=50)
63+
stackedProgressBar.setProgressItem("foo", value=100)
64+
qapp_utils.qWaitForWindowExposed(stackedProgressBar)
65+
66+
67+
def test_stack(qapp_utils, stackedProgressBar: StackedProgressBar):
68+
stackedProgressBar.setValue(1.5)
69+
stackedProgressBar.setRange(0, 100)
70+
stackedProgressBar.setProgressItem("foo1", value=10, color=qt.QColor("#FF0000"))
71+
stackedProgressBar.setProgressItem("foo2", value=50, color=qt.QColor("#00FF00"))
72+
stackedProgressBar.setProgressItem("foo3", value=20, color=qt.QColor("#0000FF"))
73+
qapp_utils.qWaitForWindowExposed(stackedProgressBar)

0 commit comments

Comments
 (0)