fix: move scale back to normal config

This commit is contained in:
2 * r + 2 * t 2026-04-11 02:38:03 +10:00
parent d530e209b3
commit 8d73d6037d
8 changed files with 445 additions and 168 deletions

View file

@ -1,10 +1,10 @@
qml_module(caelestia-config
URI Caelestia.Config
SOURCES
config.hpp config.cpp
configobject.hpp
config.cpp
configobject.cpp
appearanceconfig.cpp
advancedconfig.hpp
appearanceconfig.hpp
backgroundconfig.hpp
barconfig.hpp
borderconfig.hpp
@ -21,7 +21,6 @@ qml_module(caelestia-config
userpaths.hpp
utilitiesconfig.hpp
winfoconfig.hpp
configobject.cpp
LIBRARIES
Qt::Quick
)

View file

@ -32,14 +32,91 @@ public:
, m_expressiveSlowSpatial({ 0.39, 1.29, 0.35, 0.98, 1, 1 }) {}
};
class RoundingTokens : public ConfigObject {
Q_OBJECT
CONFIG_PROPERTY(int, small, 12)
CONFIG_PROPERTY(int, normal, 17)
CONFIG_PROPERTY(int, large, 25)
CONFIG_PROPERTY(int, full, 1000)
public:
explicit RoundingTokens(QObject* parent = nullptr)
: ConfigObject(parent) {}
};
class SpacingTokens : public ConfigObject {
Q_OBJECT
CONFIG_PROPERTY(int, small, 7)
CONFIG_PROPERTY(int, smaller, 10)
CONFIG_PROPERTY(int, normal, 12)
CONFIG_PROPERTY(int, larger, 15)
CONFIG_PROPERTY(int, large, 20)
public:
explicit SpacingTokens(QObject* parent = nullptr)
: ConfigObject(parent) {}
};
class PaddingTokens : public ConfigObject {
Q_OBJECT
CONFIG_PROPERTY(int, small, 5)
CONFIG_PROPERTY(int, smaller, 7)
CONFIG_PROPERTY(int, normal, 10)
CONFIG_PROPERTY(int, larger, 12)
CONFIG_PROPERTY(int, large, 15)
public:
explicit PaddingTokens(QObject* parent = nullptr)
: ConfigObject(parent) {}
};
class FontSizeTokens : public ConfigObject {
Q_OBJECT
CONFIG_PROPERTY(int, small, 11)
CONFIG_PROPERTY(int, smaller, 12)
CONFIG_PROPERTY(int, normal, 13)
CONFIG_PROPERTY(int, larger, 15)
CONFIG_PROPERTY(int, large, 18)
CONFIG_PROPERTY(int, extraLarge, 28)
public:
explicit FontSizeTokens(QObject* parent = nullptr)
: ConfigObject(parent) {}
};
class AnimDurationTokens : public ConfigObject {
Q_OBJECT
CONFIG_PROPERTY(int, small, 200)
CONFIG_PROPERTY(int, normal, 400)
CONFIG_PROPERTY(int, large, 600)
CONFIG_PROPERTY(int, extraLarge, 1000)
CONFIG_PROPERTY(int, expressiveFastSpatial, 350)
CONFIG_PROPERTY(int, expressiveDefaultSpatial, 500)
CONFIG_PROPERTY(int, expressiveSlowSpatial, 650)
public:
explicit AnimDurationTokens(QObject* parent = nullptr)
: ConfigObject(parent) {}
};
class AdvancedAppearance : public ConfigObject {
Q_OBJECT
CONFIG_SUBOBJECT(AnimCurves, curves)
CONFIG_SUBOBJECT(RoundingTokens, rounding)
CONFIG_SUBOBJECT(SpacingTokens, spacing)
CONFIG_SUBOBJECT(PaddingTokens, padding)
CONFIG_SUBOBJECT(FontSizeTokens, fontSize)
CONFIG_SUBOBJECT(AnimDurationTokens, animDurations)
public:
explicit AdvancedAppearance(QObject* parent = nullptr)
: ConfigObject(parent)
, m_curves(new AnimCurves(this)) {}
, m_curves(new AnimCurves(this))
, m_rounding(new RoundingTokens(this))
, m_spacing(new SpacingTokens(this))
, m_padding(new PaddingTokens(this))
, m_fontSize(new FontSizeTokens(this))
, m_animDurations(new AnimDurationTokens(this)) {}
};
class BarSizes : public ConfigObject {

View file

@ -0,0 +1,167 @@
#include "appearanceconfig.hpp"
#include "advancedconfig.hpp"
#include <qmetaobject.h>
namespace caelestia::config {
// Helper: connect all changed signals from a token object to a single valuesChanged signal,
// plus connect the local scaleChanged signal.
template <typename Source, typename Target> static void connectTokenSignals(Source* source, Target* target) {
const auto* meta = source->metaObject();
for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
if (prop.hasNotifySignal())
QObject::connect(source, prop.notifySignal(), target,
target->metaObject()->method(target->metaObject()->indexOfSignal("valuesChanged()")));
}
QObject::connect(target, &Target::scaleChanged, target, &Target::valuesChanged);
}
// AppearanceRounding
void AppearanceRounding::bindTokens(RoundingTokens* tokens) {
m_tokens = tokens;
connectTokenSignals(tokens, this);
}
int AppearanceRounding::small() const {
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
}
int AppearanceRounding::normal() const {
return m_tokens ? static_cast<int>(m_tokens->normal() * m_scale) : 0;
}
int AppearanceRounding::large() const {
return m_tokens ? static_cast<int>(m_tokens->large() * m_scale) : 0;
}
int AppearanceRounding::full() const {
return m_tokens ? static_cast<int>(m_tokens->full() * m_scale) : 0;
}
// AppearanceSpacing
void AppearanceSpacing::bindTokens(SpacingTokens* tokens) {
m_tokens = tokens;
connectTokenSignals(tokens, this);
}
int AppearanceSpacing::small() const {
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
}
int AppearanceSpacing::smaller() const {
return m_tokens ? static_cast<int>(m_tokens->smaller() * m_scale) : 0;
}
int AppearanceSpacing::normal() const {
return m_tokens ? static_cast<int>(m_tokens->normal() * m_scale) : 0;
}
int AppearanceSpacing::larger() const {
return m_tokens ? static_cast<int>(m_tokens->larger() * m_scale) : 0;
}
int AppearanceSpacing::large() const {
return m_tokens ? static_cast<int>(m_tokens->large() * m_scale) : 0;
}
// AppearancePadding
void AppearancePadding::bindTokens(PaddingTokens* tokens) {
m_tokens = tokens;
connectTokenSignals(tokens, this);
}
int AppearancePadding::small() const {
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
}
int AppearancePadding::smaller() const {
return m_tokens ? static_cast<int>(m_tokens->smaller() * m_scale) : 0;
}
int AppearancePadding::normal() const {
return m_tokens ? static_cast<int>(m_tokens->normal() * m_scale) : 0;
}
int AppearancePadding::larger() const {
return m_tokens ? static_cast<int>(m_tokens->larger() * m_scale) : 0;
}
int AppearancePadding::large() const {
return m_tokens ? static_cast<int>(m_tokens->large() * m_scale) : 0;
}
// FontSize
void FontSize::bindTokens(FontSizeTokens* tokens) {
m_tokens = tokens;
connectTokenSignals(tokens, this);
}
int FontSize::small() const {
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
}
int FontSize::smaller() const {
return m_tokens ? static_cast<int>(m_tokens->smaller() * m_scale) : 0;
}
int FontSize::normal() const {
return m_tokens ? static_cast<int>(m_tokens->normal() * m_scale) : 0;
}
int FontSize::larger() const {
return m_tokens ? static_cast<int>(m_tokens->larger() * m_scale) : 0;
}
int FontSize::large() const {
return m_tokens ? static_cast<int>(m_tokens->large() * m_scale) : 0;
}
int FontSize::extraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraLarge() * m_scale) : 0;
}
// AnimDurations
void AnimDurations::bindTokens(AnimDurationTokens* tokens) {
m_tokens = tokens;
connectTokenSignals(tokens, this);
}
int AnimDurations::small() const {
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
}
int AnimDurations::normal() const {
return m_tokens ? static_cast<int>(m_tokens->normal() * m_scale) : 0;
}
int AnimDurations::large() const {
return m_tokens ? static_cast<int>(m_tokens->large() * m_scale) : 0;
}
int AnimDurations::extraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraLarge() * m_scale) : 0;
}
int AnimDurations::expressiveFastSpatial() const {
return m_tokens ? static_cast<int>(m_tokens->expressiveFastSpatial() * m_scale) : 0;
}
int AnimDurations::expressiveDefaultSpatial() const {
return m_tokens ? static_cast<int>(m_tokens->expressiveDefaultSpatial() * m_scale) : 0;
}
int AnimDurations::expressiveSlowSpatial() const {
return m_tokens ? static_cast<int>(m_tokens->expressiveSlowSpatial() * m_scale) : 0;
}
} // namespace caelestia::config

