""" 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 = ( '%BODY%' ) 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