#include "configobject.hpp" #include #include #include #include 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 known; QSet 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()) { 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()) { 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()) 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(); if (!node) continue; const auto childKeys = node->unknownKeys(); for (const auto& childKey : childKeys) keys.append(joinPath(QString::fromUtf8(prop.name()), childKey)); } return keys; } QList ConfigObject::childNodes() const { QList 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()) 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()) { 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()) { if (auto* const globalNode = prop.read(m_global).value()) 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()) { 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()) { node->clearLoadedKeys(); node->resyncFromGlobal(); return; } if (m_global && prop.isWritable()) prop.write(this, prop.read(m_global)); } void ConfigObject::onGlobalPropertiesChanged( const QMap& 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() == 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