|
| 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) |
0 commit comments