refactor: move file logic to RootConfig + to own file

This commit is contained in:
2 * r + 2 * t 2026-04-12 02:06:13 +10:00
parent 697784abce
commit 72e0a218fd
7 changed files with 197 additions and 195 deletions

View file

@ -3,6 +3,7 @@ qml_module(caelestia-config
SOURCES SOURCES
config.cpp config.cpp
configobject.cpp configobject.cpp
rootconfig.cpp
configscope.cpp configscope.cpp
appearanceconfig.cpp appearanceconfig.cpp
tokens.cpp tokens.cpp

View file

@ -13,6 +13,7 @@
#include "lockconfig.hpp" #include "lockconfig.hpp"
#include "notifsconfig.hpp" #include "notifsconfig.hpp"
#include "osdconfig.hpp" #include "osdconfig.hpp"
#include "rootconfig.hpp"
#include "serviceconfig.hpp" #include "serviceconfig.hpp"
#include "sessionconfig.hpp" #include "sessionconfig.hpp"
#include "sidebarconfig.hpp" #include "sidebarconfig.hpp"

View file

@ -1,10 +1,6 @@
#include "configobject.hpp" #include "configobject.hpp"
#include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qjsonarray.h> #include <qjsonarray.h>
#include <qjsondocument.h>
#include <qjsonvalue.h> #include <qjsonvalue.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qmetaobject.h> #include <qmetaobject.h>
@ -15,6 +11,8 @@ namespace caelestia::config {
Q_LOGGING_CATEGORY(lcConfig, "caelestia.config", QtInfoMsg) Q_LOGGING_CATEGORY(lcConfig, "caelestia.config", QtInfoMsg)
// ConfigObject
ConfigObject::ConfigObject(QObject* parent) ConfigObject::ConfigObject(QObject* parent)
: QObject(parent) {} : QObject(parent) {}
@ -169,24 +167,6 @@ void ConfigObject::syncFromGlobal(ConfigObject* global) {
} }
} }
void ConfigObject::markPropertyLoaded(const QString& name) {
m_loadedKeys.insert(name);
}
void ConfigObject::resetOption(const QString& name) {
m_loadedKeys.remove(name);
// If synced from global, re-copy the global value
if (m_global) {
int idx = metaObject()->indexOfProperty(name.toUtf8().constData());
if (idx >= 0) {
auto prop = metaObject()->property(idx);
if (prop.isWritable())
prop.write(this, prop.read(m_global));
}
}
}
void ConfigObject::resyncFromGlobal() { void ConfigObject::resyncFromGlobal() {
if (!m_global) if (!m_global)
return; return;
@ -214,6 +194,24 @@ void ConfigObject::resyncFromGlobal() {
} }
} }
void ConfigObject::markPropertyLoaded(const QString& name) {
m_loadedKeys.insert(name);
}
void ConfigObject::resetOption(const QString& name) {
m_loadedKeys.remove(name);
// If synced from global, re-copy the global value
if (m_global) {
int idx = metaObject()->indexOfProperty(name.toUtf8().constData());
if (idx >= 0) {
auto prop = metaObject()->property(idx);
if (prop.isWritable())
prop.write(this, prop.read(m_global));
}
}
}
void ConfigObject::onGlobalPropertiesChanged(const QMap<QString, QVariant>& changed) { void ConfigObject::onGlobalPropertiesChanged(const QMap<QString, QVariant>& changed) {
for (auto it = changed.begin(); it != changed.end(); ++it) { for (auto it = changed.begin(); it != changed.end(); ++it) {
if (m_loadedKeys.contains(it.key())) if (m_loadedKeys.contains(it.key()))
@ -222,9 +220,7 @@ void ConfigObject::onGlobalPropertiesChanged(const QMap<QString, QVariant>& chan
int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData()); int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData());
if (idx >= 0) { if (idx >= 0) {
metaObject()->property(idx).write(this, it.value()); metaObject()->property(idx).write(this, it.value());
// Remove the key that was added by markPropertyLoaded in the setter — m_loadedKeys.remove(it.key()); // setter added it — remove since this is a synced value
// this is a synced value, not an explicit override
m_loadedKeys.remove(it.key());
qCDebug(lcConfig) << metaObject()->className() << "synced" << it.key() << "=" << it.value() qCDebug(lcConfig) << metaObject()->className() << "synced" << it.key() << "=" << it.value()
<< "from global change"; << "from global change";
} }
@ -253,134 +249,4 @@ void ConfigObject::emitBatchedChanges() {
emit propertiesChanged(changes); emit propertiesChanged(changes);
} }
void ConfigObject::setupFileBackend(const QString& path) {
m_filePath = path;
m_watcher = new QFileSystemWatcher(this);
m_saveTimer = new QTimer(this);
m_cooldownTimer = new QTimer(this);
m_retryTimer = new QTimer(this);
m_retryTimer->setSingleShot(true);
m_retryTimer->setInterval(50);
connect(m_retryTimer, &QTimer::timeout, this, &ConfigObject::reloadFromFile);
m_saveTimer->setSingleShot(true);
m_saveTimer->setInterval(500);
connect(m_saveTimer, &QTimer::timeout, this, [this] {
QDir().mkpath(QFileInfo(m_filePath).absolutePath());
QFile file(m_filePath);
if (!file.open(QIODevice::WriteOnly)) {
qCWarning(lcConfig, "Failed to write %s", qUtf8Printable(m_filePath));
if (auto* root = qobject_cast<RootConfig*>(this))
emit root->saveFailed(QStringLiteral("Failed to open file for writing"));
return;
}
auto json = toJsonObject();
file.write(QJsonDocument(json).toJson(QJsonDocument::Indented));
file.close();
if (auto* root = qobject_cast<RootConfig*>(this))
emit root->saved();
});
m_cooldownTimer->setSingleShot(true);
m_cooldownTimer->setInterval(2000);
connect(m_cooldownTimer, &QTimer::timeout, this, [this] {
m_recentlySaved = false;
});
connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ConfigObject::onFileChanged);
qCDebug(lcConfig) << "Setting up file backend for" << metaObject()->className() << "at" << path;
reloadFromFile();
if (QFile::exists(m_filePath))
m_watcher->addPath(m_filePath);
}
void ConfigObject::saveToFile() {
if (!m_saveTimer)
return;
m_saveTimer->start();
m_recentlySaved = true;
m_cooldownTimer->start();
}
bool ConfigObject::reloadFromFile() {
QFile file(m_filePath);
if (!file.open(QIODevice::ReadOnly)) {
qCDebug(lcConfig, "Failed to open %s", qUtf8Printable(m_filePath));
return false;
}
QJsonParseError error{};
auto doc = QJsonDocument::fromJson(file.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
if (m_retryTimer && m_parseRetries < 3) {
m_parseRetries++;
qCDebug(lcConfig, "Failed to parse %s: %s - retrying (%d/3)", qUtf8Printable(m_filePath),
qUtf8Printable(error.errorString()), m_parseRetries);
m_retryTimer->start();
} else {
qCWarning(
lcConfig, "Failed to parse %s: %s", qUtf8Printable(m_filePath), qUtf8Printable(error.errorString()));
m_parseRetries = 0;
}
return false;
}
m_parseRetries = 0;
qCDebug(lcConfig) << "Reloading" << metaObject()->className() << "from" << m_filePath;
clearLoadedKeys();
loadFromJson(doc.object());
// Re-sync non-loaded properties from global after reload
if (m_global) {
qCDebug(lcConfig) << "Re-syncing" << metaObject()->className() << "from global after reload";
resyncFromGlobal();
}
return true;
}
void ConfigObject::onFileChanged() {
if (!m_watcher->files().contains(m_filePath))
m_watcher->addPath(m_filePath);
if (!m_recentlySaved) {
m_parseRetries = 0;
if (m_retryTimer)
m_retryTimer->stop();
bool ok = reloadFromFile();
if (auto* root = qobject_cast<RootConfig*>(this)) {
if (ok)
emit root->loaded();
else
emit root->loadFailed(QStringLiteral("Failed to load config file"));
}
}
}
// RootConfig
RootConfig::RootConfig(QObject* parent)
: ConfigObject(parent) {}
void RootConfig::save() {
saveToFile();
}
void RootConfig::reload() {
if (reloadFromFile())
emit loaded();
}
} // namespace caelestia::config } // namespace caelestia::config

