Files
UnheardCity/app/spinner.py

119 lines
3.8 KiB
Python

"""Small activity spinner: a rotating accent-coloured arc. Runs only while visible."""
from PySide6.QtCore import QEvent, QRectF, QTimer, Qt
from PySide6.QtGui import QColor, QPainter, QPalette, QPen
from PySide6.QtWidgets import QWidget
from . import icons
STEP_MS = 16
STEP_DEG = 9
def paint_arc(p, rect, angle, color=icons.ACCENT):
"""Draw the spinner (faint ring + 100 degree arc) into rect; used by Spinner and the waveform overlay."""
w = max(1.5, rect.width() / 8)
r = rect.adjusted(w / 2, w / 2, -w / 2, -w / 2)
p.save()
p.setRenderHint(QPainter.RenderHint.Antialiasing)
ring = QColor(color)
ring.setAlpha(45)
p.setPen(QPen(ring, w))
p.drawEllipse(r)
pen = QPen(QColor(color), w)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
p.setPen(pen)
p.drawArc(r, int(-angle * 16), 100 * 16)
p.restore()
class Spinner(QWidget):
def __init__(self, size=16, parent=None):
super().__init__(parent)
self.setFixedSize(size, size)
self.angle = 0
self.timer = QTimer(self)
self.timer.setInterval(STEP_MS)
self.timer.timeout.connect(self._step)
self.hide()
def _step(self):
self.angle = (self.angle + STEP_DEG) % 360
self.update()
def showEvent(self, e):
self.timer.start()
super().showEvent(e)
def hideEvent(self, e):
self.timer.stop()
super().hideEvent(e)
def paintEvent(self, _):
p = QPainter(self)
paint_arc(p, QRectF(self.rect()), self.angle)
class BusyOverlay(QWidget):
"""A list being (re)loaded: a light veil over the widget with the spinner and a message in its centre, the
same arc as the waveform's loading state. Covers the target and follows its size; blocks clicks while on."""
def __init__(self, target):
super().__init__(target)
self.target = target
self.message = ""
self.angle = 0
self.timer = QTimer(self)
self.timer.setInterval(STEP_MS)
self.timer.timeout.connect(self._step)
target.installEventFilter(self)
self.hide()
def start(self, message):
self.message = message
self.setGeometry(self.target.rect())
self.raise_()
self.show()
self.timer.start()
self.update()
def stop(self):
self.timer.stop()
self.hide()
@property
def busy(self):
return not self.isHidden() # on, whether or not the window is on screen yet
def _step(self):
self.angle = (self.angle + STEP_DEG) % 360
if self.isVisible():
self.update()
def eventFilter(self, obj, e):
if obj is self.target and e.type() == QEvent.Type.Resize:
self.setGeometry(self.target.rect())
return False
def paintEvent(self, _):
p = QPainter(self)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
pal = self.palette()
veil = QColor(pal.color(QPalette.ColorRole.Base))
veil.setAlpha(210)
p.fillRect(self.rect(), veil)
fm = p.fontMetrics()
d, gap, pad = 20, 12, 16
tw = fm.horizontalAdvance(self.message)
w, h = pad + d + gap + tw + pad, max(d, fm.height()) + 2 * pad - 8
box = QRectF((self.width() - w) / 2, (self.height() - h) / 2, w, h)
border = QColor(pal.color(QPalette.ColorRole.Text))
border.setAlpha(40)
p.setPen(QPen(border, 1))
p.setBrush(pal.color(QPalette.ColorRole.Window))
p.drawRoundedRect(box, 8, 8)
y = box.center().y()
paint_arc(p, QRectF(box.left() + pad, y - d / 2, d, d), self.angle)
p.setPen(pal.color(QPalette.ColorRole.Text))
p.drawText(QRectF(box.left() + pad + d + gap, y - fm.height() / 2, tw + 2, fm.height()),
Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft, self.message)