Skip to content

Commit

Permalink
feat: add support for reloading the current theme
Browse files Browse the repository at this point in the history
  • Loading branch information
Nerixyz committed Jul 7, 2023
1 parent 45a7b7d commit b087d5d
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 10 deletions.
130 changes: 120 additions & 10 deletions src/singletons/Theme.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include <QColor>
#include <QDir>
#include <QElapsedTimer>
#include <QFile>
#include <QJsonDocument>
#include <QSet>
Expand Down Expand Up @@ -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<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 @@ -178,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 @@ -232,19 +238,22 @@ void Theme::update()
auto oTheme = this->findThemeByKey(this->themeName);

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 @@ -254,6 +263,7 @@ void Theme::update()

// Parsing the theme failed, fall back
themeJSON = loadTheme(fallbackTheme);
themePath = fallbackTheme.path;
}
}

Expand All @@ -265,8 +275,10 @@ void Theme::update()
}

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

this->updated.invoke();
this->updateCurrentWatchPath();
}

std::vector<std::pair<QString, QVariant>> Theme::availableThemes() const
Expand Down Expand Up @@ -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<QFileSystemWatcher>();

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())
Expand Down
10 changes: 10 additions & 0 deletions src/singletons/Theme.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@

#include <pajlada/settings/setting.hpp>
#include <QColor>
#include <QFileSystemWatcher>
#include <QJsonObject>
#include <QPixmap>
#include <QString>
#include <QVariant>

#include <memory>
#include <optional>
#include <vector>

Expand Down Expand Up @@ -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
**/
Expand All @@ -152,6 +157,11 @@ class Theme final : public Singleton

std::vector<ThemeDescriptor> availableThemes_;

QString currentThemePath_;
std::unique_ptr<QFileSystemWatcher> themeWatcher_;
void watchedFileChanged(const QString &path);
void updateCurrentWatchPath();

/**
* Figure out which themes are available in the Themes directory
*
Expand Down
9 changes: 9 additions & 0 deletions src/widgets/settingspages/GeneralPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down

0 comments on commit b087d5d

Please sign in to comment.