""" Theme tokens and the stylesheet rules the app never had ======================================================= `StyleSheet.MAIN` (upstream's, in ytsage_stylesheet.py) styles the window, line edits, buttons, tables, progress bars and vertical scrollbars -- and nothing else. Everything it misses falls through to `QWidget { background: #15181b; color: #ffffff }` plus the platform style, which is why the app looked half-finished: the main tab bar was drawn by Fusion from the system palette with white text forced onto it, combo boxes and sliders were native, tooltips came out in the system's light style on a black app, and a native light horizontal scrollbar appeared under any wide view. Notably `SmoothTabWidget` names its content frame `tabContent` with the comment "We draw border on content instead" -- and the only `QFrame#tabContent` rules in the codebase were inside two dialogs. The main window's tab bar got none of it. This module holds the colour tokens and `EXTRA_QSS`, appended to `StyleSheet.MAIN` at the single application site. Keeping it separate means one changed line in an upstream-owned file, so the next merge from YTSage does not fight over it. The two dialogs that define their own tab rules keep them. Unifying would cost more in merge conflicts than the duplication does. """ from ..utils.ytsage_logger import logger # --- colour tokens ------------------------------------------------------- # Names, not hexes, at the call sites. These match what StyleSheet.MAIN # already uses so the two halves agree. BG = "#15181b" # window SURFACE = "#1b2021" # panels, inputs, list backgrounds SURFACE_ALT = "#1d2124" # menus, tooltips, elevated surfaces SURFACE_HOVER = "#252a2d" BORDER = "#2a2d2e" BORDER_STRONG = "#3d3d3d" TEXT = "#ffffff" TEXT_MUTED = "#9aa0a6" TEXT_DISABLED = "#6b7075" ACCENT = "#c90000" ACCENT_HOVER = "#a50000" ACCENT_PRESSED = "#800000" FOCUS = "#ff6b6b" #: Default icon colour: near-white, deliberately not pure #ffffff so it does #: not out-glare the text beside it. ICON = "#e8eaed" #: Icons sitting on an accent-coloured button. ICON_ON_ACCENT = "#ffffff" def ensure_ui_assets() -> dict: """ Write the few bitmaps Qt stylesheets can only reference as files. QSS cannot draw a shape: `::down-arrow` and `::indicator` need an actual `image:`. Everything else here is drawn with borders and radii, but a caret and a tick are not expressible that way -- restyling a combo box without supplying one leaves it with a blank square where its arrow was. Rendered from the same SVG set as every other icon and cached next to the thumbnails, so nothing extra ships and nothing is fetched. """ from . import ytsage_icons as icons from ..utils.ytsage_constants import APP_DATA_DIR out_dir = APP_DATA_DIR / "ui" # Rendered larger than the box they are drawn into (see the QSS below): # a 24-unit viewBox squeezed straight into 12px leaves a 1px stroke that # all but disappears on this background. wanted = { "chevron": ("chevron-down", TEXT_MUTED, 14), "chevron_disabled": ("chevron-down", TEXT_DISABLED, 14), "check": ("check", TEXT, 12), } paths = {} try: out_dir.mkdir(parents=True, exist_ok=True) for key, (name, color, size) in wanted.items(): target = out_dir / f"{key}.png" if not target.exists(): # dpr 1.0 and the final pixel size: Qt draws a QSS `image:` at # its natural size, so anything else is scaled or clipped. icons.pixmap(name, color, size, 1.0).save(str(target), "PNG") # QSS wants forward slashes even on Windows. paths[key] = str(target).replace("\\", "/") except Exception as e: # pragma: no cover - cosmetic only logger.debug(f"Could not write UI assets, falling back to plain styling: {e}") return {} return paths def build_extra_qss() -> str: """EXTRA_QSS plus the rules that need generated images, when available.""" qss = EXTRA_QSS assets = ensure_ui_assets() if not assets: return qss return qss + f""" QComboBox::down-arrow {{ image: url("{assets['chevron']}"); border: none; }} QComboBox::down-arrow:disabled {{ image: url("{assets['chevron_disabled']}"); }} /* Upstream styles checkboxes as filled circles (border-radius: 9px), which reads as a radio button -- a control that means "one of these" rather than "on or off". Square them off and show an actual tick. Widget-level stylesheets win over this one, so StyleSheet.CHECKBOX is corrected at source; these rules cover every checkbox that does not use it. */ QCheckBox::indicator {{ width: 16px; height: 16px; border-radius: 4px; border: 2px solid {BORDER_STRONG}; background-color: {SURFACE}; }} QCheckBox::indicator:hover {{ border-color: {TEXT_MUTED}; }} QCheckBox::indicator:checked {{ border-color: {ACCENT}; background-color: {ACCENT}; image: url("{assets['check']}"); }} QCheckBox::indicator:checked:hover {{ border-color: {FOCUS}; }} QCheckBox::indicator:disabled {{ border-color: {BORDER}; background-color: {BG}; }} """ EXTRA_QSS = f""" /* ---- main tab bar (SmoothTabWidget) --------------------------------- */ QTabBar {{ background-color: {BG}; border: none; }} QTabBar::tab {{ background-color: transparent; color: {TEXT_MUTED}; padding: 9px 18px; margin-right: 2px; border: none; border-bottom: 2px solid transparent; font-size: 13px; }} QTabBar::tab:hover:!selected {{ color: {TEXT}; background-color: {SURFACE}; }} QTabBar::tab:selected {{ color: {TEXT}; background-color: {SURFACE}; border-bottom: 2px solid {ACCENT}; }} QTabBar::tab:disabled {{ color: {TEXT_DISABLED}; }} QFrame#tabContent {{ background-color: {BG}; border: none; border-top: 1px solid {BORDER}; }} /* ---- combo boxes ---------------------------------------------------- */ QComboBox {{ background-color: {SURFACE}; color: {TEXT}; border: 1px solid {BORDER}; border-radius: 4px; padding: 5px 10px; min-height: 18px; }} QComboBox:hover {{ border-color: {BORDER_STRONG}; }} QComboBox:focus {{ border-color: {FOCUS}; }} QComboBox:disabled {{ color: {TEXT_DISABLED}; background-color: {BG}; }} QComboBox::drop-down {{ border: none; width: 22px; subcontrol-origin: padding; subcontrol-position: center right; }} /* ::down-arrow is deliberately not styled here -- QSS cannot draw a caret, and setting only a border leaves a small square where the arrow was. build_extra_qss() supplies a real image; without it Qt's native arrow is the better fallback. */ QComboBox QAbstractItemView {{ background-color: {SURFACE_ALT}; color: {TEXT}; border: 1px solid {BORDER}; selection-background-color: {ACCENT}; selection-color: {TEXT}; outline: none; padding: 2px; }} /* ---- lists ---------------------------------------------------------- */ QListWidget, QListView {{ background-color: {SURFACE}; color: {TEXT}; border: 1px solid {BORDER}; border-radius: 4px; outline: none; }} QListWidget::item, QListView::item {{ padding: 6px 8px; border-radius: 3px; }} QListWidget::item:hover, QListView::item:hover {{ background-color: {SURFACE_HOVER}; }} QListWidget::item:selected, QListView::item:selected {{ background-color: {ACCENT}; color: {TEXT}; }} /* ---- menus ---------------------------------------------------------- */ QMenu {{ background-color: {SURFACE_ALT}; color: {TEXT}; border: 1px solid {BORDER}; padding: 4px; }} QMenu::item {{ padding: 6px 22px 6px 14px; border-radius: 3px; }} QMenu::item:selected {{ background-color: {ACCENT}; }} QMenu::item:disabled {{ color: {TEXT_DISABLED}; }} QMenu::separator {{ height: 1px; background-color: {BORDER}; margin: 4px 6px; }} /* ---- sliders (seek and volume were fully native) --------------------- */ QSlider::groove:horizontal {{ height: 4px; background-color: {BORDER}; border-radius: 2px; }} QSlider::sub-page:horizontal {{ background-color: {ACCENT}; border-radius: 2px; }} QSlider::handle:horizontal {{ background-color: {TEXT}; width: 12px; height: 12px; margin: -5px 0; border-radius: 6px; }} QSlider::handle:horizontal:hover {{ background-color: {FOCUS}; }} QSlider::handle:horizontal:disabled {{ background-color: {TEXT_DISABLED}; }} QSlider::groove:vertical {{ width: 4px; background-color: {BORDER}; border-radius: 2px; }} QSlider::handle:vertical {{ background-color: {TEXT}; height: 12px; margin: 0 -5px; border-radius: 6px; }} /* ---- splitters ------------------------------------------------------- */ QSplitter::handle {{ background-color: {BORDER}; }} QSplitter::handle:horizontal {{ width: 4px; }} QSplitter::handle:vertical {{ height: 4px; }} QSplitter::handle:hover {{ background-color: {ACCENT}; }} /* ---- tooltips (were rendering in the system's light style) ----------- */ QToolTip {{ background-color: {SURFACE_ALT}; color: {TEXT}; border: 1px solid {BORDER}; padding: 5px 8px; border-radius: 4px; }} /* ---- horizontal scrollbar (only the vertical one was styled) --------- */ QScrollBar:horizontal {{ background-color: {BG}; height: 12px; margin: 0; border: none; }} QScrollBar::handle:horizontal {{ background-color: {BORDER_STRONG}; min-width: 24px; border-radius: 6px; }} QScrollBar::handle:horizontal:hover {{ background-color: {ACCENT}; }} QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{ width: 0; border: none; background: none; }} QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {{ background: none; }} /* ---- assorted controls left unstyled --------------------------------- */ QRadioButton, QCheckBox {{ color: {TEXT}; spacing: 7px; }} QRadioButton:disabled, QCheckBox:disabled {{ color: {TEXT_DISABLED}; }} QGroupBox {{ border: 1px solid {BORDER}; border-radius: 5px; margin-top: 10px; padding-top: 8px; color: {TEXT}; }} QGroupBox::title {{ subcontrol-origin: margin; left: 10px; padding: 0 5px; color: {TEXT_MUTED}; }} QSpinBox, QDoubleSpinBox {{ background-color: {SURFACE}; color: {TEXT}; border: 1px solid {BORDER}; border-radius: 4px; padding: 4px 6px; }} /* ---- corrections to rules that already exist ------------------------- */ /* Upstream's QPushButton:pressed changes the padding, which shifts every label two pixels down-and-right on click and clips the artwork on the fixed-width icon buttons. The background change alone reads as pressed. */ QPushButton:pressed {{ padding: 8px 15px; }} /* One consistent icon size everywhere, since icons are now real artwork. */ QPushButton, QToolButton {{ qproperty-iconSize: 18px 18px; }} QToolButton {{ background-color: transparent; border: none; border-radius: 4px; padding: 5px; color: {TEXT}; }} QToolButton:hover {{ background-color: {SURFACE_HOVER}; }} QToolButton:pressed {{ background-color: {BORDER}; }} """