View file

@ -1,6 +1,5 @@
#pragma once #pragma once
#include <qfilesystemwatcher.h>
#include <qjsonobject.h> #include <qjsonobject.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qmap.h> #include <qmap.h>
@ -55,14 +54,7 @@ public:
void loadFromJson(const QJsonObject& obj); void loadFromJson(const QJsonObject& obj);
[[nodiscard]] QJsonObject toJsonObject() const; [[nodiscard]] QJsonObject toJsonObject() const;
// File-backed config support. Call setupFileBackend() to enable
// automatic file watching, debounced saving, and reload.
void setupFileBackend(const QString& path);
void saveToFile();
bool reloadFromFile();
// Per-monitor overlay support (Qt Resolve Mask pattern). // Per-monitor overlay support (Qt Resolve Mask pattern).
// Eagerly syncs non-overridden properties from a global ConfigObject.
void syncFromGlobal(ConfigObject* global); void syncFromGlobal(ConfigObject* global);
void resyncFromGlobal(); void resyncFromGlobal();
void clearLoadedKeys(); void clearLoadedKeys();
@ -71,8 +63,6 @@ public:
Q_INVOKABLE void resetOption(const QString& name); Q_INVOKABLE void resetOption(const QString& name);
[[nodiscard]] bool recentlySaved() const { return m_recentlySaved; }
template <typename T> static bool updateMember(T& member, const T& value) { template <typename T> static bool updateMember(T& member, const T& value) {
if constexpr (std::is_floating_point_v<T>) { if constexpr (std::is_floating_point_v<T>) {
if (qFuzzyCompare(member + 1.0, value + 1.0)) if (qFuzzyCompare(member + 1.0, value + 1.0))
@ -93,20 +83,9 @@ protected:
void notifyPropertyChanged(const QString& name, const QVariant& value); void notifyPropertyChanged(const QString& name, const QVariant& value);
private: private:
void onFileChanged();
void onGlobalPropertiesChanged(const QMap<QString, QVariant>& changed); void onGlobalPropertiesChanged(const QMap<QString, QVariant>& changed);
void emitBatchedChanges(); void emitBatchedChanges();
QString m_filePath;
bool m_recentlySaved = false;
// File backend (heap-allocated only when setupFileBackend is called)
QFileSystemWatcher* m_watcher = nullptr;
QTimer* m_saveTimer = nullptr;
QTimer* m_cooldownTimer = nullptr;
QTimer* m_retryTimer = nullptr;
int m_parseRetries = 0;
// Per-monitor overlay state // Per-monitor overlay state
ConfigObject* m_global = nullptr; ConfigObject* m_global = nullptr;
QSet<QString> m_loadedKeys; QSet<QString> m_loadedKeys;
@ -114,22 +93,4 @@ private:
QTimer* m_batchTimer = nullptr; QTimer* m_batchTimer = nullptr;
}; };
// Intermediate base for singleton config roots (GlobalConfig, TokenConfig).
// Provides save/reload with file lifecycle signals.
class RootConfig : public ConfigObject {
Q_OBJECT
public:
explicit RootConfig(QObject* parent = nullptr);
Q_INVOKABLE void save();
Q_INVOKABLE void reload();
signals:
void loaded();
void loadFailed(const QString& error);
void saved();
void saveFailed(const QString& error);
};
} // namespace caelestia::config } // namespace caelestia::config

View file

@ -0,0 +1,127 @@
#include "rootconfig.hpp"
#include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qjsondocument.h>
namespace caelestia::config {
RootConfig::RootConfig(QObject* parent)
: ConfigObject(parent) {}
void RootConfig::setupFileBackend(const QString& path) {
m_filePath = path;
m_watcher = new QFileSystemWatcher(this);
m_saveTimer = new QTimer(this);
m_cooldownTimer = new QTimer(this);
m_retryTimer = new QTimer(this);
m_retryTimer->setSingleShot(true);
m_retryTimer->setInterval(50);
connect(m_retryTimer, &QTimer::timeout, this, &RootConfig::reloadFromFile);
m_saveTimer->setSingleShot(true);
m_saveTimer->setInterval(500);
connect(m_saveTimer, &QTimer::timeout, this, [this] {
QDir().mkpath(QFileInfo(m_filePath).absolutePath());
QFile file(m_filePath);
if (!file.open(QIODevice::WriteOnly)) {
qCWarning(lcConfig, "Failed to write %s", qUtf8Printable(m_filePath));
emit saveFailed(QStringLiteral("Failed to open file for writing"));
return;
}
auto json = toJsonObject();
file.write(QJsonDocument(json).toJson(QJsonDocument::Indented));
file.close();
emit saved();
});
m_cooldownTimer->setSingleShot(true);
m_cooldownTimer->setInterval(2000);
connect(m_cooldownTimer, &QTimer::timeout, this, [this] {
m_recentlySaved = false;
});
connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &RootConfig::onFileChanged);
qCDebug(lcConfig) << "Setting up file backend for" << metaObject()->className() << "at" << path;
reloadFromFile();
if (QFile::exists(m_filePath))
m_watcher->addPath(m_filePath);
}
void RootConfig::saveToFile() {
if (!m_saveTimer)
return;
m_saveTimer->start();
m_recentlySaved = true;
m_cooldownTimer->start();
}
bool RootConfig::reloadFromFile() {
QFile file(m_filePath);
if (!file.open(QIODevice::ReadOnly)) {
qCDebug(lcConfig, "Failed to open %s", qUtf8Printable(m_filePath));
return false;
}
QJsonParseError error{};
auto doc = QJsonDocument::fromJson(file.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
if (m_retryTimer && m_parseRetries < 3) {
m_parseRetries++;
qCDebug(lcConfig, "Failed to parse %s: %s - retrying (%d/3)", qUtf8Printable(m_filePath),
qUtf8Printable(error.errorString()), m_parseRetries);
m_retryTimer->start();
} else {
qCWarning(
lcConfig, "Failed to parse %s: %s", qUtf8Printable(m_filePath), qUtf8Printable(error.errorString()));
m_parseRetries = 0;
}
return false;
}
m_parseRetries = 0;
qCDebug(lcConfig) << "Reloading" << metaObject()->className() << "from" << m_filePath;
clearLoadedKeys();
loadFromJson(doc.object());
return true;
}
void RootConfig::onFileChanged() {
if (!m_watcher->files().contains(m_filePath))
m_watcher->addPath(m_filePath);
if (!m_recentlySaved) {
m_parseRetries = 0;
if (m_retryTimer)
m_retryTimer->stop();
if (reloadFromFile())
emit loaded();
else
emit loadFailed(QStringLiteral("Failed to load config file"));
}
}
void RootConfig::save() {
saveToFile();
}
void RootConfig::reload() {
if (reloadFromFile())
emit loaded();
}
} // namespace caelestia::config

