Skip to content

Commit

Permalink
feat: Add a fallback theme to custom themes
Browse files Browse the repository at this point in the history
If no fallback theme is specified by the custom theme, the fallback theme will be "Dark"
  • Loading branch information
pajlada committed Feb 24, 2024
1 parent df2b5f9 commit 9ccda1c
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions docs/ChatterinoTheme.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
119 changes: 88 additions & 31 deletions src/singletons/Theme.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
}
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -290,6 +326,7 @@ void Theme::update()

std::optional<QJsonObject> themeJSON;
QString themePath;
bool isCustomTheme = false;
if (!oTheme)
{
qCWarning(chatterinoTheme)
Expand All @@ -316,6 +353,10 @@ void Theme::update()
themeJSON = loadTheme(fallbackTheme);
themePath = fallbackTheme.path;
}
else
{
isCustomTheme = theme.custom;
}
}
auto loadTs = double(timer.nsecsElapsed()) * nsToMs;

Expand All @@ -331,7 +372,7 @@ void Theme::update()
return;
}

this->parseFrom(*themeJSON);
this->parseFrom(*themeJSON, isCustomTheme);
this->currentThemePath_ = themePath;

auto parseTs = double(timer.nsecsElapsed()) * nsToMs;
Expand Down Expand Up @@ -422,13 +463,29 @@ std::optional<ThemeDescriptor> 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<QJsonObject> 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;
Expand Down
2 changes: 1 addition & 1 deletion src/singletons/Theme.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ class Theme final : public Singleton

std::optional<ThemeDescriptor> findThemeByKey(const QString &key);

void parseFrom(const QJsonObject &root);
void parseFrom(const QJsonObject &root, bool isCustomTheme);

pajlada::Signals::NoArgSignal repaintVisibleChatWidgets_;

Expand Down

0 comments on commit 9ccda1c

Please sign in to comment.