From 9ccda1c52594ca666a958483b65d38c79e6aa1bd Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Sat, 24 Feb 2024 13:03:48 +0100 Subject: [PATCH] feat: Add a fallback theme to custom themes If no fallback theme is specified by the custom theme, the fallback theme will be "Dark" --- CHANGELOG.md | 1 + docs/ChatterinoTheme.schema.json | 5 ++ src/singletons/Theme.cpp | 119 +++++++++++++++++++++++-------- src/singletons/Theme.hpp | 2 +- 4 files changed, 95 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cc0f93699b..ba7f2355852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - Minor: Live streams that are marked as reruns now mark a tab as yellow instead of red. (#5176) - Minor: Updated to Emoji v15.1. Google emojis are now used as the fallback instead of Twitter emojis. (#5182) - Minor: Allow theming of tab live and rerun indicators. (#5188) +- Minor: Added a fallback theme field to custom themes that will be used in case the custom theme does not contain a property we want. If no fallback theme is specified, we'l pull the property from the included Dark theme. (#5198) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) - Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834) diff --git a/docs/ChatterinoTheme.schema.json b/docs/ChatterinoTheme.schema.json index 30f4a7945ad..462a330d161 100644 --- a/docs/ChatterinoTheme.schema.json +++ b/docs/ChatterinoTheme.schema.json @@ -390,6 +390,11 @@ "$comment": "Determines which icons to use. 'dark' will use dark icons (best for a light theme). 'light' will use light icons.", "enum": ["light", "dark"], "default": "light" + }, + "fallbackTheme": { + "$comment": "Determined which built-in Chatterino theme to use as a fallback in case a color isn't configured.", + "enum": ["White", "Light", "Dark", "Black"], + "default": "Dark" } }, "required": ["iconTheme"] diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index f7d3b0800ff..4930f082712 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -25,15 +25,21 @@ namespace { using namespace chatterino; using namespace literals; -void parseInto(const QJsonObject &obj, QLatin1String key, QColor &color) +void parseInto(const QJsonObject &obj, const QJsonObject &fallbackObj, + QLatin1String key, QColor &color) { - const auto &jsonValue = obj[key]; + auto jsonValue = obj[key]; if (!jsonValue.isString()) [[unlikely]] { - qCWarning(chatterinoTheme) << key - << "was expected but not found in the " - "current theme - using previous value."; - return; + jsonValue = fallbackObj[key]; + if (!jsonValue.isString()) [[unlikely]] + { + qCWarning(chatterinoTheme) + << key + << "was expected but not found in the " + "current theme, and no fallback value found."; + return; + } } QColor parsed = {jsonValue.toString()}; if (!parsed.isValid()) [[unlikely]] @@ -49,27 +55,33 @@ void parseInto(const QJsonObject &obj, QLatin1String key, QColor &color) // NOLINTBEGIN(cppcoreguidelines-macro-usage) #define _c2StringLit(s, ty) s##ty #define parseColor(to, from, key) \ - parseInto(from, _c2StringLit(#key, _L1), (to).from.key) + parseInto(from, from##Fallback, _c2StringLit(#key, _L1), (to).from.key) // NOLINTEND(cppcoreguidelines-macro-usage) -void parseWindow(const QJsonObject &window, chatterino::Theme &theme) +void parseWindow(const QJsonObject &window, const QJsonObject &windowFallback, + chatterino::Theme &theme) { parseColor(theme, window, background); parseColor(theme, window, text); } -void parseTabs(const QJsonObject &tabs, chatterino::Theme &theme) +void parseTabs(const QJsonObject &tabs, const QJsonObject &tabsFallback, + chatterino::Theme &theme) { - const auto parseTabColors = [](const auto &json, auto &tab) { - parseInto(json, "text"_L1, tab.text); + const auto parseTabColors = [](const auto &json, const auto &jsonFallback, + auto &tab) { + parseInto(json, jsonFallback, "text"_L1, tab.text); { const auto backgrounds = json["backgrounds"_L1].toObject(); + const auto backgroundsFallback = + jsonFallback["backgrounds"_L1].toObject(); parseColor(tab, backgrounds, regular); parseColor(tab, backgrounds, hover); parseColor(tab, backgrounds, unfocused); } { const auto line = json["line"_L1].toObject(); + const auto lineFallback = jsonFallback["line"_L1].toObject(); parseColor(tab, line, regular); parseColor(tab, line, hover); parseColor(tab, line, unfocused); @@ -78,16 +90,26 @@ void parseTabs(const QJsonObject &tabs, chatterino::Theme &theme) parseColor(theme, tabs, dividerLine); parseColor(theme, tabs, liveIndicator); parseColor(theme, tabs, rerunIndicator); - parseTabColors(tabs["regular"_L1].toObject(), theme.tabs.regular); - parseTabColors(tabs["newMessage"_L1].toObject(), theme.tabs.newMessage); - parseTabColors(tabs["highlighted"_L1].toObject(), theme.tabs.highlighted); - parseTabColors(tabs["selected"_L1].toObject(), theme.tabs.selected); + parseTabColors(tabs["regular"_L1].toObject(), + tabsFallback["regular"_L1].toObject(), theme.tabs.regular); + parseTabColors(tabs["newMessage"_L1].toObject(), + tabsFallback["newMessage"_L1].toObject(), + theme.tabs.newMessage); + parseTabColors(tabs["highlighted"_L1].toObject(), + tabsFallback["highlighted"_L1].toObject(), + theme.tabs.highlighted); + parseTabColors(tabs["selected"_L1].toObject(), + tabsFallback["selected"_L1].toObject(), theme.tabs.selected); } -void parseMessages(const QJsonObject &messages, chatterino::Theme &theme) +void parseMessages(const QJsonObject &messages, + const QJsonObject &messagesFallback, + chatterino::Theme &theme) { { const auto textColors = messages["textColors"_L1].toObject(); + const auto textColorsFallback = + messagesFallback["textColors"_L1].toObject(); parseColor(theme.messages, textColors, regular); parseColor(theme.messages, textColors, caret); parseColor(theme.messages, textColors, link); @@ -96,6 +118,8 @@ void parseMessages(const QJsonObject &messages, chatterino::Theme &theme) } { const auto backgrounds = messages["backgrounds"_L1].toObject(); + const auto backgroundsFallback = + messagesFallback["backgrounds"_L1].toObject(); parseColor(theme.messages, backgrounds, regular); parseColor(theme.messages, backgrounds, alternate); } @@ -105,14 +129,17 @@ void parseMessages(const QJsonObject &messages, chatterino::Theme &theme) parseColor(theme, messages, highlightAnimationEnd); } -void parseScrollbars(const QJsonObject &scrollbars, chatterino::Theme &theme) +void parseScrollbars(const QJsonObject &scrollbars, + const QJsonObject &scrollbarsFallback, + chatterino::Theme &theme) { parseColor(theme, scrollbars, background); parseColor(theme, scrollbars, thumb); parseColor(theme, scrollbars, thumbSelected); } -void parseSplits(const QJsonObject &splits, chatterino::Theme &theme) +void parseSplits(const QJsonObject &splits, const QJsonObject &splitsFallback, + chatterino::Theme &theme) { parseColor(theme, splits, messageSeperator); parseColor(theme, splits, background); @@ -125,6 +152,7 @@ void parseSplits(const QJsonObject &splits, chatterino::Theme &theme) { const auto header = splits["header"_L1].toObject(); + const auto headerFallback = splitsFallback["header"_L1].toObject(); parseColor(theme.splits, header, border); parseColor(theme.splits, header, focusedBorder); parseColor(theme.splits, header, background); @@ -134,22 +162,30 @@ void parseSplits(const QJsonObject &splits, chatterino::Theme &theme) } { const auto input = splits["input"_L1].toObject(); + const auto inputFallback = splitsFallback["input"_L1].toObject(); parseColor(theme.splits, input, background); parseColor(theme.splits, input, text); } } -void parseColors(const QJsonObject &root, chatterino::Theme &theme) +void parseColors(const QJsonObject &root, const QJsonObject &fallbackTheme, + chatterino::Theme &theme) { const auto colors = root["colors"_L1].toObject(); - - parseInto(colors, "accent"_L1, theme.accent); - - parseWindow(colors["window"_L1].toObject(), theme); - parseTabs(colors["tabs"_L1].toObject(), theme); - parseMessages(colors["messages"_L1].toObject(), theme); - parseScrollbars(colors["scrollbars"_L1].toObject(), theme); - parseSplits(colors["splits"_L1].toObject(), theme); + const auto fallbackColors = fallbackTheme["colors"_L1].toObject(); + + parseInto(colors, fallbackColors, "accent"_L1, theme.accent); + + parseWindow(colors["window"_L1].toObject(), + fallbackColors["window"_L1].toObject(), theme); + parseTabs(colors["tabs"_L1].toObject(), + fallbackColors["tabs"_L1].toObject(), theme); + parseMessages(colors["messages"_L1].toObject(), + fallbackColors["messages"_L1].toObject(), theme); + parseScrollbars(colors["scrollbars"_L1].toObject(), + fallbackColors["scrollbars"_L1].toObject(), theme); + parseSplits(colors["splits"_L1].toObject(), + fallbackColors["splits"_L1].toObject(), theme); } #undef parseColor #undef _c2StringLit @@ -290,6 +326,7 @@ void Theme::update() std::optional themeJSON; QString themePath; + bool isCustomTheme = false; if (!oTheme) { qCWarning(chatterinoTheme) @@ -316,6 +353,10 @@ void Theme::update() themeJSON = loadTheme(fallbackTheme); themePath = fallbackTheme.path; } + else + { + isCustomTheme = theme.custom; + } } auto loadTs = double(timer.nsecsElapsed()) * nsToMs; @@ -331,7 +372,7 @@ void Theme::update() return; } - this->parseFrom(*themeJSON); + this->parseFrom(*themeJSON, isCustomTheme); this->currentThemePath_ = themePath; auto parseTs = double(timer.nsecsElapsed()) * nsToMs; @@ -422,13 +463,29 @@ std::optional Theme::findThemeByKey(const QString &key) return std::nullopt; } -void Theme::parseFrom(const QJsonObject &root) +void Theme::parseFrom(const QJsonObject &root, bool isCustomTheme) { - parseColors(root, *this); - this->isLight_ = root["metadata"_L1]["iconTheme"_L1].toString() == u"dark"_s; + std::optional fallbackTheme; + if (isCustomTheme) + { + // Only attempt to load a fallback theme if the theme we're loading is a custom theme + auto fallbackThemeName = + root["metadata"_L1]["fallbackTheme"_L1].toString("Dark"); + for (const auto &theme : Theme::builtInThemes) + { + if (fallbackThemeName.compare(theme.key, Qt::CaseInsensitive) == 0) + { + fallbackTheme = loadTheme(theme); + break; + } + } + } + + parseColors(root, fallbackTheme.value_or(QJsonObject()), *this); + this->splits.input.styleSheet = uR"( background: %1; border: %2; diff --git a/src/singletons/Theme.hpp b/src/singletons/Theme.hpp index 599dcd75600..d64172e45b3 100644 --- a/src/singletons/Theme.hpp +++ b/src/singletons/Theme.hpp @@ -182,7 +182,7 @@ class Theme final : public Singleton std::optional findThemeByKey(const QString &key); - void parseFrom(const QJsonObject &root); + void parseFrom(const QJsonObject &root, bool isCustomTheme); pajlada::Signals::NoArgSignal repaintVisibleChatWidgets_;