b9251290ca
This adds Python 3.9 and 3.10 typing syntax to files that import attributions from __future___. Python 3.9 should be able to cope with the 3.10 syntax, but Python 3.8 will no longer work. On Windows/Mac, install the latest Python 3.9 version from python.org. There are currently no orjson wheels for Python 3.10 on Windows/Mac, which will break the build unless you have Rust installed separately. On Linux, modern distros should have Python 3.9 available already. If you're on an older distro, you'll need to build Python from source first.
252 lines
7.6 KiB
Python
252 lines
7.6 KiB
Python
# 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 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: bool | None = None
|
|
default_palette: QPalette | None = None
|
|
|
|
# Qt applies a gradient to the buttons in dark mode
|
|
# from about #505050 to #606060.
|
|
DARK_MODE_BUTTON_BG_MIDPOINT = "#555555"
|
|
|
|
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: 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: bool | None = 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: bool | None = 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.default_palette = app.style().standardPalette()
|
|
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: {}; }}
|
|
QScrollBar::handle {{ background-color: {}; 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: {}; }}
|
|
""".format(
|
|
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()
|