"""
Icons
=====
The app had no icon system. Transport buttons used
`QStyle.standardIcon(SP_MediaPlay)`, which returns the platform style's dark
monochrome glyph -- painted onto the app's saturated red buttons, at a fixed
36px with no text and no tooltip, that reads as a black or empty square. On
styles that return a null icon for some standard pixmaps it *was* an empty
square. Everything else called an "icon" was an emoji baked into the English
translation file, which renders as tofu wherever the emoji font is missing.
Approach: a small set of hand-drawn SVGs rendered through QtSvg (part of
PySide6-Essentials, so no new dependency), recoloured at load. Qt stylesheets
cannot recolour a QIcon, which is the whole reason the old icons were
unreadable -- so the colour is an argument here.
The sources live in this module rather than as asset files on purpose: no
package-data to keep in sync, no path resolution, and nothing to go missing
from a wheel or a frozen build.
Drawing conventions: 24x24 viewBox, 2px round strokes, `%COLOR%` wherever the
colour goes. Shapes that must read as solid at 16px (the play triangle) carry
their own fill.
"""
from functools import lru_cache
from typing import Dict, Optional
from PySide6.QtCore import QByteArray, QRectF, Qt
from PySide6.QtGui import QIcon, QPainter, QPixmap
from PySide6.QtSvg import QSvgRenderer
from ..utils.ytsage_logger import logger
#: Default stroke colour. Overridden per call; kept in step with ytsage_theme.
DEFAULT_COLOR = "#e8eaed"
_SVG: Dict[str, str] = {
# --- transport -------------------------------------------------------
"play": '',
"pause": ''
'',
"stop": '',
"skip-back": ''
'',
"skip-forward": ''
'',
"rewind-10": ''
'10',
"forward-10": ''
'10',
# --- audio -----------------------------------------------------------
"volume-high": ''
'',
"volume-low": ''
'',
"volume-mute": ''
'',
"captions": ''
'',
# --- window ----------------------------------------------------------
"maximize": ''
'',
"minimize": ''
'',
# --- tabs / navigation -----------------------------------------------
"rss": ''
'',
"search": '',
"library": ''
'',
"download": ''
'',
"user": '',
"user-check": ''
'',
# --- actions ---------------------------------------------------------
"queue": ''
''
'',
"refresh": '',
"folder-open": ''
'',
"x": '',
"check": '',
"plus": '',
"trash": ''
''
'',
"settings": ''
''
'',
"info": ''
'',
"clock": '',
"scissors": ''
''
'',
"external-link": ''
'',
"chevron-down": '',
"chevron-right": '',
"clipboard": ''
'',
"play-circle": '',
}
_DOC = (
''
)
def available() -> list:
"""Every icon name this module can draw. Useful when adding call sites."""
return sorted(_SVG)
def _document(name: str, color: str) -> Optional[bytes]:
body = _SVG.get(name)
if body is None:
return None
return _DOC.replace("%BODY%", body).replace("%COLOR%", color).encode("utf-8")
@lru_cache(maxsize=512)
def pixmap(name: str, color: str = DEFAULT_COLOR, size: int = 20, dpr: float = 1.0) -> QPixmap:
"""One rendered pixmap. A missing name yields a transparent one, never an error."""
px = QPixmap(max(1, int(size * dpr)), max(1, int(size * dpr)))
px.setDevicePixelRatio(dpr)
px.fill(Qt.GlobalColor.transparent)
document = _document(name, color)
if document is None:
# A typo in a call site should show a gap, not take a dialog down.
logger.debug(f"Unknown icon name: {name!r}")
return px
renderer = QSvgRenderer(QByteArray(document))
if not renderer.isValid():
logger.debug(f"Icon {name!r} failed to parse")
return px
painter = QPainter(px)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
try:
renderer.render(painter, QRectF(0, 0, size * dpr, size * dpr))
finally:
painter.end()
return px
@lru_cache(maxsize=512)
def icon(name: str, color: str = DEFAULT_COLOR, size: int = 20, disabled_color: str = "#6b7075") -> QIcon:
"""
A QIcon with its own Normal and Disabled artwork.
Both 1x and 2x are added so the icon stays sharp on a HiDPI screen without
Qt upscaling a small bitmap.
"""
result = QIcon()
for dpr in (1.0, 2.0):
result.addPixmap(pixmap(name, color, size, dpr), QIcon.Mode.Normal, QIcon.State.Off)
result.addPixmap(pixmap(name, disabled_color, size, dpr), QIcon.Mode.Disabled, QIcon.State.Off)
return result