#include "listnode.hpp" #include #include namespace ZShell::settings { using Qt::StringLiterals::operator""_s; namespace { QString valuesKey() { return u"values"_s; } void deleteNode(Node* node) { node->detachFallback(); node->setParent(nullptr); node->deleteLater(); } bool isVariantListType(const QVariant& value) { return value.typeId() == QMetaType::QVariantList || value.typeId() == qMetaTypeId>(); } QList toMaps(const QVariant& value) { if (value.typeId() == qMetaTypeId>()) return value.value>(); const auto list = value.toList(); QList maps; maps.reserve(list.count()); for (const auto& el : list) { if (el.typeId() != QMetaType::QVariantMap) { qCCritical(lcSettings, "Expected a map in list properties, got %s.", el.typeName()); continue; } maps << el.toMap(); } return maps; } void syncNodeRecursive(Node* node, const QVariantMap& props) { const auto& schema = node->schema(); for (const auto& [key, val] : props.asKeyValueRange()) { const auto* desc = schema.get(key); if (!desc) { qCCritical(lcSettings, "Unknown key %s in props for %s, something is wrong.", qUtf8Printable(key), qUtf8Printable(node->path())); continue; } // `setValue` warns on type mismatches itself if (!desc->isNode) { node->setValue(key, val); continue; } auto* const child = node->value(key).value(); // Nested lists take their elements straight from props if (auto* const list = qobject_cast(child)) { if (!isVariantListType(val)) { qCCritical(lcSettings, "Expected a list for node key %s on %s, got %s.", qUtf8Printable(key), qUtf8Printable(node->path()), val.typeName()); continue; } list->setValue(valuesKey(), val); continue; } if (val.typeId() != QMetaType::QVariantMap) { qCCritical(lcSettings, "Expected a map for node key %s on %s, got %s.", qUtf8Printable(key), qUtf8Printable(node->path()), val.typeName()); continue; } syncNodeRecursive(child, val.toMap()); } } } // namespace ListNode::ListNode(ListNode* fallback, QObject* parent, bool globalOnly) : Node(fallback, parent, globalOnly) { if (fallback) { // Disconnect generic fallback notify, lists use a custom one QObject::disconnect(fallback, &ListNode::optionChanged, this, nullptr); if (!m_globalOnly) QObject::connect(fallback, &ListNode::elementsChanged, this, &ListNode::onFallbackListNotify); } } qsizetype ListNode::count() const { return m_elements.count(); } QVariantList ListNode::values() const { QVariantList list; list.reserve(m_elements.count()); for (auto* const element : m_elements) list << QVariant::fromValue(element); return list; } void ListNode::remove(qsizetype index) { if (rejectGlobalMutation()) return; const WriteScope scope(this, WriteOrigin::Qml); if (!validIndex(index)) { qCWarning(lcSettings, "Attempted to remove invalid index %lld from list node %s, ignoring.", index, qUtf8Printable(path())); recordWrite(valuesKey(), false); return; } deleteNode(m_elements.takeAt(index)); recordWrite(valuesKey(), true); emit countChanged(); emit elementsChanged({}, { index }, {}); } void ListNode::move(qsizetype from, qsizetype to) { if (rejectGlobalMutation()) return; const WriteScope scope(this, WriteOrigin::Qml); const auto fromInvalid = !validIndex(from); if (fromInvalid || !validIndex(to)) { if (fromInvalid) qCWarning(lcSettings, "Attempted to move invalid index %lld to index %lld on list node %s, ignoring.", from, to, qUtf8Printable(path())); else qCWarning(lcSettings, "Attempted to move index %lld to invalid index %lld on list node %s, ignoring.", from, to, qUtf8Printable(path())); recordWrite(valuesKey(), false); return; } m_elements.move(from, to); recordWrite(valuesKey(), true); emit elementsChanged({}, {}, { { from, to } }); } void ListNode::clear() { if (rejectGlobalMutation()) return; const WriteScope scope(this, WriteOrigin::Qml); if (m_elements.isEmpty()) { recordWrite(valuesKey(), false); return; } const auto len = m_elements.count(); for (auto* const element : std::as_const(m_elements)) deleteNode(element); m_elements.clear(); recordWrite(valuesKey(), true); emit countChanged(); NodeChanges removed(len); std::iota(removed.begin(), removed.end(), 0); emit elementsChanged({}, removed, {}); } QString ListNode::pathFor(const QString& key) const { return elementPath(path(), key); } const Schema& ListNode::schema() const { // Offset +1 so count is not registered (allow read only cause values is read only) static const auto k_schema = Schema::build(&staticMetaObject, Node::staticMetaObject.propertyCount() + 1, true); return k_schema; } QVariant ListNode::value(const QString& key) const { if (key != valuesKey()) { qCCritical(lcSettings, "Attempted to read %s on list node %s. List nodes only have a 'values' key, something is wrong.", qUtf8Printable(key), qUtf8Printable(path())); return {}; } return QVariant::fromValue(m_elements); } bool ListNode::setValue(const QString& key, const QVariant& value) { return setValue(key, value, nullptr); } bool ListNode::setValue(const QString& key, const QVariant& value, QList* diagnostics) { if (key != valuesKey()) { qCCritical(lcSettings, "Attempted to write %s on list node %s. List nodes only have a 'values' key, something is wrong.", qUtf8Printable(key), qUtf8Printable(path())); return false; } const auto len = m_elements.count(); for (auto* const element : std::as_const(m_elements)) deleteNode(element); m_elements.clear(); if (value.typeId() == qMetaTypeId>()) { // On reset to fallback const auto nodes = value.value>(); for (auto* const node : nodes) insertNode(m_elements.count(), node); } else if (isVariantListType(value)) { // From defaults or QML const auto list = toMaps(value); for (qsizetype i = 0; i < list.count(); ++i) insertNode(i, list.at(i), fallbackFor(i)); } else if (value.typeId() == QMetaType::QJsonArray) { // From syncJson const auto array = value.toJsonArray(); for (const auto& json : array) insertNode(m_elements.count(), json.toObject(), *diagnostics); } else { qCCritical(lcSettings, "Unexpected type %s for list node %s, something is wrong.", value.typeName(), qUtf8Printable(path())); return false; } if (!recordWrite(valuesKey(), true)) return false; if (len != m_elements.count()) emit countChanged(); NodeChanges added(m_elements.count()); NodeChanges removed(len); std::iota(added.begin(), added.end(), 0); std::iota(removed.begin(), removed.end(), 0); emit elementsChanged(added, removed, {}); return true; } void ListNode::resetToDefaults() { if (isNested()) { qCCritical(lcSettings, "List node %s has a parent list node, resetToDefaults should never be called on it.", qUtf8Printable(path())); return; } // Don't reset global only list nodes on overlays if (fallbackNode() && m_globalOnly) return; const WriteScope scope(this, WriteOrigin::FileReset); setValue(valuesKey(), fallbackNode() ? fallbackNode()->value(valuesKey()) : QVariant::fromValue(defaultValue())); } QJsonValue ListNode::toJson(bool sparse) const { Q_UNUSED(sparse) // Lists can't be sparse QJsonArray array; for (auto* const element : m_elements) array << element->toJson(false); return array; } bool ListNode::syncJson(const QJsonValue& json, QList& diagnostics) { if (!json.isArray()) { const auto d = Diagnostic::mismatch(ExpectedType::Array, json, path()); qCWarning(lcSettings, "Error decoding option %s: %s", qUtf8Printable(d.option), qUtf8Printable(d.message)); diagnostics << d; return false; } // Refuse syncs to global only list nodes on overlays if (rejectGlobalSync(diagnostics)) return false; const WriteScope scope(this, WriteOrigin::File); setValue(valuesKey(), json.toArray(), &diagnostics); return true; } bool ListNode::recordWrite(const QString& key, bool changed) { const auto wasOverride = isOverride(key); const auto notify = Node::recordWrite(key, changed); // Elements inside overridden lists have no fallbacks, so detach here if (!wasOverride && isOverride(key)) for (auto* const element : std::as_const(m_elements)) element->detachFallback(); return notify; } QString ListNode::keyOf(const Node* child) const { return QString::number(m_elements.indexOf(const_cast(child))); } Node* ListNode::elementAt(qsizetype index) const { if (!validIndex(index)) return nullptr; return m_elements.at(index); } Node* ListNode::insertElement(const QVariantMap& props, qsizetype index) { if (rejectGlobalMutation()) return nullptr; const WriteScope scope(this, WriteOrigin::Qml); if (!validIndex(index)) // Invalid index means append index = m_elements.count(); auto* const element = insertNode(index, props, nullptr); recordWrite(valuesKey(), true); emit countChanged(); emit elementsChanged({ index }, {}, {}); return element; } QList ListNode::defaultValue() const { const auto* desc = getDescriptor(); if (!desc) return {}; return desc->defaultValue().value>(); } bool ListNode::isNested() const { return qobject_cast(parentNode()); } bool ListNode::rejectGlobalMutation() const { if (!m_globalOnly || !fallbackNode()) return false; qCWarning(lcSettings, "Attempted to mutate global list %s from an overlay layer, ignoring. " "This should not be used, mutate global lists from the global layer instead.", qUtf8Printable(path())); return true; } bool ListNode::validIndex(qsizetype index) const { return index >= 0 && index < m_elements.count(); } const Descriptor* ListNode::getDescriptor() const { if (!parentNode()) { // List nodes should not be root nodes qCCritical(lcSettings, "List node %s has no parent, something is wrong.", qUtf8Printable(path())); return nullptr; } const auto* desc = parentNode()->schema().get(key()); if (!desc) { qCCritical(lcSettings, "List node %s is not in parent schema, something is wrong.", qUtf8Printable(path())); return nullptr; } return desc; } Node* ListNode::fallbackFor(qsizetype index) const { if (isOverride(valuesKey())) return nullptr; const auto* fallback = static_cast(fallbackNode()); if (!fallback || !fallback->validIndex(index)) return nullptr; return fallback->m_elements.at(index); } Node* ListNode::insertNode(qsizetype index, const QVariantMap& props, Node* fallback) { auto* const node = createElement(fallback); m_elements.insert(index, node); const WriteScope scope(node, WriteOrigin::Init); syncNodeRecursive(node, props); return node; } Node* ListNode::insertNode(qsizetype index, Node* node) { auto* const copy = createElement(node); m_elements.insert(index, copy); return copy; } Node* ListNode::insertNode(qsizetype index, const QJsonObject& json, QList& diagnostics) { // Write origin will be file/file reset, which is correct. // This function should only be called from the JSON path of setValue. auto* const node = createElement(nullptr); // Nodes synced from JSON get no fallback as they are overrides m_elements.insert(index, node); node->syncJson(json, diagnostics); return node; } void ListNode::onFallbackListNotify(const NodeChanges& added, const NodeChanges& removed, const MoveChanges& moved) { if (isOverride(valuesKey())) return; const WriteScope scope(this, WriteOrigin::Layer); for (const auto index : removed | std::views::reverse) deleteNode(m_elements.takeAt(index)); for (const auto index : added) insertNode(index, fallbackFor(index)); for (const auto& move : moved) m_elements.move(move.first, move.second); if (!recordWrite(valuesKey(), true)) return; if (!added.isEmpty() || !removed.isEmpty()) emit countChanged(); emit elementsChanged(added, removed, moved); } } // namespace ZShell::settings