View file

@ -6,76 +6,93 @@
namespace caelestia::config {
// Forward declare token types from advancedconfig.hpp
class RoundingTokens;
class SpacingTokens;
class PaddingTokens;
class FontSizeTokens;
class AnimDurationTokens;
class AppearanceRounding : public ConfigObject {
Q_OBJECT
CONFIG_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int small READ small NOTIFY scaleChanged)
Q_PROPERTY(int normal READ normal NOTIFY scaleChanged)
Q_PROPERTY(int large READ large NOTIFY scaleChanged)
Q_PROPERTY(int full READ full NOTIFY scaleChanged)
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
Q_PROPERTY(int normal READ normal NOTIFY valuesChanged)
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
Q_PROPERTY(int full READ full NOTIFY valuesChanged)
public:
explicit AppearanceRounding(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] int small() const { return static_cast<int>(12 * m_scale); }
void bindTokens(RoundingTokens* tokens);
[[nodiscard]] int normal() const { return static_cast<int>(17 * m_scale); }
[[nodiscard]] int small() const;
[[nodiscard]] int normal() const;
[[nodiscard]] int large() const;
[[nodiscard]] int full() const;
[[nodiscard]] int large() const { return static_cast<int>(25 * m_scale); }
Q_SIGNAL void valuesChanged();
[[nodiscard]] int full() const { return static_cast<int>(1000 * m_scale); }
private:
RoundingTokens* m_tokens = nullptr;
};
class AppearanceSpacing : public ConfigObject {
Q_OBJECT
CONFIG_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int small READ small NOTIFY scaleChanged)
Q_PROPERTY(int smaller READ smaller NOTIFY scaleChanged)
Q_PROPERTY(int normal READ normal NOTIFY scaleChanged)
Q_PROPERTY(int larger READ larger NOTIFY scaleChanged)
Q_PROPERTY(int large READ large NOTIFY scaleChanged)
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged)
Q_PROPERTY(int normal READ normal NOTIFY valuesChanged)
Q_PROPERTY(int larger READ larger NOTIFY valuesChanged)
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
public:
explicit AppearanceSpacing(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] int small() const { return static_cast<int>(7 * m_scale); }
void bindTokens(SpacingTokens* tokens);
[[nodiscard]] int smaller() const { return static_cast<int>(10 * m_scale); }
[[nodiscard]] int small() const;
[[nodiscard]] int smaller() const;
[[nodiscard]] int normal() const;
[[nodiscard]] int larger() const;
[[nodiscard]] int large() const;
[[nodiscard]] int normal() const { return static_cast<int>(12 * m_scale); }
Q_SIGNAL void valuesChanged();
[[nodiscard]] int larger() const { return static_cast<int>(15 * m_scale); }
[[nodiscard]] int large() const { return static_cast<int>(20 * m_scale); }
private:
SpacingTokens* m_tokens = nullptr;
};
class AppearancePadding : public ConfigObject {
Q_OBJECT
CONFIG_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int small READ small NOTIFY scaleChanged)
Q_PROPERTY(int smaller READ smaller NOTIFY scaleChanged)
Q_PROPERTY(int normal READ normal NOTIFY scaleChanged)
Q_PROPERTY(int larger READ larger NOTIFY scaleChanged)
Q_PROPERTY(int large READ large NOTIFY scaleChanged)
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged)
Q_PROPERTY(int normal READ normal NOTIFY valuesChanged)
Q_PROPERTY(int larger READ larger NOTIFY valuesChanged)
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
public:
explicit AppearancePadding(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] int small() const { return static_cast<int>(5 * m_scale); }
void bindTokens(PaddingTokens* tokens);
[[nodiscard]] int smaller() const { return static_cast<int>(7 * m_scale); }
[[nodiscard]] int small() const;
[[nodiscard]] int smaller() const;
[[nodiscard]] int normal() const;
[[nodiscard]] int larger() const;
[[nodiscard]] int large() const;
[[nodiscard]] int normal() const { return static_cast<int>(10 * m_scale); }
Q_SIGNAL void valuesChanged();
[[nodiscard]] int larger() const { return static_cast<int>(12 * m_scale); }
[[nodiscard]] int large() const { return static_cast<int>(15 * m_scale); }
private:
PaddingTokens* m_tokens = nullptr;
};
class FontFamily : public ConfigObject {
@ -94,28 +111,30 @@ class FontSize : public ConfigObject {
Q_OBJECT
CONFIG_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int small READ small NOTIFY scaleChanged)
Q_PROPERTY(int smaller READ smaller NOTIFY scaleChanged)
Q_PROPERTY(int normal READ normal NOTIFY scaleChanged)
Q_PROPERTY(int larger READ larger NOTIFY scaleChanged)
Q_PROPERTY(int large READ large NOTIFY scaleChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY scaleChanged)
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged)
Q_PROPERTY(int normal READ normal NOTIFY valuesChanged)
Q_PROPERTY(int larger READ larger NOTIFY valuesChanged)
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged)
public:
explicit FontSize(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] int small() const { return static_cast<int>(11 * m_scale); }
void bindTokens(FontSizeTokens* tokens);
[[nodiscard]] int smaller() const { return static_cast<int>(12 * m_scale); }
[[nodiscard]] int small() const;
[[nodiscard]] int smaller() const;
[[nodiscard]] int normal() const;
[[nodiscard]] int larger() const;
[[nodiscard]] int large() const;
[[nodiscard]] int extraLarge() const;
[[nodiscard]] int normal() const { return static_cast<int>(13 * m_scale); }
Q_SIGNAL void valuesChanged();
[[nodiscard]] int larger() const { return static_cast<int>(15 * m_scale); }
[[nodiscard]] int large() const { return static_cast<int>(18 * m_scale); }
[[nodiscard]] int extraLarge() const { return static_cast<int>(28 * m_scale); }
private:
FontSizeTokens* m_tokens = nullptr;
};
class AppearanceFont : public ConfigObject {
@ -134,31 +153,32 @@ class AnimDurations : public ConfigObject {
Q_OBJECT
CONFIG_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int small READ small NOTIFY scaleChanged)
Q_PROPERTY(int normal READ normal NOTIFY scaleChanged)
Q_PROPERTY(int large READ large NOTIFY scaleChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY scaleChanged)
Q_PROPERTY(int expressiveFastSpatial READ expressiveFastSpatial NOTIFY scaleChanged)
Q_PROPERTY(int expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY scaleChanged)
Q_PROPERTY(int expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY scaleChanged)
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
Q_PROPERTY(int normal READ normal NOTIFY valuesChanged)
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged)
Q_PROPERTY(int expressiveFastSpatial READ expressiveFastSpatial NOTIFY valuesChanged)
Q_PROPERTY(int expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY valuesChanged)
Q_PROPERTY(int expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY valuesChanged)
public:
explicit AnimDurations(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] int small() const { return static_cast<int>(200 * m_scale); }
void bindTokens(AnimDurationTokens* tokens);
[[nodiscard]] int normal() const { return static_cast<int>(400 * m_scale); }
[[nodiscard]] int small() const;
[[nodiscard]] int normal() const;
[[nodiscard]] int large() const;
[[nodiscard]] int extraLarge() const;
[[nodiscard]] int expressiveFastSpatial() const;
[[nodiscard]] int expressiveDefaultSpatial() const;
[[nodiscard]] int expressiveSlowSpatial() const;
[[nodiscard]] int large() const { return static_cast<int>(600 * m_scale); }
Q_SIGNAL void valuesChanged();
[[nodiscard]] int extraLarge() const { return static_cast<int>(1000 * m_scale); }
[[nodiscard]] int expressiveFastSpatial() const { return static_cast<int>(350 * m_scale); }
[[nodiscard]] int expressiveDefaultSpatial() const { return static_cast<int>(500 * m_scale); }
[[nodiscard]] int expressiveSlowSpatial() const { return static_cast<int>(650 * m_scale); }
private:
AnimDurationTokens* m_tokens = nullptr;
};
class AppearanceAnim : public ConfigObject {

View file

@ -1,17 +1,15 @@
#include "config.hpp"
#include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qlogging.h>
#include <qstandardpaths.h>
namespace caelestia::config {
static GlobalConfig* s_instance = nullptr;
static QString configDir() {
return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/caelestia/");
}
GlobalConfig::GlobalConfig(QObject* parent)
: ConfigObject(parent)
, m_appearance(new AppearanceConfig(this))
@ -34,91 +32,32 @@ GlobalConfig::GlobalConfig(QObject* parent)
, m_advanced(new AdvancedConfig(this)) {
s_instance = this;
m_configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) +
QStringLiteral("/caelestia/shell.json");
// Bind token base values from advanced config to appearance computed properties
auto* adv = m_advanced->appearance();
m_appearance->rounding()->bindTokens(adv->rounding());
m_appearance->spacing()->bindTokens(adv->spacing());
m_appearance->padding()->bindTokens(adv->padding());
m_appearance->font()->size()->bindTokens(adv->fontSize());
m_appearance->anim()->durations()->bindTokens(adv->animDurations());
m_saveTimer.setSingleShot(true);
m_saveTimer.setInterval(500);
connect(&m_saveTimer, &QTimer::timeout, this, &GlobalConfig::writeToFile);
m_cooldownTimer.setSingleShot(true);
m_cooldownTimer.setInterval(2000);
connect(&m_cooldownTimer, &QTimer::timeout, this, [this] {
m_recentlySaved = false;
emit recentlySavedChanged();
});
connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, &GlobalConfig::onFileChanged);
reload();
if (QFile::exists(m_configPath)) {
m_watcher.addPath(m_configPath);
}
// Each has its own file backend
setupFileBackend(configDir() + QStringLiteral("shell.json"));
m_advanced->setupFileBackend(configDir() + QStringLiteral("advanced.json"));
}
GlobalConfig* GlobalConfig::instance() {
return s_instance;
}
bool GlobalConfig::recentlySaved() const {
return m_recentlySaved;
}
void GlobalConfig::save() {
m_saveTimer.start();
m_recentlySaved = true;
emit recentlySavedChanged();
m_cooldownTimer.start();
saveToFile();
}
void GlobalConfig::reload() {
QFile file(m_configPath);
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "Config: failed to open" << m_configPath;
return;
}
load(file.readAll());
reloadFromFile();
}
void GlobalConfig::load(const QByteArray& contents) {
QJsonParseError error{};
auto doc = QJsonDocument::fromJson(contents, &error);
if (error.error != QJsonParseError::NoError) {
qWarning() << "Config: failed to parse JSON:" << error.errorString();
return;
}
loadFromJson(doc.object());
}
void GlobalConfig::onFileChanged() {
if (!m_watcher.files().contains(m_configPath)) {
m_watcher.addPath(m_configPath);
}
if (!m_recentlySaved) {
reload();
}
}
void GlobalConfig::writeToFile() {
QDir().mkpath(QFileInfo(m_configPath).absolutePath());
QFile file(m_configPath);
if (!file.open(QIODevice::WriteOnly)) {
qWarning() << "Config: failed to write" << m_configPath;
return;
}
file.write(QJsonDocument(toJsonObject()).toJson(QJsonDocument::Indented));
}
// Config
// Config (attached type)
Config::Config(QQuickItem* parent)
: QQuickItem(parent) {}

