nosignal-shell/plugin/src/Caelestia/Config/configobject.cpp

252 lines
7.8 KiB
C++

#include "configobject.hpp"
#include <qjsonarray.h>
#include <qjsonvalue.h>
#include <qloggingcategory.h>
#include <qmetaobject.h>
#include <qstringlist.h>
#include <qvariant.h>
namespace caelestia::config {
Q_LOGGING_CATEGORY(lcConfig, "caelestia.config", QtInfoMsg)
// ConfigObject
ConfigObject::ConfigObject(QObject* parent)
: QObject(parent) {}
void ConfigObject::loadFromJson(const QJsonObject& obj) {
const auto* meta = metaObject();
qCDebug(lcConfig) << "Loading JSON into" << meta->className() << "with" << obj.keys().size()
<< "keys:" << obj.keys();
for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name());
if (!obj.contains(key))
continue;
const auto jsonVal = obj.value(key);
// Recurse into sub-objects
auto current = prop.read(this);
auto* subObj = current.value<ConfigObject*>();
if (subObj) {
qCDebug(lcConfig) << " Recursing into sub-object" << key;
subObj->loadFromJson(jsonVal.toObject());
continue;
}
// Skip read-only properties
if (!prop.isWritable())
continue;
// Handle QStringList explicitly (QJsonArray → QStringList needs manual conversion)
if (prop.metaType().id() == QMetaType::QStringList) {
QStringList list;
const auto jsonArr = jsonVal.toArray();
for (const auto& v : jsonArr)
list.append(v.toString());
prop.write(this, QVariant::fromValue(list));
m_loadedKeys.insert(key);
qCDebug(lcConfig) << " Loaded" << key << "=" << list;
continue;
}
// For all other types, let Qt's variant conversion handle it
prop.write(this, jsonVal.toVariant());
m_loadedKeys.insert(key);
qCDebug(lcConfig) << " Loaded" << key << "=" << jsonVal.toVariant();
}
}
QJsonObject ConfigObject::toJsonObject() const {
QJsonObject obj;
const auto* meta = metaObject();
for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
if (!prop.isReadable())
continue;
const auto key = QString::fromUtf8(prop.name());
const auto value = prop.read(this);
// Recurse into sub-objects — include only if they have loaded keys
if (value.canView<ConfigObject*>()) {
auto* const subObj = value.value<ConfigObject*>();
if (subObj) {
auto subJson = subObj->toJsonObject();
if (!subJson.isEmpty())
obj.insert(key, subJson);
}
continue;
}
// Only include properties that were explicitly loaded
if (!m_loadedKeys.contains(key))
continue;
if (!prop.isWritable())
continue;
if (prop.metaType().id() == QMetaType::QStringList) {
QJsonArray arr;
const auto strList = value.toStringList();
for (const auto& s : strList)
arr.append(s);
obj.insert(key, arr);
continue;
}
if (prop.metaType().id() == QMetaType::QVariantList) {
obj.insert(key, QJsonArray::fromVariantList(value.toList()));
continue;
}
obj.insert(key, QJsonValue::fromVariant(value));
}
return obj;
}
void ConfigObject::clearLoadedKeys() {
m_loadedKeys.clear();
const auto* meta = metaObject();
for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
auto value = prop.read(this);
auto* subObj = value.value<ConfigObject*>();
if (subObj)
subObj->clearLoadedKeys();
}
}
void ConfigObject::syncFromGlobal(ConfigObject* global) {
m_global = global;
const auto* meta = metaObject();
qCDebug(lcConfig) << "Syncing" << meta->className() << "from global, loaded keys:" << m_loadedKeys;
// Connect batched change signal (single connection per ConfigObject pair)
connect(global, &ConfigObject::propertiesChanged, this, &ConfigObject::onGlobalPropertiesChanged);
// Initial sync: copy all non-loaded property values from global
for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name());
auto current = prop.read(this);
auto* subObj = current.value<ConfigObject*>();
if (subObj) {
auto globalVal = prop.read(global);
auto* globalSub = globalVal.value<ConfigObject*>();
if (globalSub)
subObj->syncFromGlobal(globalSub);
continue;
}
if (!prop.isWritable())
continue;
if (!m_loadedKeys.contains(key)) {
auto val = prop.read(global);
prop.write(this, val);
m_loadedKeys.remove(key); // setter added it — remove since this is a synced value
qCDebug(lcConfig) << " Synced" << key << "=" << val << "from global";
} else {
qCDebug(lcConfig) << " Keeping loaded" << key << "=" << prop.read(this);
}
}
}
void ConfigObject::resyncFromGlobal() {
if (!m_global)
return;
const auto* meta = metaObject();
for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name());
auto current = prop.read(this);
auto* subObj = current.value<ConfigObject*>();
if (subObj) {
subObj->resyncFromGlobal();
continue;
}
if (!prop.isWritable())
continue;
if (!m_loadedKeys.contains(key)) {
prop.write(this, prop.read(m_global));
m_loadedKeys.remove(key); // setter added it — remove since this is a synced value
}
}
}
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) {
for (auto it = changed.begin(); it != changed.end(); ++it) {
if (m_loadedKeys.contains(it.key()))
continue;
int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData());
if (idx >= 0) {
metaObject()->property(idx).write(this, it.value());
m_loadedKeys.remove(it.key()); // setter added it — remove since this is a synced value
qCDebug(lcConfig) << metaObject()->className() << "synced" << it.key() << "=" << it.value()
<< "from global change";
}
}
}
void ConfigObject::notifyPropertyChanged(const QString& name, const QVariant& value) {
m_pendingChanges.insert(name, value);
if (!m_batchTimer) {
m_batchTimer = new QTimer(this);
m_batchTimer->setSingleShot(true);
m_batchTimer->setInterval(0);
connect(m_batchTimer, &QTimer::timeout, this, &ConfigObject::emitBatchedChanges);
}
m_batchTimer->start();
}
void ConfigObject::emitBatchedChanges() {
if (m_pendingChanges.isEmpty())
return;
auto changes = std::move(m_pendingChanges);
m_pendingChanges.clear();
emit propertiesChanged(changes);
}
} // namespace caelestia::config