From bff76727fe1270ccb56de86b35c4a813af8c22ae Mon Sep 17 00:00:00 2001 From: Matthias Metelka <62722460+kleinerpirat@users.noreply.github.com> Date: Wed, 21 Sep 2022 04:02:30 +0200 Subject: [PATCH] Make mdi icons for Qt themeable (#2078) * Fix create_vars_from_map not creating vars with default definition * Add white and black to vars * Replace some hard-coded SVGs with mdi equivalents * Implement function to dynamically adjust SVG icon color * Use new svg function to make Qt stylesheet icons respond to theme changes * Use svg function for sidebar tool icons * Create copy for each new color instead of modifying source file * Fix check fails * Add custom checkbox style for #2079 * Add example of how to generate svgs during build (dae) * Create arbitrary color variants for each icon with Bazel * Remove unused label (dae) --- qt/aqt/BUILD.bazel | 12 +- qt/aqt/browser/sidebar/toolbar.py | 12 +- qt/aqt/data/qt/icons/BUILD.bazel | 77 +++++++++- qt/aqt/data/qt/icons/chevron-down.svg | 3 - qt/aqt/data/qt/icons/chevron-up.svg | 3 - qt/aqt/data/qt/icons/color_svg.bzl | 30 ++++ qt/aqt/data/qt/icons/color_svg.py | 47 ++++++ qt/aqt/data/qt/icons/magnifying_glass.svg | 84 ----------- qt/aqt/data/qt/icons/select.svg | 168 ---------------------- qt/aqt/stylesheets.py | 83 +++++++---- qt/aqt/theme.py | 32 ++++- sass/_functions.scss | 3 + sass/_vars.scss | 18 ++- 13 files changed, 276 insertions(+), 296 deletions(-) delete mode 100644 qt/aqt/data/qt/icons/chevron-down.svg delete mode 100644 qt/aqt/data/qt/icons/chevron-up.svg create mode 100644 qt/aqt/data/qt/icons/color_svg.bzl create mode 100644 qt/aqt/data/qt/icons/color_svg.py delete mode 100644 qt/aqt/data/qt/icons/magnifying_glass.svg delete mode 100644 qt/aqt/data/qt/icons/select.svg diff --git a/qt/aqt/BUILD.bazel b/qt/aqt/BUILD.bazel index 5a60e3393..c09b9fc26 100644 --- a/qt/aqt/BUILD.bazel +++ b/qt/aqt/BUILD.bazel @@ -2,7 +2,6 @@ load("@rules_python//python:defs.bzl", "py_library") load("@py_deps//:requirements.bzl", "requirement") load("@rules_python//python:packaging.bzl", "py_package", "py_wheel") load("//:defs.bzl", "anki_version") - load("//ts:copy.bzl", "copy_files_into_group") load("//ts:compile_sass.bzl", "compile_sass") @@ -31,13 +30,22 @@ genrule( srcs = [ "_vars.css", ], - outs = ["colors.py", "props.py"], + outs = [ + "colors.py", + "props.py", + ], cmd = "$(location //qt:extract_sass_vars) $(SRCS) $(OUTS)", tools = [ "//qt:extract_sass_vars", ], ) +py_library( + name = "colors", + srcs = [":colors.py"], + visibility = ["//qt:__subpackages__"], +) + _py_srcs = glob( [ "**/*.py", diff --git a/qt/aqt/browser/sidebar/toolbar.py b/qt/aqt/browser/sidebar/toolbar.py index 865df2fdd..a111fe3e4 100644 --- a/qt/aqt/browser/sidebar/toolbar.py +++ b/qt/aqt/browser/sidebar/toolbar.py @@ -20,8 +20,16 @@ class SidebarTool(Enum): class SidebarToolbar(QToolBar): _tools: tuple[tuple[SidebarTool, str, Callable[[], str]], ...] = ( - (SidebarTool.SEARCH, "icons:magnifying_glass.svg", tr.actions_search), - (SidebarTool.SELECT, "icons:select.svg", tr.actions_select), + ( + SidebarTool.SEARCH, + "mdi:magnify", + tr.actions_search, + ), + ( + SidebarTool.SELECT, + "mdi:selection-drag", + tr.actions_select, + ), ) def __init__(self, sidebar: aqt.browser.sidebar.SidebarTreeView) -> None: diff --git a/qt/aqt/data/qt/icons/BUILD.bazel b/qt/aqt/data/qt/icons/BUILD.bazel index 9692fe8d5..4d8ed0382 100644 --- a/qt/aqt/data/qt/icons/BUILD.bazel +++ b/qt/aqt/data/qt/icons/BUILD.bazel @@ -1,4 +1,5 @@ load("//ts:vendor.bzl", "copy_mdi_icons") +load("color_svg.bzl", "color_svg") copy_mdi_icons( name = "mdi-icons", @@ -33,6 +34,19 @@ copy_mdi_icons( # tags "tag-outline.svg", "tag-off-outline.svg", + ], +) + +copy_mdi_icons( + name = "mdi-themed", + icons = [ + # sidebar tools + "magnify.svg", + "selection-drag.svg", + + # QComboBox arrows + "chevron-up.svg", + "chevron-down.svg", # QHeaderView arrows "menu-up.svg", @@ -41,12 +55,73 @@ copy_mdi_icons( # drag handle "drag-vertical.svg", "drag-horizontal.svg", + + # checkbox + "check.svg", + "minus-thick.svg", ], ) +py_binary( + name = "color_svg", + srcs = [ + "color_svg.py", + "//qt/aqt:colors", + ], + imports = ["."], + visibility = [":__subpackages__"], +) + +color_svg( + name = "magnify", +) +color_svg( + name = "selection-drag", +) +color_svg( + name = "chevron-up", + extra_colors = ["FG_DISABLED"], +) +color_svg( + name = "chevron-down", + extra_colors = ["FG_DISABLED"], +) +color_svg( + name = "menu-up", +) +color_svg( + name = "menu-down", +) +color_svg( + name = "drag-vertical", + extra_colors = ["FG_SUBTLE"], +) +color_svg( + name = "drag-horizontal", + extra_colors = ["FG_SUBTLE"], +) +color_svg( + name = "check", +) +color_svg( + name = "minus-thick", +) + filegroup( name = "icons", - srcs = ["mdi-icons"] + glob([ + srcs = [ + "mdi-icons", + "magnify", + "selection-drag", + "chevron-up", + "chevron-down", + "menu-up", + "menu-down", + "drag-vertical", + "drag-horizontal", + "check", + "minus-thick", + ] + glob([ "*.svg", "*.png", ]), diff --git a/qt/aqt/data/qt/icons/chevron-down.svg b/qt/aqt/data/qt/icons/chevron-down.svg deleted file mode 100644 index 7bd900144..000000000 --- a/qt/aqt/data/qt/icons/chevron-down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/qt/aqt/data/qt/icons/chevron-up.svg b/qt/aqt/data/qt/icons/chevron-up.svg deleted file mode 100644 index 19bdb1440..000000000 --- a/qt/aqt/data/qt/icons/chevron-up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/qt/aqt/data/qt/icons/color_svg.bzl b/qt/aqt/data/qt/icons/color_svg.bzl new file mode 100644 index 000000000..8dea97eb4 --- /dev/null +++ b/qt/aqt/data/qt/icons/color_svg.bzl @@ -0,0 +1,30 @@ +def color_svg(name, extra_colors = [], visibility = ["//qt:__submodules__"]): + native.genrule( + name = name, + srcs = ["mdi-themed"], + outs = [ + name + "-light.svg", + ] + [ + # additional light colors + "{}{}{}".format( + name, + "-{}".format(color), + "-light.svg" + ) for color in extra_colors + ] + [ + name + "-dark.svg", + ] + [ + # additional dark colors + "{}{}{}".format( + name, + "-{}".format(color), + "-dark.svg" + ) for color in extra_colors + ], + cmd = "$(location color_svg) {}.svg {} $(OUTS) $(SRCS)".format( + name, ":".join(["FG"] + extra_colors) + ), + tools = [ + "color_svg", + ], + ) diff --git a/qt/aqt/data/qt/icons/color_svg.py b/qt/aqt/data/qt/icons/color_svg.py new file mode 100644 index 000000000..2fc33a7fc --- /dev/null +++ b/qt/aqt/data/qt/icons/color_svg.py @@ -0,0 +1,47 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import re +import sys +from pathlib import Path + +from qt.aqt import colors + +input_filename = sys.argv[1] +input_name = input_filename.replace(".svg", "") +color_names = sys.argv[2].split(":") + +# two files created for each additional color +offset = len(color_names) * 2 +svg_paths = sys.argv[3 : 3 + offset] + +# as we've received a group of files, we need to manually join the path +input_folder = Path(sys.argv[4]).parent +input_svg = input_folder / input_filename + +with open(input_svg, "r") as f: + svg_data = f.read() + + for color_name in color_names: + color = getattr(colors, color_name) + light_svg = dark_svg = "" + + if color_name == "FG": + prefix = input_name + else: + prefix = f"{input_name}-{color_name}" + + for path in svg_paths: + if f"{prefix}-light.svg" in path: + light_svg = path + elif f"{prefix}-dark.svg" in path: + dark_svg = path + + for (idx, filename) in enumerate((light_svg, dark_svg)): + data = svg_data + if "fill" in data: + data = re.sub(r"fill=\"#.+?\"", f'fill="{color[idx]}"', data) + else: + data = re.sub(r" - - - - - - - - - image/svg+xml - - - - - - - - - - - - - diff --git a/qt/aqt/data/qt/icons/select.svg b/qt/aqt/data/qt/icons/select.svg deleted file mode 100644 index fe4ee8c67..000000000 --- a/qt/aqt/data/qt/icons/select.svg +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - diff --git a/qt/aqt/stylesheets.py b/qt/aqt/stylesheets.py index 025f52229..7d354e010 100644 --- a/qt/aqt/stylesheets.py +++ b/qt/aqt/stylesheets.py @@ -100,19 +100,19 @@ QComboBox:!editable:pressed {{ def splitter_styles(tm: ThemeManager, buf: str) -> str: - buf += """ + buf += f""" QSplitter::handle, -QMainWindow::separator { +QMainWindow::separator {{ height: 16px; -} +}} QSplitter::handle:vertical, -QMainWindow::separator:horizontal { - image: url(icons:drag-horizontal.svg); -} +QMainWindow::separator:horizontal {{ + image: url({tm.themed_icon("mdi:drag-horizontal-FG_SUBTLE")}); +}} QSplitter::handle:horizontal, -QMainWindow::separator:vertical { - image: url(icons:drag-vertical.svg); -} +QMainWindow::separator:vertical {{ + image: url({tm.themed_icon("mdi:drag-vertical-FG_SUBTLE")}); +}} """ return buf @@ -156,21 +156,21 @@ QComboBox::drop-down {{ border-bottom-right-radius: {tm.var(props.BORDER_RADIUS)}; }} QComboBox::down-arrow {{ - image: url(icons:chevron-down.svg); + image: url({tm.themed_icon("mdi:chevron-down")}); }} QComboBox::drop-down {{ background: { button_gradient( - tm.var(colors.BUTTON_PRIMARY_GRADIENT_START), - tm.var(colors.BUTTON_PRIMARY_GRADIENT_END) + tm.var(colors.BUTTON_GRADIENT_START), + tm.var(colors.BUTTON_GRADIENT_END) ) }; }} QComboBox::drop-down:hover {{ background: { button_gradient( - tm.var(colors.BUTTON_PRIMARY_HOVER_GRADIENT_START), - tm.var(colors.BUTTON_PRIMARY_HOVER_GRADIENT_END) + tm.var(colors.BUTTON_HOVER_GRADIENT_START), + tm.var(colors.BUTTON_HOVER_GRADIENT_END) ) }; }} @@ -288,10 +288,10 @@ QHeaderView::down-arrow {{ height: 20px; }} QHeaderView::up-arrow {{ - image: url(icons:menu-up.svg); + image: url({tm.themed_icon("mdi:menu-up")}); }} QHeaderView::down-arrow {{ - image: url(icons:menu-down.svg); + image: url({tm.themed_icon("mdi:menu-down")}); }} """ return buf @@ -306,8 +306,8 @@ QSpinBox::down-button {{ border: 1px solid {tm.var(colors.BUTTON_BORDER)}; background: { button_gradient( - tm.var(colors.BUTTON_PRIMARY_GRADIENT_START), - tm.var(colors.BUTTON_PRIMARY_GRADIENT_END) + tm.var(colors.BUTTON_GRADIENT_START), + tm.var(colors.BUTTON_GRADIENT_END) ) }; }} @@ -316,8 +316,8 @@ QSpinBox::down-button:pressed {{ border: 1px solid {tm.var(colors.BUTTON_PRESSED_BORDER)}; background: { button_pressed_gradient( - tm.var(colors.BUTTON_PRIMARY_GRADIENT_START), - tm.var(colors.BUTTON_PRIMARY_GRADIENT_END), + tm.var(colors.BUTTON_GRADIENT_START), + tm.var(colors.BUTTON_GRADIENT_END), tm.var(colors.BUTTON_PRESSED_SHADOW) ) } @@ -326,8 +326,8 @@ QSpinBox::up-button:hover, QSpinBox::down-button:hover {{ background: { button_gradient( - tm.var(colors.BUTTON_PRIMARY_HOVER_GRADIENT_START), - tm.var(colors.BUTTON_PRIMARY_HOVER_GRADIENT_END) + tm.var(colors.BUTTON_HOVER_GRADIENT_START), + tm.var(colors.BUTTON_HOVER_GRADIENT_END) ) }; }} @@ -342,10 +342,10 @@ QSpinBox::down-button {{ border-bottom-right-radius: {tm.var(props.BORDER_RADIUS)}; }} QSpinBox::up-arrow {{ - image: url(icons:chevron-up.svg); + image: url({tm.themed_icon("mdi:chevron-up")}); }} QSpinBox::down-arrow {{ - image: url(icons:chevron-down.svg); + image: url({tm.themed_icon("mdi:chevron-down")}); }} QSpinBox::up-arrow, QSpinBox::down-arrow, @@ -363,12 +363,45 @@ QSpinBox::down-arrow:hover {{ }} QSpinBox::up-button:disabled, QSpinBox::up-button:off, QSpinBox::down-button:disabled, QSpinBox::down-button:off {{ - background: {tm.var(colors.BUTTON_PRIMARY_DISABLED)}; + background: {tm.var(colors.BUTTON_DISABLED)}; +}} +QSpinBox::up-arrow:off, +QSpinBox::down-arrow:off {{ + image: url({tm.themed_icon("mdi:chevron-down-FG_DISABLED")}); }} """ return buf +def checkbox_styles(tm: ThemeManager, buf: str) -> str: + buf += f""" +QCheckBox {{ + spacing: 8px; + margin: 2px 0; +}} +QCheckBox::indicator {{ + border: 1px solid {tm.var(colors.BUTTON_BORDER)}; + border-radius: {tm.var(props.BORDER_RADIUS)}; + background: {tm.var(colors.CANVAS_INSET)}; + width: 16px; + height: 16px; +}} +QCheckBox::indicator:hover, +QCheckBox::indicator:checked:hover {{ + border: 2px solid {tm.var(colors.BORDER_STRONG)}; + width: 14px; + height: 14px; +}} +QCheckBox::indicator:checked {{ + image: url({tm.themed_icon("mdi:check")}); +}} +QCheckBox::indicator:indeterminate {{ + image: url({tm.themed_icon("mdi:minus-thick")}); +}} + """ + return buf + + def scrollbar_styles(tm: ThemeManager, buf: str) -> str: buf += f""" QAbstractScrollArea::corner {{ diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py index c2e83b546..8cdc90868 100644 --- a/qt/aqt/theme.py +++ b/qt/aqt/theme.py @@ -4,7 +4,9 @@ from __future__ import annotations import enum +import os import platform +import re import subprocess from dataclasses import dataclass from typing import Callable, List, Tuple @@ -81,8 +83,21 @@ class ThemeManager: night_mode = property(get_night_mode, set_night_mode) + def themed_icon(self, path: str) -> str: + "Fetch themed version of svg." + from aqt.utils import aqt_data_folder + + if m := re.match(r"(?:mdi:)(.+)$", path): + name = m.group(1) + else: + return path + + filename = f"{name}-{'dark' if self.night_mode else 'light'}.svg" + + return os.path.join(aqt_data_folder(), "qt", "icons", filename) + def icon_from_resources(self, path: str | ColoredIcon) -> QIcon: - "Fetch icon from Qt resources, and invert if in night mode." + "Fetch icon from Qt resources." if self.night_mode: cache = self._icon_cache_light else: @@ -99,11 +114,14 @@ class ThemeManager: 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)) + if "mdi:" in path: + icon = QIcon(self.themed_icon(path)) + else: + 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) @@ -193,6 +211,7 @@ class ThemeManager: if not is_mac: from aqt.stylesheets import ( button_styles, + checkbox_styles, combobox_styles, general_styles, scrollbar_styles, @@ -210,6 +229,7 @@ class ThemeManager: tabwidget_styles(self, buf), table_styles(self, buf), spinbox_styles(self, buf), + checkbox_styles(self, buf), scrollbar_styles(self, buf), ] ) diff --git a/sass/_functions.scss b/sass/_functions.scss index 3b1363f15..a639b4e4c 100644 --- a/sass/_functions.scss +++ b/sass/_functions.scss @@ -22,6 +22,9 @@ ); } } + @else if $key == "default" { + @return map.set($output, $name, map.get($map, $key)); + } } @return $output; } diff --git a/sass/_vars.scss b/sass/_vars.scss index 0cd59a402..3d728bb62 100644 --- a/sass/_vars.scss +++ b/sass/_vars.scss @@ -18,6 +18,12 @@ $vars: ( ), ), colors: ( + white: ( + default: white, + ), + black: ( + default: black, + ), fg: ( default: ( light: palette(darkgray, 9), @@ -28,8 +34,8 @@ $vars: ( dark: palette(lightgray, 3), ), disabled: ( - light: palette(darkgray, 3), - dark: palette(lightgray, 6), + light: palette(darkgray, 2), + dark: palette(lightgray, 8), ), faint: ( light: palette(lightgray, 7), @@ -67,6 +73,10 @@ $vars: ( light: palette(lightgray, 5), dark: palette(darkgray, 4), ), + strong: ( + light: palette(lightgray, 9), + dark: palette(darkgray, 1), + ), focus: ( light: palette(blue, 5), dark: palette(blue, 5), @@ -91,6 +101,10 @@ $vars: ( dark: palette(darkgray, 9), ), ), + disabled: ( + light: color.scale(palette(lightgray, 5), $alpha: -50%), + dark: color.scale(palette(darkgray, 3), $alpha: -50%), + ), gradient: ( start: ( light: white,