View file

@ -1,12 +1,7 @@
#pragma once
#include <qfilesystemwatcher.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qquickitem.h>
#include <qtimer.h>
#include "advancedconfig.hpp"
#include "appearanceconfig.hpp"
@ -51,33 +46,21 @@ class GlobalConfig : public ConfigObject {
CONFIG_SUBOBJECT(SidebarConfig, sidebar)
CONFIG_SUBOBJECT(ServiceConfig, services)
CONFIG_SUBOBJECT(UserPaths, paths)
CONFIG_SUBOBJECT(AdvancedConfig, advanced)
Q_PROPERTY(bool recentlySaved READ recentlySaved NOTIFY recentlySavedChanged)
// advanced is NOT a CONFIG_SUBOBJECT — it has its own file backend
Q_PROPERTY(AdvancedConfig* advanced READ advanced CONSTANT)
public:
explicit GlobalConfig(QObject* parent = nullptr);
static GlobalConfig* instance();
[[nodiscard]] bool recentlySaved() const;
[[nodiscard]] AdvancedConfig* advanced() const { return m_advanced; }
Q_INVOKABLE void save();
Q_INVOKABLE void reload();
signals:
void recentlySavedChanged();
private:
void load(const QByteArray& contents);
void onFileChanged();
void writeToFile();
bool m_recentlySaved = false;
QString m_configPath;
QFileSystemWatcher m_watcher;
QTimer m_saveTimer;
QTimer m_cooldownTimer;
AdvancedConfig* m_advanced = nullptr;
};
class Config : public QQuickItem {

View file

@ -1,6 +1,10 @@
#include "configobject.hpp"
#include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qjsonarray.h>
#include <qjsondocument.h>
#include <qjsonvalue.h>
#include <qloggingcategory.h>
#include <qmetaobject.h>
@ -104,4 +108,74 @@ QJsonObject ConfigObject::toJsonObject() const {
return obj;
}
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_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" << m_filePath;
return;
}
file.write(QJsonDocument(toJsonObject()).toJson(QJsonDocument::Indented));
});
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);
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();
}
void ConfigObject::reloadFromFile() {
QFile file(m_filePath);
if (!file.open(QIODevice::ReadOnly)) {
qCDebug(lcConfig) << "Failed to open" << m_filePath;
return;
}
QJsonParseError error{};
auto doc = QJsonDocument::fromJson(file.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(lcConfig) << "Failed to parse" << m_filePath << ":" << error.errorString();
return;
}
loadFromJson(doc.object());
}
void ConfigObject::onFileChanged() {
if (!m_watcher->files().contains(m_filePath))
m_watcher->addPath(m_filePath);
if (!m_recentlySaved)
reloadFromFile();
}
} // namespace caelestia::config