View file

@ -0,0 +1,46 @@
#pragma once
#include "configobject.hpp"
#include <qfilesystemwatcher.h>
#include <qtimer.h>
namespace caelestia::config {
// Intermediate base for singleton config roots (GlobalConfig, TokenConfig).
// Provides file-backed persistence, save/reload, and lifecycle signals.
class RootConfig : public ConfigObject {
Q_OBJECT
public:
explicit RootConfig(QObject* parent = nullptr);
void setupFileBackend(const QString& path);
void saveToFile();
bool reloadFromFile();
[[nodiscard]] bool recentlySaved() const { return m_recentlySaved; }
Q_INVOKABLE void save();
Q_INVOKABLE void reload();
signals:
void loaded();
void loadFailed(const QString& error);
void saved();
void saveFailed(const QString& error);
private:
void onFileChanged();
QString m_filePath;
bool m_recentlySaved = false;
QFileSystemWatcher* m_watcher = nullptr;
QTimer* m_saveTimer = nullptr;
QTimer* m_cooldownTimer = nullptr;
QTimer* m_retryTimer = nullptr;
int m_parseRetries = 0;
};
} // namespace caelestia::config

View file

@ -2,7 +2,7 @@
#include "anim.hpp" #include "anim.hpp"
#include "appearanceconfig.hpp" #include "appearanceconfig.hpp"
#include "configobject.hpp" #include "rootconfig.hpp"
#include <qlist.h> #include <qlist.h>
#include <qqmlengine.h> #include <qqmlengine.h>