207 lines
7.8 KiB
Python
207 lines
7.8 KiB
Python
"""Vector icons drawn with QPainter (one consistent style, no image files, no emoji).
|
|
|
|
`icon(name, color)` returns a QIcon rendered at several sizes. Names: play, stop, record, open, apply, revert,
|
|
save, export, import, settings, star, star-off, warning, ab, loop, search, wave.
|
|
`app_icon()` uses the artwork in app/assets (made by tools/make_assets.py from the user's images).
|
|
"""
|
|
import math
|
|
import os
|
|
|
|
from PySide6.QtCore import QPointF, QRectF, Qt
|
|
from PySide6.QtGui import QColor, QIcon, QPainter, QPainterPath, QPen, QPixmap
|
|
|
|
ACCENT = "#b0242c"
|
|
STEEL = "#c4c4c4"
|
|
TEXT = "#e6e6e6"
|
|
DANGER = "#e0533d"
|
|
MUTED = "#9a9a9a"
|
|
|
|
_cache = {}
|
|
|
|
|
|
def _arrow_arc(p, r, start, span, head=True):
|
|
rect = QRectF(-r, -r, 2 * r, 2 * r)
|
|
path = QPainterPath()
|
|
path.arcMoveTo(rect, start)
|
|
path.arcTo(rect, start, span)
|
|
p.drawPath(path)
|
|
if head:
|
|
a = math.radians(start + span)
|
|
tip = QPointF(r * math.cos(a), -r * math.sin(a))
|
|
d = 1 if span > 0 else -1
|
|
tang = math.radians(start + span + 90 * d)
|
|
back = QPointF(math.cos(tang), -math.sin(tang))
|
|
nrm = QPointF(math.cos(a), -math.sin(a))
|
|
L = r * 0.55
|
|
p.drawLine(tip, tip - back * L + nrm * L * 0.6)
|
|
p.drawLine(tip, tip - back * L - nrm * L * 0.6)
|
|
|
|
|
|
def _draw(p, name, c, s):
|
|
"""Draw icon `name` in a box of size s (origin top-left)."""
|
|
pen = QPen(QColor(c), max(1.2, s * 0.09), Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin)
|
|
p.setPen(pen)
|
|
p.setBrush(Qt.BrushStyle.NoBrush)
|
|
m = s * 0.18
|
|
if name == "play":
|
|
path = QPainterPath()
|
|
path.moveTo(s * 0.30, s * 0.20)
|
|
path.lineTo(s * 0.80, s * 0.50)
|
|
path.lineTo(s * 0.30, s * 0.80)
|
|
path.closeSubpath()
|
|
p.setBrush(QColor(c))
|
|
p.drawPath(path)
|
|
elif name == "stop":
|
|
p.setBrush(QColor(c))
|
|
p.drawRoundedRect(QRectF(s * 0.26, s * 0.26, s * 0.48, s * 0.48), s * 0.06, s * 0.06)
|
|
elif name == "record":
|
|
p.setPen(Qt.PenStyle.NoPen)
|
|
p.setBrush(QColor(DANGER))
|
|
p.drawEllipse(QRectF(s * 0.22, s * 0.22, s * 0.56, s * 0.56))
|
|
elif name == "open":
|
|
path = QPainterPath()
|
|
path.moveTo(m, s * 0.30)
|
|
path.lineTo(s * 0.40, s * 0.30)
|
|
path.lineTo(s * 0.48, s * 0.38)
|
|
path.lineTo(s - m, s * 0.38)
|
|
path.lineTo(s - m, s * 0.78)
|
|
path.lineTo(m, s * 0.78)
|
|
path.closeSubpath()
|
|
p.drawPath(path)
|
|
elif name == "apply":
|
|
path = QPainterPath()
|
|
path.moveTo(s * 0.20, s * 0.52)
|
|
path.lineTo(s * 0.42, s * 0.74)
|
|
path.lineTo(s * 0.82, s * 0.28)
|
|
p.drawPath(path)
|
|
elif name in ("revert", "loop"):
|
|
p.translate(s / 2, s / 2)
|
|
if name == "revert":
|
|
_arrow_arc(p, s * 0.30, 60, 270)
|
|
else:
|
|
_arrow_arc(p, s * 0.30, 20, 150)
|
|
_arrow_arc(p, s * 0.30, 200, 150)
|
|
p.translate(-s / 2, -s / 2)
|
|
elif name == "save": # disk
|
|
path = QPainterPath()
|
|
path.moveTo(m, m)
|
|
path.lineTo(s * 0.70, m)
|
|
path.lineTo(s - m, s * 0.30)
|
|
path.lineTo(s - m, s - m)
|
|
path.lineTo(m, s - m)
|
|
path.closeSubpath()
|
|
p.drawPath(path)
|
|
p.drawRect(QRectF(s * 0.32, m, s * 0.30, s * 0.20))
|
|
p.drawRect(QRectF(s * 0.30, s * 0.56, s * 0.40, s * 0.26))
|
|
elif name in ("export", "import"):
|
|
p.drawLine(QPointF(m, s * 0.62), QPointF(m, s - m))
|
|
p.drawLine(QPointF(m, s - m), QPointF(s - m, s - m))
|
|
p.drawLine(QPointF(s - m, s - m), QPointF(s - m, s * 0.62))
|
|
up = name == "export"
|
|
a, b = (s * 0.66, s * 0.16) if up else (s * 0.16, s * 0.66)
|
|
p.drawLine(QPointF(s / 2, a), QPointF(s / 2, b))
|
|
d = -1 if up else 1
|
|
p.drawLine(QPointF(s / 2, b), QPointF(s / 2 - s * 0.16, b - d * s * 0.16))
|
|
p.drawLine(QPointF(s / 2, b), QPointF(s / 2 + s * 0.16, b - d * s * 0.16))
|
|
elif name == "settings":
|
|
p.translate(s / 2, s / 2)
|
|
path = QPainterPath()
|
|
teeth = 8
|
|
for i in range(teeth * 2):
|
|
a = math.pi * i / teeth
|
|
r = s * (0.36 if i % 2 == 0 else 0.27)
|
|
pt = QPointF(r * math.cos(a), r * math.sin(a))
|
|
path.moveTo(pt) if i == 0 else path.lineTo(pt)
|
|
path.closeSubpath()
|
|
p.drawPath(path)
|
|
p.drawEllipse(QPointF(0, 0), s * 0.11, s * 0.11)
|
|
p.translate(-s / 2, -s / 2)
|
|
elif name in ("star", "star-off"):
|
|
path = QPainterPath()
|
|
for i in range(10):
|
|
a = -math.pi / 2 + math.pi * i / 5
|
|
r = s * (0.40 if i % 2 == 0 else 0.17)
|
|
pt = QPointF(s / 2 + r * math.cos(a), s * 0.54 + r * math.sin(a))
|
|
path.moveTo(pt) if i == 0 else path.lineTo(pt)
|
|
path.closeSubpath()
|
|
if name == "star":
|
|
p.setBrush(QColor(c))
|
|
p.drawPath(path)
|
|
elif name == "warning":
|
|
path = QPainterPath()
|
|
path.moveTo(s / 2, s * 0.14)
|
|
path.lineTo(s * 0.88, s * 0.84)
|
|
path.lineTo(s * 0.12, s * 0.84)
|
|
path.closeSubpath()
|
|
p.drawPath(path)
|
|
p.drawLine(QPointF(s / 2, s * 0.40), QPointF(s / 2, s * 0.60))
|
|
p.drawPoint(QPointF(s / 2, s * 0.72))
|
|
elif name == "ab": # compare: half-filled circle
|
|
rect = QRectF(s * 0.18, s * 0.18, s * 0.64, s * 0.64)
|
|
half = QPainterPath()
|
|
half.moveTo(s / 2, s * 0.18)
|
|
half.arcTo(rect, 90, 180)
|
|
half.closeSubpath()
|
|
p.setBrush(QColor(c))
|
|
p.drawPath(half)
|
|
p.setBrush(Qt.BrushStyle.NoBrush)
|
|
p.drawEllipse(rect)
|
|
elif name in ("prev", "next"): # chevron
|
|
d = -1 if name == "prev" else 1
|
|
x0 = s / 2 - d * s * 0.12
|
|
p.drawLine(QPointF(x0, s * 0.24), QPointF(x0 + d * s * 0.24, s / 2))
|
|
p.drawLine(QPointF(x0 + d * s * 0.24, s / 2), QPointF(x0, s * 0.76))
|
|
elif name == "add": # plus
|
|
p.drawLine(QPointF(s / 2, s * 0.22), QPointF(s / 2, s * 0.78))
|
|
p.drawLine(QPointF(s * 0.22, s / 2), QPointF(s * 0.78, s / 2))
|
|
elif name == "edit": # pencil
|
|
p.translate(s / 2, s / 2)
|
|
p.rotate(-45)
|
|
p.drawRect(QRectF(-s * 0.30, -s * 0.09, s * 0.48, s * 0.18))
|
|
path = QPainterPath()
|
|
path.moveTo(s * 0.18, -s * 0.09)
|
|
path.lineTo(s * 0.36, 0)
|
|
path.lineTo(s * 0.18, s * 0.09)
|
|
p.drawPath(path)
|
|
p.rotate(45)
|
|
p.translate(-s / 2, -s / 2)
|
|
elif name == "search":
|
|
p.drawEllipse(QRectF(s * 0.18, s * 0.18, s * 0.46, s * 0.46))
|
|
p.drawLine(QPointF(s * 0.58, s * 0.58), QPointF(s * 0.84, s * 0.84))
|
|
elif name == "wave": # fallback app glyph: waveform bars
|
|
p.setPen(Qt.PenStyle.NoPen)
|
|
p.setBrush(QColor(c))
|
|
for i, h in enumerate((0.25, 0.5, 0.8, 0.45, 0.65, 0.3)):
|
|
p.drawRoundedRect(QRectF(s * (0.14 + i * 0.13), s * (0.5 - h / 2), s * 0.08, s * h), s * 0.03, s * 0.03)
|
|
|
|
|
|
def icon(name, color=TEXT):
|
|
key = (name, color)
|
|
if key not in _cache:
|
|
ic = QIcon()
|
|
for sz in (16, 20, 24, 32, 48, 64, 128, 256):
|
|
pm = QPixmap(sz, sz)
|
|
pm.fill(Qt.GlobalColor.transparent)
|
|
p = QPainter(pm)
|
|
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
_draw(p, name, color, sz)
|
|
p.end()
|
|
ic.addPixmap(pm)
|
|
_cache[key] = ic
|
|
return _cache[key]
|
|
|
|
|
|
ASSETS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
|
|
|
|
|
|
def asset(name):
|
|
p = os.path.join(ASSETS, name)
|
|
return p if os.path.exists(p) else None
|
|
|
|
|
|
def app_icon():
|
|
"""The app icon (Arkham City logo bat sending out sound arcs); a plain waveform glyph if the assets are missing."""
|
|
p = asset("app_icon.ico") or asset("app_icon.png")
|
|
return QIcon(p) if p else icon("wave", ACCENT)
|
|
|