65cfcf9226
Anki now solely relies on the night mode setting in the preferences to decide whether to show in light or dark mode. Some users wanted to run Anki in light mode while keeping the rest of their system dark, and there were various display problems when dark mode was changed after Anki started that couldn't be easily worked around. NSRequiresAquaAppearance is set again, which means we can rely on the interface appearing properly and not changing as the macOS theme is changed. Users who only use dark mode, and preferred the native look of widgets in dark mode, can achieve the previous appearance by running the following command in the terminal: defaults write net.ankiweb.dtop NSRequiresAquaSystemAppearance -bool no And the following in the debug console: mw.pm.meta["dark_mode_widgets"] = True This is hidden behind a debug console command because it requires the user ensure their system is always set to the same light/dark mode as Anki.
208 lines
6.2 KiB
Python
208 lines
6.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
import platform
|
|
from typing import Dict
|
|
|
|
from anki.utils import isMac
|
|
from aqt import QApplication, gui_hooks, isWin
|
|
from aqt.colors import colors
|
|
from aqt.qt import QColor, QIcon, QPalette, QPixmap, QStyleFactory, Qt
|
|
|
|
|
|
class ThemeManager:
|
|
_night_mode_preference = False
|
|
_icon_cache_light: Dict[str, QIcon] = {}
|
|
_icon_cache_dark: Dict[str, QIcon] = {}
|
|
_icon_size = 128
|
|
|
|
def macos_dark_mode(self) -> bool:
|
|
"True if the user has night mode on, and has forced native widgets."
|
|
if not isMac:
|
|
return False
|
|
|
|
from aqt import mw
|
|
|
|
return self._night_mode_preference 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) -> 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
|
|
icon = cache.get(path)
|
|
if icon:
|
|
return icon
|
|
|
|
icon = QIcon(path)
|
|
|
|
if self.night_mode:
|
|
img = icon.pixmap(self._icon_size, self._icon_size).toImage()
|
|
img.invertPixels()
|
|
icon = QIcon(QPixmap(img))
|
|
|
|
return cache.setdefault(path, icon)
|
|
|
|
def body_class(self) -> 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 self.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) -> str:
|
|
"Returns body classes used when showing a card."
|
|
return f"card card{card_ord+1} {self.body_class()}"
|
|
|
|
def str_color(self, key: str) -> str:
|
|
"""Get a color defined in _vars.scss
|
|
|
|
If the colour is called '$day-frame-bg', key should be
|
|
'frame-bg'.
|
|
|
|
Returns the color as a string hex code or color name."""
|
|
prefix = self.night_mode and "night-" or "day-"
|
|
c = colors.get(prefix + key)
|
|
if c is None:
|
|
raise Exception("no such color:", key)
|
|
return c
|
|
|
|
def qcolor(self, key: str) -> QColor:
|
|
"""Get a color defined in _vars.scss as a QColor."""
|
|
return QColor(self.str_color(key))
|
|
|
|
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.str_color("window-bg"),
|
|
colors.get("fusion-button-hover-bg"),
|
|
self.str_color("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("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("highlight-bg")
|
|
hlbg.setAlpha(64)
|
|
palette.setColor(QPalette.HighlightedText, self.qcolor("highlight-fg"))
|
|
palette.setColor(QPalette.Highlight, hlbg)
|
|
|
|
window_bg = self.qcolor("window-bg")
|
|
palette.setColor(QPalette.Window, window_bg)
|
|
palette.setColor(QPalette.AlternateBase, window_bg)
|
|
|
|
palette.setColor(QPalette.Button, QColor(colors.get("fusion-button-base-bg")))
|
|
|
|
frame_bg = self.qcolor("frame-bg")
|
|
palette.setColor(QPalette.Base, frame_bg)
|
|
palette.setColor(QPalette.ToolTipBase, frame_bg)
|
|
|
|
disabled_color = self.qcolor("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("link"))
|
|
|
|
palette.setColor(QPalette.BrightText, Qt.red)
|
|
|
|
app.setPalette(palette)
|
|
|
|
def _update_stat_colors(self) -> None:
|
|
import anki.stats as s
|
|
|
|
s.colLearn = self.str_color("new-count")
|
|
s.colRelearn = self.str_color("learn-count")
|
|
s.colCram = self.str_color("suspended-bg")
|
|
s.colSusp = self.str_color("suspended-bg")
|
|
s.colMature = self.str_color("review-count")
|
|
|
|
|
|
theme_manager = ThemeManager()
|