# Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import annotations import platform from dataclasses import dataclass from typing import Dict, Optional, Tuple, Union from anki.utils import isMac from aqt import QApplication, colors, gui_hooks, isWin from aqt.platform import set_dark_mode from aqt.qt import QColor, QIcon, QPainter, QPalette, QPixmap, QStyleFactory, Qt @dataclass class ColoredIcon: path: str # (day, night) color: Tuple[str, str] def current_color(self, night_mode: bool) -> str: if night_mode: return self.color[1] else: return self.color[0] def with_color(self, color: Tuple[str, str]) -> ColoredIcon: return ColoredIcon(path=self.path, color=color) class ThemeManager: _night_mode_preference = False _icon_cache_light: Dict[str, QIcon] = {} _icon_cache_dark: Dict[str, QIcon] = {} _icon_size = 128 _dark_mode_available: Optional[bool] = None def macos_dark_mode(self) -> bool: "True if the user has night mode on, and has forced native widgets." if not isMac: return False if not self._night_mode_preference: return False if self._dark_mode_available is None: self._dark_mode_available = set_dark_mode(True) from aqt import mw return self._dark_mode_available and mw.pm.dark_mode_widgets() def get_night_mode(self) -> bool: return self._night_mode_preference def set_night_mode(self, val: bool) -> None: self._night_mode_preference = val self._update_stat_colors() night_mode = property(get_night_mode, set_night_mode) def icon_from_resources(self, path: Union[str, ColoredIcon]) -> QIcon: "Fetch icon from Qt resources, and invert if in night mode." if self.night_mode: cache = self._icon_cache_light else: cache = self._icon_cache_dark if isinstance(path, str): key = path else: key = f"{path.path}-{path.color}" icon = cache.get(key) if icon: return icon if isinstance(path, str): # default black/white icon = QIcon(path) if self.night_mode: img = icon.pixmap(self._icon_size, self._icon_size).toImage() img.invertPixels() icon = QIcon(QPixmap(img)) else: # specified colours icon = QIcon(path.path) pixmap = icon.pixmap(16) painter = QPainter(pixmap) painter.setCompositionMode(QPainter.CompositionMode_SourceIn) painter.fillRect(pixmap.rect(), QColor(path.current_color(self.night_mode))) painter.end() icon = QIcon(pixmap) return icon return cache.setdefault(path, icon) def body_class(self, night_mode: Optional[bool] = None) -> str: "Returns space-separated class list for platform/theme." classes = [] if isWin: classes.append("isWin") elif isMac: classes.append("isMac") else: classes.append("isLin") if night_mode is None: night_mode = self.night_mode if night_mode: classes.extend(["nightMode", "night_mode"]) if self.macos_dark_mode(): classes.append("macos-dark-mode") return " ".join(classes) def body_classes_for_card_ord( self, card_ord: int, night_mode: Optional[bool] = None ) -> str: "Returns body classes used when showing a card." return f"card card{card_ord+1} {self.body_class(night_mode)}" def color(self, colors: Tuple[str, str]) -> str: """Given day/night colors, return the correct one for the current theme.""" idx = 1 if self.night_mode else 0 return colors[idx] def qcolor(self, colors: Tuple[str, str]) -> QColor: return QColor(self.color(colors)) def apply_style(self, app: QApplication) -> None: self._apply_palette(app) self._apply_style(app) def _apply_style(self, app: QApplication) -> None: buf = "" if isWin and platform.release() == "10" and not self.night_mode: # add missing bottom border to menubar buf += """ QMenuBar { border-bottom: 1px solid #aaa; background: white; } """ # qt bug? setting the above changes the browser sidebar # to white as well, so set it back buf += """ QTreeWidget { background: #eee; } """ if self.night_mode: buf += """ QToolTip { border: 0; } """ if not self.macos_dark_mode(): buf += """ QScrollBar { background-color: %s; } QScrollBar::handle { background-color: %s; border-radius: 5px; } QScrollBar:horizontal { height: 12px; } QScrollBar::handle:horizontal { min-width: 50px; } QScrollBar:vertical { width: 12px; } QScrollBar::handle:vertical { min-height: 50px; } QScrollBar::add-line { border: none; background: none; } QScrollBar::sub-line { border: none; background: none; } QTabWidget { background-color: %s; } """ % ( self.color(colors.WINDOW_BG), # fushion-button-hover-bg "#656565", self.color(colors.WINDOW_BG), ) # allow addons to modify the styling buf = gui_hooks.style_did_init(buf) app.setStyleSheet(buf) def _apply_palette(self, app: QApplication) -> None: if not self.night_mode: return if not self.macos_dark_mode(): app.setStyle(QStyleFactory.create("fusion")) # type: ignore palette = QPalette() text_fg = self.qcolor(colors.TEXT_FG) palette.setColor(QPalette.WindowText, text_fg) palette.setColor(QPalette.ToolTipText, text_fg) palette.setColor(QPalette.Text, text_fg) palette.setColor(QPalette.ButtonText, text_fg) hlbg = self.qcolor(colors.HIGHLIGHT_BG) hlbg.setAlpha(64) palette.setColor(QPalette.HighlightedText, self.qcolor(colors.HIGHLIGHT_FG)) palette.setColor(QPalette.Highlight, hlbg) window_bg = self.qcolor(colors.WINDOW_BG) palette.setColor(QPalette.Window, window_bg) palette.setColor(QPalette.AlternateBase, window_bg) palette.setColor(QPalette.Button, QColor("#454545")) frame_bg = self.qcolor(colors.FRAME_BG) palette.setColor(QPalette.Base, frame_bg) palette.setColor(QPalette.ToolTipBase, frame_bg) disabled_color = self.qcolor(colors.DISABLED) palette.setColor(QPalette.Disabled, QPalette.Text, disabled_color) palette.setColor(QPalette.Disabled, QPalette.ButtonText, disabled_color) palette.setColor(QPalette.Disabled, QPalette.HighlightedText, disabled_color) palette.setColor(QPalette.Link, self.qcolor(colors.LINK)) palette.setColor(QPalette.BrightText, Qt.red) app.setPalette(palette) def _update_stat_colors(self) -> None: import anki.stats as s s.colLearn = self.color(colors.NEW_COUNT) s.colRelearn = self.color(colors.LEARN_COUNT) s.colCram = self.color(colors.SUSPENDED_BG) s.colSusp = self.color(colors.SUSPENDED_BG) s.colMature = self.color(colors.REVIEW_COUNT) theme_manager = ThemeManager()