View file

@ -1,10 +1,10 @@
#pragma once
#include <qfilesystemwatcher.h>
#include <qjsonobject.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <type_traits>
#include <qtimer.h>
// Declares a serialized config property with getter, setter (change-detected), signal, and member.
#define CONFIG_PROPERTY(Type, name, ...) \
@ -16,7 +16,7 @@ public:
} \
void set_##name(const Type& val) { \
if (caelestia::config::ConfigObject::updateMember(m_##name, val)) \
emit name##Changed(); \
Q_EMIT name##Changed(); \
} \
Q_SIGNAL void name##Changed(); \
\
@ -47,6 +47,14 @@ public:
void loadFromJson(const QJsonObject& obj);
[[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();
void reloadFromFile();
[[nodiscard]] bool recentlySaved() const { return m_recentlySaved; }
template <typename T> static bool updateMember(T& member, const T& value) {
if constexpr (std::is_floating_point_v<T>) {
if (qFuzzyCompare(member + 1.0, value + 1.0))
@ -58,6 +66,16 @@ public:
member = value;
return true;
}
private:
void onFileChanged();
QString m_filePath;
bool m_recentlySaved = false;
// These are heap-allocated only when setupFileBackend is called
QFileSystemWatcher* m_watcher = nullptr;
QTimer* m_saveTimer = nullptr;
QTimer* m_cooldownTimer = nullptr;
};
} // namespace caelestia::config