Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add a fallback theme to custom themes #5198

Merged
merged 5 commits into from
Feb 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 color Chatterino needs. If no fallback theme is specified, we'll pull the color 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.",
pajlada marked this conversation as resolved.
Show resolved Hide resolved
"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];
pajlada marked this conversation as resolved.
Show resolved Hide resolved
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");
pajlada marked this conversation as resolved.
Show resolved Hide resolved
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
Loading