From b087d5dffbd6fa582468bad92d7b8f7c62cd40a4 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Fri, 7 Jul 2023 19:41:08 +0200 Subject: [PATCH] feat: add support for reloading the current theme --- src/singletons/Theme.cpp | 130 ++++++++++++++++++++-- src/singletons/Theme.hpp | 10 ++ src/widgets/settingspages/GeneralPage.cpp | 9 ++ 3 files changed, 139 insertions(+), 10 deletions(-) diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index 68b9c34b01c..23e5d25cb8a 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -147,20 +148,13 @@ void parseColors(const QJsonObject &root, chatterino::Theme &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 loadTheme(const ThemeDescriptor &theme) +std::optional 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; } @@ -178,6 +172,18 @@ std::optional 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 loadTheme(const ThemeDescriptor &theme) +{ + return loadThemeFromPath(theme.path); +} + } // namespace namespace chatterino { @@ -232,6 +238,7 @@ void Theme::update() auto oTheme = this->findThemeByKey(this->themeName); std::optional themeJSON; + QString themePath; if (!oTheme) { qCWarning(chatterinoTheme) @@ -239,12 +246,14 @@ void Theme::update() << "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) { @@ -254,6 +263,7 @@ void Theme::update() // Parsing the theme failed, fall back themeJSON = loadTheme(fallbackTheme); + themePath = fallbackTheme.path; } } @@ -265,8 +275,10 @@ void Theme::update() } this->parseFrom(*themeJSON); + this->currentThemePath_ = themePath; this->updated.invoke(); + this->updateCurrentWatchPath(); } std::vector> Theme::availableThemes() const @@ -373,6 +385,104 @@ void Theme::parseFrom(const QJsonObject &root) } } +bool Theme::isAutoReloading() const +{ + return this->themeWatcher_ != nullptr; +} + +void Theme::setAutoReload(bool autoReload) +{ + // autoReload <=> themeWatcher != nullptr + if (autoReload == this->isAutoReloading()) + { + return; + } + + if (!autoReload) + { + this->themeWatcher_.reset(); + return; + } + + this->themeWatcher_ = std::make_unique(); + + QObject::connect(this->themeWatcher_.get(), + &QFileSystemWatcher::fileChanged, + [this](const auto &path) { + this->watchedFileChanged(path); + }); + this->updateCurrentWatchPath(); + qCDebug(chatterinoTheme) << "Enabled theme watcher"; +} + +void Theme::updateCurrentWatchPath() +{ + if (this->themeWatcher_ == nullptr) + { + return; + } + auto files = this->themeWatcher_->files(); + + if (files.empty()) + { + this->themeWatcher_->addPath(this->currentThemePath_); + return; + } + + if (files.length() == 1) + { + auto watched = files[0]; + if (watched == this->currentThemePath_) + { + return; + } + this->themeWatcher_->addPath(this->currentThemePath_); + this->themeWatcher_->removePath(watched); + return; + } + + this->themeWatcher_->removePaths(files); + this->themeWatcher_->addPath(this->currentThemePath_); +} + +void Theme::watchedFileChanged(const QString &path) +{ + QFile target(path); + if (!target.exists()) + { + return; + } + if (target.size() < 2) + { + return; + } + + constexpr const double nsToMs = 1.0 / 1000000.0; + QElapsedTimer timer; + timer.start(); + + auto json = loadThemeFromPath(path); + auto loadTs = double(timer.nsecsElapsed()) * nsToMs; + + if (!json) + { + qCWarning(chatterinoTheme) << "Failed to load theme JSON from" << path; + return; + } + this->parseFrom(*json); + auto parseTs = double(timer.nsecsElapsed()) * nsToMs; + + this->updated.invoke(); + auto updateTs = double(timer.nsecsElapsed()) * nsToMs; + + qCDebug(chatterinoTheme).nospace().noquote() + << "Reloaded 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)"; +} + void Theme::normalizeColor(QColor &color) const { if (this->isLightTheme()) diff --git a/src/singletons/Theme.hpp b/src/singletons/Theme.hpp index 3a7e1c9841e..b6137e67f4c 100644 --- a/src/singletons/Theme.hpp +++ b/src/singletons/Theme.hpp @@ -6,11 +6,13 @@ #include #include +#include #include #include #include #include +#include #include #include @@ -138,6 +140,9 @@ class Theme final : public Singleton void normalizeColor(QColor &color) const; void update(); + bool isAutoReloading() const; + void setAutoReload(bool autoReload); + /** * Return a list of available themes **/ @@ -152,6 +157,11 @@ class Theme final : public Singleton std::vector availableThemes_; + QString currentThemePath_; + std::unique_ptr themeWatcher_; + void watchedFileChanged(const QString &path); + void updateCurrentWatchPath(); + /** * Figure out which themes are available in the Themes directory * diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index d55dc1f4d27..4106c88f0d3 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -1002,6 +1002,15 @@ void GeneralPage::initLayout(GeneralPageView &layout) layout.addIntInput("Usercard scrollback limit (requires restart)", s.scrollbackUsercardLimit, 100, 100000, 100); + layout.addSessionCheckbox( + "Automatically reload custom theme (session only)", + getTheme()->isAutoReloading(), + [](bool on) { + getTheme()->setAutoReload(on); + }, + "Automatically reloads the theme when the custom theme file is " + "updated. This only applies to the current session to save resources."); + layout.addCheckbox("Enable experimental IRC support (requires restart)", s.enableExperimentalIrc, false, "When enabled, attempting to join a channel will "