345 lines
8.2 KiB
C++
345 lines
8.2 KiB
C++
#include "configobject.hpp"
|
|
|
|
#include <QJsonArray>
|
|
#include <QMetaObject>
|
|
#include <QStringList>
|
|
#include <QVariant>
|
|
|
|
namespace ZShell::config {
|
|
|
|
namespace {
|
|
|
|
bool isStringArray(const QJsonArray& arr) {
|
|
for (const auto& v : arr) {
|
|
if (!v.isString()) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool jsonValueMatchesType(const QJsonValue& val, QMetaType type) {
|
|
switch (val.type()) {
|
|
case QJsonValue::Bool:
|
|
return type.id() == QMetaType::Bool;
|
|
|
|
case QJsonValue::Double:
|
|
switch (type.id()) {
|
|
case QMetaType::Int:
|
|
case QMetaType::UInt:
|
|
case QMetaType::LongLong:
|
|
case QMetaType::ULongLong:
|
|
case QMetaType::Double:
|
|
case QMetaType::Float:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
case QJsonValue::String:
|
|
return type.id() == QMetaType::QString;
|
|
|
|
case QJsonValue::Array:
|
|
if (type.id() == QMetaType::QStringList)
|
|
return isStringArray(val.toArray());
|
|
return type.id() == QMetaType::QVariantList;
|
|
|
|
case QJsonValue::Object:
|
|
case QJsonValue::Null:
|
|
case QJsonValue::Undefined:
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ConfigObject::ConfigObject(QObject* parent) : ConfigNode(parent) {}
|
|
|
|
void ConfigObject::loadFromJson(const QJsonValue& json) {
|
|
const auto obj = json.toObject();
|
|
const auto* meta = metaObject();
|
|
|
|
QSet<QString> known;
|
|
QSet<QString> invalid;
|
|
|
|
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
|
|
auto prop = meta->property(i);
|
|
const auto key = QString::fromUtf8(prop.name());
|
|
|
|
known.insert(key);
|
|
|
|
if (!obj.contains(key)) continue;
|
|
|
|
const auto jsonVal = obj.value(key);
|
|
|
|
if (prop.metaType().flags().testFlag(QMetaType::PointerToQObject)) {
|
|
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
|
|
node->loadFromJson(jsonVal);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!prop.isWritable()) continue;
|
|
|
|
if (!jsonValueMatchesType(jsonVal, prop.metaType())) {
|
|
qWarning() << "Config: type mismatch for" << key << "in"
|
|
<< meta->className() << "- expected"
|
|
<< prop.metaType().name() << "got value" << jsonVal
|
|
<< "- resetting to default";
|
|
invalid.insert(key);
|
|
continue;
|
|
}
|
|
|
|
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);
|
|
continue;
|
|
}
|
|
|
|
if (prop.metaType().id() == QMetaType::QVariantList) {
|
|
prop.write(this, jsonVal.toVariant());
|
|
m_loadedKeys.insert(key);
|
|
continue;
|
|
}
|
|
|
|
prop.write(this, jsonVal.toVariant());
|
|
m_loadedKeys.insert(key);
|
|
}
|
|
|
|
m_extras = {};
|
|
for (auto it = obj.begin(); it != obj.end(); ++it) {
|
|
if (!known.contains(it.key()) || invalid.contains(it.key())) {
|
|
m_extras.insert(it.key(), it.value());
|
|
}
|
|
}
|
|
|
|
materializeDefaults();
|
|
}
|
|
|
|
QJsonValue ConfigObject::toJson() const {
|
|
QJsonObject obj;
|
|
const auto* meta = metaObject();
|
|
|
|
for (int i = basePropertyOffset(); 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);
|
|
|
|
if (prop.metaType().flags().testFlag(QMetaType::PointerToQObject)) {
|
|
if (auto* const node = value.value<ConfigNode*>()) {
|
|
const auto childJson = node->toJson();
|
|
if (!childJson.isUndefined()) obj.insert(key, childJson);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
if (obj.isEmpty()) return QJsonValue::Undefined;
|
|
|
|
return obj;
|
|
}
|
|
|
|
void ConfigObject::clearLoadedKeys() {
|
|
m_loadedKeys.clear();
|
|
m_extras = {};
|
|
|
|
const auto* meta = metaObject();
|
|
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
|
|
auto prop = meta->property(i);
|
|
if (auto* const node = prop.read(this).value<ConfigNode*>())
|
|
node->clearLoadedKeys();
|
|
}
|
|
}
|
|
|
|
QStringList ConfigObject::identityKeys() const {
|
|
return {};
|
|
}
|
|
|
|
QStringList ConfigObject::unknownKeys() const {
|
|
auto keys = m_extras.keys();
|
|
|
|
const auto* meta = metaObject();
|
|
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
|
|
const auto prop = meta->property(i);
|
|
auto* const node = prop.read(this).value<ConfigNode*>();
|
|
if (!node) continue;
|
|
|
|
const auto childKeys = node->unknownKeys();
|
|
for (const auto& childKey : childKeys)
|
|
keys.append(joinPath(QString::fromUtf8(prop.name()), childKey));
|
|
}
|
|
|
|
return keys;
|
|
}
|
|
|
|
QList<ConfigNode*> ConfigObject::childNodes() const {
|
|
QList<ConfigNode*> nodes;
|
|
|
|
const auto* meta = metaObject();
|
|
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
|
|
const auto prop = meta->property(i);
|
|
|
|
if (auto* const node = prop.read(this).value<ConfigNode*>())
|
|
nodes.append(node);
|
|
}
|
|
|
|
return nodes;
|
|
}
|
|
|
|
void ConfigObject::materializeDefaults() {
|
|
const auto* meta = metaObject();
|
|
|
|
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
|
|
const auto prop = meta->property(i);
|
|
const auto key = QString::fromUtf8(prop.name());
|
|
|
|
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
|
|
node->materializeDefaults();
|
|
continue;
|
|
}
|
|
|
|
if (!prop.isWritable()) continue;
|
|
|
|
if (m_global) continue;
|
|
|
|
m_loadedKeys.insert(key);
|
|
}
|
|
}
|
|
|
|
void ConfigObject::syncValuesFromGlobal() {
|
|
const auto* meta = metaObject();
|
|
|
|
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
|
|
auto prop = meta->property(i);
|
|
const auto key = QString::fromUtf8(prop.name());
|
|
|
|
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
|
|
if (auto* const globalNode =
|
|
prop.read(m_global).value<ConfigNode*>())
|
|
node->syncFromGlobal(globalNode);
|
|
continue;
|
|
}
|
|
|
|
if (!prop.isWritable()) continue;
|
|
|
|
if (!m_loadedKeys.contains(key)) {
|
|
auto val = prop.read(m_global);
|
|
prop.write(this, val);
|
|
m_loadedKeys.remove(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ConfigObject::resyncFromGlobal() {
|
|
if (!m_global) return;
|
|
|
|
const auto* meta = metaObject();
|
|
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
|
|
auto prop = meta->property(i);
|
|
const auto key = QString::fromUtf8(prop.name());
|
|
|
|
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
|
|
node->resyncFromGlobal();
|
|
continue;
|
|
}
|
|
|
|
if (!prop.isWritable()) continue;
|
|
|
|
if (!m_loadedKeys.contains(key)) {
|
|
prop.write(this, prop.read(m_global));
|
|
m_loadedKeys.remove(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ConfigObject::isPropertyLoaded(const QString& name) const {
|
|
return m_loadedKeys.contains(name);
|
|
}
|
|
|
|
bool ConfigObject::isGlobalOnly(const QString& name) const {
|
|
return m_globalOnlyKeys.contains(name);
|
|
}
|
|
|
|
QStringList ConfigObject::globalOnlyKeys() const {
|
|
return {m_globalOnlyKeys.begin(), m_globalOnlyKeys.end()};
|
|
}
|
|
|
|
void ConfigObject::resetOption(const QString& name) {
|
|
m_loadedKeys.remove(name);
|
|
|
|
const int idx = metaObject()->indexOfProperty(name.toUtf8().constData());
|
|
if (idx < 0) return;
|
|
|
|
const auto prop = metaObject()->property(idx);
|
|
|
|
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
|
|
node->clearLoadedKeys();
|
|
node->resyncFromGlobal();
|
|
return;
|
|
}
|
|
|
|
if (m_global && 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()) || isGlobalOnly(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());
|
|
}
|
|
}
|
|
}
|
|
|
|
QString ConfigObject::childPath(const ConfigNode* child) const {
|
|
const auto* meta = metaObject();
|
|
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
|
|
const auto prop = meta->property(i);
|
|
|
|
if (prop.read(this).value<ConfigNode*>() == child)
|
|
return QString::fromUtf8(prop.name());
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
void ConfigObject::markPropertyLoaded(const QString& name) {
|
|
m_loadedKeys.insert(name);
|
|
}
|
|
|
|
void ConfigObject::markGlobalOnly(const QString& name) {
|
|
m_globalOnlyKeys.insert(name);
|
|
}
|
|
|
|
} // namespace ZShell::config
|