#include "configlist.hpp" #include #include #include #include namespace ZShell::config { ConfigList::ConfigList(QObject* parent, const QVariantList& defaults) : ConfigNode(parent), m_defaults(QJsonArray::fromVariantList(defaults)) {} int ConfigList::count() const { return static_cast(m_items.size()); } QVariantList ConfigList::values() const { QVariantList vals; vals.reserve(m_items.size()); for (auto* const item : m_items) vals.append(QVariant::fromValue(item)); return vals; } ConfigObject* ConfigList::itemAt(int index) const { if (index < 0 || index >= m_items.size()) return nullptr; return m_items.at(index); } const QList& ConfigList::items() const { return m_items; } void ConfigList::remove(int index) { if (index < 0 || index >= m_items.size()) { qCWarning(lcConfig) << metaObject()->className() << "cannot remove index" << index << "of" << count(); return; } destroyItem(m_items.takeAt(index)); m_loaded = true; emit countChanged(); emit valuesChanged(); notifyChanged(); } void ConfigList::move(int from, int to) { if (from < 0 || from >= m_items.size() || to < 0 || to >= m_items.size()) { qCWarning(lcConfig) << metaObject()->className() << "cannot move" << from << "to" << to << "of" << count(); return; } if (from == to) return; m_items.move(from, to); m_loaded = true; emit valuesChanged(); notifyChanged(); } void ConfigList::clear() { if (m_items.isEmpty()) return; destroyItems(); m_loaded = true; emit countChanged(); emit valuesChanged(); notifyChanged(); } void ConfigList::loadFromJson(const QJsonValue& json) { if (!json.isArray()) { qCWarning( lcConfig, "Option '%s' must be a list, resetting to default", qUtf8Printable(propertyPath())); resetToDefaults(); materializeDefaults(); return; } populate(json.toArray()); m_loaded = true; } QJsonValue ConfigList::toJson() const { if (m_loaded) return elementsToJson(); return QJsonValue::Undefined; } void ConfigList::clearLoadedKeys() { m_loaded = false; } QStringList ConfigList::unknownKeys() const { QStringList keys; for (int i = 0; i < m_items.size(); ++i) { const auto childKeys = m_items.at(i)->unknownKeys(); for (const auto& childKey : childKeys) keys.append(joinPath(QString("[%1]").arg(i), childKey)); } return keys; } void ConfigList::materializeDefaults() { for (auto* const item : m_items) item->materializeDefaults(); if (m_global) return; m_loaded = true; } void ConfigList::resyncFromGlobal() { syncValuesFromGlobal(); } ConfigObject* ConfigList::insertItem(const QVariantMap& props, int index) { const auto pos = index < 0 || index > m_items.size() ? m_items.size() : index; appendItem(QJsonObject::fromVariantMap(props)); m_items.move(m_items.size() - 1, pos); auto* const item = m_items.at(pos); const auto unknown = item->unknownKeys(); for (const auto& key : unknown) qCWarning(lcConfig) << "Unknown option" << key << "for" << item->metaObject()->className(); m_loaded = true; emit countChanged(); emit valuesChanged(); notifyChanged(); return item; } QJsonArray ConfigList::elementsToJson() const { QJsonArray arr; for (auto* const item : m_items) arr.append(item->toJson().toObject()); return arr; } void ConfigList::resetToDefaults() { loadFromJsonQuietly(m_defaults); m_loaded = false; } void ConfigList::syncValuesFromGlobal() { if (m_loaded) return; if (auto* const global = qobject_cast(m_global)) populate(global->elementsToJson()); } void ConfigList::onGlobalPropertiesChanged(const QMap&) { syncValuesFromGlobal(); } QString ConfigList::childPath(const ConfigNode* child) const { for (int i = 0; i < m_items.size(); ++i) if (m_items.at(i) == child) return QString("[%1]").arg(i); return {}; } void ConfigList::populate(const QJsonArray& arr) { const auto current = elementsToJson(); if (current == arr) return; const auto old = m_items; QList reused(old.size(), false); m_items.clear(); m_items.reserve(arr.size()); for (int i = 0; i < arr.size(); ++i) { auto match = i < old.size() && !reused.at(i) && current.at(i) == arr.at(i) ? i : -1; for (int j = 0; match < 0 && j < old.size(); ++j) if (!reused.at(j) && current.at(j) == arr.at(i)) match = j; auto update = false; for (int j = 0; match < 0 && j < old.size(); ++j) { if (!reused.at(j) && sameElement(old.at(j), current.at(j), arr.at(i))) { match = j; update = true; } } if (match < 0) { appendItem(arr.at(i)); continue; } reused[match] = true; if (update) old.at(match)->loadFromJsonQuietly(arr.at(i)); m_items.append(old.at(match)); } for (int i = 0; i < old.size(); ++i) if (!reused.at(i)) destroyItem(old.at(i)); if (m_items.size() != old.size()) emit countChanged(); emit valuesChanged(); notifyChanged(); } bool ConfigList::sameElement( const ConfigObject* item, const QJsonValue& current, const QJsonValue& json) { const auto keys = item->identityKeys(); if (keys.isEmpty()) return false; const auto currentObj = current.toObject(); const auto newObj = json.toObject(); if (currentObj.keys() != newObj.keys()) return false; for (const auto& key : keys) if (!newObj.contains(key) || currentObj.value(key) != newObj.value(key)) return false; return true; } void ConfigList::appendItem(const QJsonValue& json) { auto* const item = createItem(); QQmlEngine::setObjectOwnership(item, QQmlEngine::CppOwnership); if (m_items.isEmpty()) rejectGlobalOnlyElement(item); item->loadFromJsonQuietly(json); connectItem(item); m_items.append(item); } void ConfigList::rejectGlobalOnlyElement(const ConfigObject* item) const { const auto keys = item->globalOnlyKeys(); if (keys.isEmpty()) return; qCCritical( lcConfig, "%s declares global-only options (%s) which do not work inside a list, " "use CONFIG_GLOBAL_LIST on %s instead", item->metaObject()->className(), qUtf8Printable(keys.join(", ")), metaObject()->className()); } void ConfigList::connectItem(ConfigNode* node) { connect( node, &ConfigNode::propertiesChanged, this, &ConfigList::onItemChanged); const auto children = node->childNodes(); for (auto* const child : children) connectItem(child); } void ConfigList::disconnectItem(ConfigNode* node) { node->disconnect(this); const auto children = node->childNodes(); for (auto* const child : children) disconnectItem(child); } void ConfigList::destroyItems() { for (auto* const item : std::as_const(m_items)) destroyItem(item); m_items.clear(); } void ConfigList::destroyItem(ConfigObject* item) { disconnectItem(item); item->deleteLater(); } void ConfigList::onItemChanged() { m_loaded = true; notifyChanged(); } void ConfigList::notifyChanged() { notifyPropertyChanged("values", count()); } } // namespace ZShell::config