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

Add command to automatically reload your theme #4718

Merged
merged 13 commits into from
Jul 23, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- Minor: Add accelerators to the right click menu for messages (#4705)
- Minor: Add pin action to usercards and reply threads. (#4692)
- Minor: Stream status requests are now batched. (#4713)
- Minor: Added `/c2-theme-autoreload` command to automatically reload a custom theme. This is useful for when you're developing your own theme. (#4718)
- Bugfix: Increased amount of blocked users loaded from 100 to 1,000. (#4721)
- Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667)
- Bugfix: Fix spacing issue with mentions inside RTL text. (#4677)
Expand Down
2 changes: 1 addition & 1 deletion docs/ChatterinoTheme.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
},
{
"title": "SVG Color",
"description": "This is stricter than Qt. You could theoretically put tabs an spaces between characters in a named color and capitalize the color.",
"description": "This enum is stricter than Qt. You could theoretically put tabs and spaces between characters in a named color and capitalize the color.",
"$comment": "https://www.w3.org/TR/SVG11/types.html#ColorKeywords",
"enum": [
"aliceblue",
Expand Down
1 change: 1 addition & 0 deletions src/controllers/commands/CommandController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3241,6 +3241,7 @@ void CommandController::initialize(Settings &, Paths &paths)
this->registerCommand("/shoutout", &commands::sendShoutout);

this->registerCommand("/c2-set-logging-rules", &commands::setLoggingRules);
this->registerCommand("/c2-theme-autoreload", &commands::toggleThemeReload);
}

void CommandController::save()
Expand Down
21 changes: 21 additions & 0 deletions src/controllers/commands/builtin/chatterino/Debugging.cpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
#include "controllers/commands/builtin/chatterino/Debugging.hpp"

#include "common/Channel.hpp"
#include "common/Literals.hpp"
#include "controllers/commands/CommandContext.hpp"
#include "messages/MessageBuilder.hpp"
#include "singletons/Theme.hpp"

#include <QLoggingCategory>
#include <QString>

namespace chatterino::commands {

using namespace literals;

QString setLoggingRules(const CommandContext &ctx)
{
if (ctx.words.size() < 2)
Expand Down Expand Up @@ -42,4 +46,21 @@ QString setLoggingRules(const CommandContext &ctx)
return {};
}

QString toggleThemeReload(const CommandContext &ctx)
{
if (getTheme()->isAutoReloading())
{
getTheme()->setAutoReload(false);
ctx.channel->addMessage(
makeSystemMessage(u"Disabled theme auto reloading."_s));
return {};
}

getTheme()->setAutoReload(true);
ctx.channel->addMessage(
makeSystemMessage(u"Auto reloading theme every %1 ms."_s.arg(
Theme::AUTO_RELOAD_INTERVAL_MS)));
return {};
}

} // namespace chatterino::commands
2 changes: 2 additions & 0 deletions src/controllers/commands/builtin/chatterino/Debugging.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ namespace chatterino::commands {

QString setLoggingRules(const CommandContext &ctx);

QString toggleThemeReload(const CommandContext &ctx);

} // namespace chatterino::commands
153 changes: 113 additions & 40 deletions src/singletons/Theme.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
#include "singletons/Theme.hpp"

#include "Application.hpp"
#include "common/Literals.hpp"
#include "common/QLogging.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Resources.hpp"

#include <QColor>
#include <QDir>
#include <QElapsedTimer>
#include <QFile>
#include <QJsonDocument>
#include <QSet>
Expand All @@ -17,8 +19,9 @@
namespace {

using namespace chatterino;
using namespace literals;

void parseInto(const QJsonObject &obj, const QLatin1String &key, QColor &color)
void parseInto(const QJsonObject &obj, QLatin1String key, QColor &color)
{
const auto &jsonValue = obj[key];
if (!jsonValue.isString()) [[unlikely]]
Expand All @@ -40,8 +43,9 @@ void parseInto(const QJsonObject &obj, const QLatin1String &key, QColor &color)
}

// NOLINTBEGIN(cppcoreguidelines-macro-usage)
#define _c2StringLit(s, ty) s##ty
#define parseColor(to, from, key) \
parseInto(from, QLatin1String(#key), (to).from.key)
parseInto(from, _c2StringLit(#key, _L1), (to).from.key)
// NOLINTEND(cppcoreguidelines-macro-usage)

void parseWindow(const QJsonObject &window, chatterino::Theme &theme)
Expand All @@ -52,40 +56,40 @@ void parseWindow(const QJsonObject &window, chatterino::Theme &theme)

void parseTabs(const QJsonObject &tabs, chatterino::Theme &theme)
{
const auto parseTabColors = [](auto json, auto &tab) {
parseInto(json, QLatin1String("text"), tab.text);
const auto parseTabColors = [](const auto &json, auto &tab) {
parseInto(json, "text"_L1, tab.text);
{
const auto backgrounds = json["backgrounds"].toObject();
const auto backgrounds = json["backgrounds"_L1].toObject();
parseColor(tab, backgrounds, regular);
parseColor(tab, backgrounds, hover);
parseColor(tab, backgrounds, unfocused);
}
{
const auto line = json["line"].toObject();
const auto line = json["line"_L1].toObject();
parseColor(tab, line, regular);
parseColor(tab, line, hover);
parseColor(tab, line, unfocused);
}
};
parseColor(theme, tabs, dividerLine);
parseTabColors(tabs["regular"].toObject(), theme.tabs.regular);
parseTabColors(tabs["newMessage"].toObject(), theme.tabs.newMessage);
parseTabColors(tabs["highlighted"].toObject(), theme.tabs.highlighted);
parseTabColors(tabs["selected"].toObject(), theme.tabs.selected);
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);
}

void parseMessages(const QJsonObject &messages, chatterino::Theme &theme)
{
{
const auto textColors = messages["textColors"].toObject();
const auto textColors = messages["textColors"_L1].toObject();
parseColor(theme.messages, textColors, regular);
parseColor(theme.messages, textColors, caret);
parseColor(theme.messages, textColors, link);
parseColor(theme.messages, textColors, system);
parseColor(theme.messages, textColors, chatPlaceholder);
}
{
const auto backgrounds = messages["backgrounds"].toObject();
const auto backgrounds = messages["backgrounds"_L1].toObject();
parseColor(theme.messages, backgrounds, regular);
parseColor(theme.messages, backgrounds, alternate);
}
Expand Down Expand Up @@ -114,7 +118,7 @@ void parseSplits(const QJsonObject &splits, chatterino::Theme &theme)
parseColor(theme, splits, resizeHandleBackground);

{
const auto header = splits["header"].toObject();
const auto header = splits["header"_L1].toObject();
parseColor(theme.splits, header, border);
parseColor(theme.splits, header, focusedBorder);
parseColor(theme.splits, header, background);
Expand All @@ -123,40 +127,34 @@ void parseSplits(const QJsonObject &splits, chatterino::Theme &theme)
parseColor(theme.splits, header, focusedText);
}
{
const auto input = splits["input"].toObject();
const auto input = splits["input"_L1].toObject();
parseColor(theme.splits, input, background);
parseColor(theme.splits, input, text);
}
}

void parseColors(const QJsonObject &root, chatterino::Theme &theme)
{
const auto colors = root["colors"].toObject();
const auto colors = root["colors"_L1].toObject();

parseInto(colors, QLatin1String("accent"), theme.accent);
parseInto(colors, "accent"_L1, theme.accent);

parseWindow(colors["window"].toObject(), theme);
parseTabs(colors["tabs"].toObject(), theme);
parseMessages(colors["messages"].toObject(), theme);
parseScrollbars(colors["scrollbars"].toObject(), theme);
parseSplits(colors["splits"].toObject(), theme);
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);
}
#undef parseColor
#undef _c2StringLit

/**
* Load the given theme descriptor from its path
*
* Returns a JSON object containing theme data if the theme is valid, otherwise nullopt
*
* NOTE: No theme validation is done by this function
**/
std::optional<QJsonObject> loadTheme(const ThemeDescriptor &theme)
std::optional<QJsonObject> loadThemeFromPath(const QString &path)
{
QFile file(theme.path);
QFile file(path);
if (!file.open(QFile::ReadOnly))
{
qCWarning(chatterinoTheme)
<< "Failed to open" << file.fileName() << "at" << theme.path;
<< "Failed to open" << file.fileName() << "at" << path;
return std::nullopt;
}

Expand All @@ -174,6 +172,18 @@ std::optional<QJsonObject> loadTheme(const ThemeDescriptor &theme)
return json.object();
}

/**
* Load the given theme descriptor from its path
*
* Returns a JSON object containing theme data if the theme is valid, otherwise nullopt
*
* NOTE: No theme validation is done by this function
**/
std::optional<QJsonObject> loadTheme(const ThemeDescriptor &theme)
{
return loadThemeFromPath(theme.path);
}

} // namespace

namespace chatterino {
Expand Down Expand Up @@ -227,20 +237,27 @@ void Theme::update()
{
auto oTheme = this->findThemeByKey(this->themeName);

constexpr const double nsToMs = 1.0 / 1000000.0;
QElapsedTimer timer;
timer.start();

std::optional<QJsonObject> themeJSON;
QString themePath;
if (!oTheme)
{
qCWarning(chatterinoTheme)
<< "Theme" << this->themeName
<< "not found, falling back to the fallback theme";

themeJSON = loadTheme(fallbackTheme);
themePath = fallbackTheme.path;
}
else
{
const auto &theme = *oTheme;

themeJSON = loadTheme(theme);
themePath = theme.path;

if (!themeJSON)
{
Expand All @@ -250,8 +267,10 @@ void Theme::update()

// Parsing the theme failed, fall back
themeJSON = loadTheme(fallbackTheme);
themePath = fallbackTheme.path;
}
}
auto loadTs = double(timer.nsecsElapsed()) * nsToMs;

if (!themeJSON)
{
Expand All @@ -260,9 +279,29 @@ void Theme::update()
return;
}

if (this->isAutoReloading() && this->currentThemeJson_ == *themeJSON)
{
return;
}

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

auto parseTs = double(timer.nsecsElapsed()) * nsToMs;

this->updated.invoke();
auto updateTs = double(timer.nsecsElapsed()) * nsToMs;
qCDebug(chatterinoTheme).nospace().noquote()
<< "Updated theme in " << QString::number(updateTs, 'f', 2)
<< "ms (load: " << QString::number(loadTs, 'f', 2)
<< "ms, parse: " << QString::number(parseTs - loadTs, 'f', 2)
<< "ms, update: " << QString::number(updateTs - parseTs, 'f', 2)
<< "ms)";

if (this->isAutoReloading())
{
this->currentThemeJson_ = *themeJSON;
}
}

std::vector<std::pair<QString, QVariant>> Theme::availableThemes() const
Expand Down Expand Up @@ -341,15 +380,20 @@ void Theme::parseFrom(const QJsonObject &root)
parseColors(root, *this);

this->isLight_ =
root["metadata"]["iconTheme"].toString() == QStringLiteral("dark");

this->splits.input.styleSheet =
"background:" + this->splits.input.background.name() + ";" +
"border:" + this->tabs.selected.backgrounds.regular.name() + ";" +
"color:" + this->messages.textColors.regular.name() + ";" +
"selection-background-color:" +
(this->isLightTheme() ? "#68B1FF"
: this->tabs.selected.backgrounds.regular.name());
root["metadata"_L1]["iconTheme"_L1].toString() == u"dark"_s;

this->splits.input.styleSheet = uR"(
background: %1;
border: %2;
color: %3;
selection-background-color: %4;
)"_s.arg(
this->splits.input.background.name(QColor::HexArgb),
this->tabs.selected.backgrounds.regular.name(QColor::HexArgb),
this->messages.textColors.regular.name(QColor::HexArgb),
this->isLightTheme()
? u"#68B1FF"_s
: this->tabs.selected.backgrounds.regular.name(QColor::HexArgb));

// Usercard buttons
if (this->isLightTheme())
Expand All @@ -364,6 +408,35 @@ void Theme::parseFrom(const QJsonObject &root)
}
}

bool Theme::isAutoReloading() const
{
return this->themeReloadTimer_ != nullptr;
}

void Theme::setAutoReload(bool autoReload)
{
if (autoReload == this->isAutoReloading())
{
return;
}

if (!autoReload)
{
this->themeReloadTimer_.reset();
this->currentThemeJson_ = {};
return;
}

this->themeReloadTimer_ = std::make_unique<QTimer>();
QObject::connect(this->themeReloadTimer_.get(), &QTimer::timeout, [this]() {
this->update();
});
this->themeReloadTimer_->setInterval(Theme::AUTO_RELOAD_INTERVAL_MS);
this->themeReloadTimer_->start();

qCDebug(chatterinoTheme) << "Enabled theme watcher";
}

void Theme::normalizeColor(QColor &color) const
{
if (this->isLightTheme())
Expand Down
Loading
Loading