anki/qt/aqt/theme.py
Damien Elmes 65cfcf9226 pare back dark mode support
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.
2020-04-15 21:44:56 +10:00

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()