From 18febc9d76de021e4828a9072577da32faf20f75 Mon Sep 17 00:00:00 2001 From: Ali Kianian Date: Fri, 3 May 2024 18:03:11 +0300 Subject: [PATCH] QmlDesigner: Support more json structures in Model Editor * A visitor is added to detect the property order of the nested json models. * A pure json object is defined as a json which does not contain any array or object as its member. * All of the json lists which has pure models, will be imported. * A pure object which is a child of another object, will be imported. Fixes: QDS-12546 Change-Id: Ib44e1567e3dde0fc5cb433b4f1dc20358e6a3949 Reviewed-by: Mahmoud Badri --- src/plugins/qmldesigner/CMakeLists.txt | 1 + .../collectioneditor/collectiondetails.cpp | 111 +------- .../collectioneditor/collectiondetails.h | 4 +- .../collectioneditorutils.cpp | 15 +- .../collectioneditor/collectionjsonparser.cpp | 257 ++++++++++++++++++ .../collectioneditor/collectionjsonparser.h | 58 ++++ .../collectioneditor/collectionwidget.cpp | 33 ++- 7 files changed, 368 insertions(+), 111 deletions(-) create mode 100644 src/plugins/qmldesigner/components/collectioneditor/collectionjsonparser.cpp create mode 100644 src/plugins/qmldesigner/components/collectioneditor/collectionjsonparser.h diff --git a/src/plugins/qmldesigner/CMakeLists.txt b/src/plugins/qmldesigner/CMakeLists.txt index d41518e0182..5d34810d438 100644 --- a/src/plugins/qmldesigner/CMakeLists.txt +++ b/src/plugins/qmldesigner/CMakeLists.txt @@ -856,6 +856,7 @@ extend_qtc_plugin(QmlDesigner collectiondetailssortfiltermodel.cpp collectiondetailssortfiltermodel.h collectioneditorconstants.h collectioneditorutils.cpp collectioneditorutils.h + collectionjsonparser.cpp collectionjsonparser.h collectionlistmodel.cpp collectionlistmodel.h collectionview.cpp collectionview.h collectionwidget.cpp collectionwidget.h diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.cpp index 7e61f5e6100..eedfbd3178a 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.cpp +++ b/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.cpp @@ -5,12 +5,11 @@ #include "collectiondatatypemodel.h" #include "collectioneditorutils.h" +#include "collectionjsonparser.h" -#include -#include -#include -#include #include +#include +#include #include #include @@ -279,47 +278,6 @@ QStringList csvReadLine(const QString &line) return result; } -class PropertyOrderFinder : public QmlJS::AST::Visitor -{ -public: - static QStringList parse(const QString &jsonContent) - { - PropertyOrderFinder finder; - QmlJS::Document::MutablePtr jsonDoc = QmlJS::Document::create(Utils::FilePath::fromString( - ""), - QmlJS::Dialect::Json); - - jsonDoc->setSource(jsonContent); - jsonDoc->parseJavaScript(); - - if (!jsonDoc->isParsedCorrectly()) - return {}; - - jsonDoc->ast()->accept(&finder); - return finder.m_orderedList; - } - -protected: - bool visit(QmlJS::AST::PatternProperty *patternProperty) override - { - const QString propertyName = patternProperty->name->asString(); - if (!m_propertySet.contains(propertyName)) { - m_propertySet.insert(propertyName); - m_orderedList.append(propertyName); - } - return true; - } - - void throwRecursionDepthError() override - { - qWarning() << Q_FUNC_INFO << __LINE__ << "Recursion depth error"; - }; - -private: - QSet m_propertySet; - QStringList m_orderedList; -}; - QString CollectionParseError::errorString() const { switch (errorNo) { @@ -757,63 +715,24 @@ CollectionDetails CollectionDetails::fromImportedCsv(const QByteArray &document, return fromImportedJson(importedArray, headers); } -CollectionDetails CollectionDetails::fromImportedJson(const QByteArray &json, QJsonParseError *error) +QList CollectionDetails::fromImportedJson(const QByteArray &jsonContent, + QJsonParseError *error) { - QJsonArray importedCollection; - auto refineJsonArray = [](const QJsonArray &array) -> QJsonArray { - QJsonArray resultArray; - for (const QJsonValue &collectionData : array) { - if (collectionData.isObject()) { - QJsonObject rowObject = collectionData.toObject(); - const QStringList rowKeys = rowObject.keys(); - for (const QString &key : rowKeys) { - const QJsonValue cellValue = rowObject.value(key); - if (cellValue.isArray()) - rowObject.remove(key); - } - resultArray.push_back(rowObject); - } - } - return resultArray; - }; - QJsonParseError parseError; - QJsonDocument document = QJsonDocument::fromJson(json, &parseError); + + QList collectionObjects = JsonCollectionParser::parseCollectionObjects(jsonContent, + error); + if (error) *error = parseError; if (parseError.error != QJsonParseError::NoError) - return CollectionDetails{}; - - if (document.isArray()) { - importedCollection = refineJsonArray(document.array()); - } else if (document.isObject()) { - QJsonObject documentObject = document.object(); - const QStringList mainKeys = documentObject.keys(); - - bool arrayFound = false; - for (const QString &key : mainKeys) { - const QJsonValue value = documentObject.value(key); - if (value.isArray()) { - arrayFound = true; - importedCollection = refineJsonArray(value.toArray()); - break; - } - } - - if (!arrayFound) { - QJsonObject singleObject; - for (const QString &key : mainKeys) { - const QJsonValue value = documentObject.value(key); - - if (!value.isObject()) - singleObject.insert(key, value); - } - importedCollection.push_back(singleObject); - } - } - - return fromImportedJson(importedCollection, PropertyOrderFinder::parse(QLatin1String(json))); + return {}; + return Utils::transform(collectionObjects, [](const CollectionObject &object) { + CollectionDetails result = fromImportedJson(object.array, object.propertyOrder); + result.d->reference.name = object.name; + return result; + }); } CollectionDetails CollectionDetails::fromLocalJson(const QJsonDocument &document, diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.h b/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.h index 7243c585c6b..984feabc0a3 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.h +++ b/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.h @@ -127,8 +127,8 @@ public: static CollectionDetails fromImportedCsv(const QByteArray &document, const bool &firstRowIsHeader = true); - static CollectionDetails fromImportedJson(const QByteArray &json, - QJsonParseError *error = nullptr); + static QList fromImportedJson(const QByteArray &jsonContent, + QJsonParseError *error = nullptr); static CollectionDetails fromLocalJson(const QJsonDocument &document, const QString &collectionName, CollectionParseError *error = nullptr); diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectioneditorutils.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectioneditorutils.cpp index 29b833cc2ce..40350802d26 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectioneditorutils.cpp +++ b/src/plugins/qmldesigner/components/collectioneditor/collectioneditorutils.cpp @@ -315,18 +315,25 @@ QJsonObject defaultColorCollection() FileReader fileReader; if (!fileReader.fetch(templatePath)) { - qWarning() << Q_FUNC_INFO << __LINE__ << "Can't read the content of the file" << templatePath; + qWarning() << __FUNCTION__ << "Can't read the content of the file" << templatePath; return {}; } QJsonParseError parseError; - const CollectionDetails collection = CollectionDetails::fromImportedJson(fileReader.data(), - &parseError); + const QList collections = CollectionDetails::fromImportedJson(fileReader.data(), + &parseError); + if (parseError.error != QJsonParseError::NoError) { - qWarning() << Q_FUNC_INFO << __LINE__ << "Error in template file" << parseError.errorString(); + qWarning() << __FUNCTION__ << "Error in template file" << parseError.errorString(); return {}; } + if (!collections.size()) { + qWarning() << __FUNCTION__ << "Can not generate collections from template file!"; + return {}; + } + + const CollectionDetails collection = collections.first(); return collection.toLocalJson(); } diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionjsonparser.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectionjsonparser.cpp new file mode 100644 index 00000000000..3756af47857 --- /dev/null +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionjsonparser.cpp @@ -0,0 +1,257 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "collectionjsonparser.h" + +#include +#include +#include + +#include +#include +#include + +namespace QmlDesigner { + +/** + * @brief A json object is a plain object, if it has only primitive properties (not arrays or objects) + * @return true if @param jsonObject is a plain object + */ +inline static bool isPlainObject(const QJsonObject &jsonObj) +{ + return !Utils::anyOf(jsonObj, [](const QJsonValueConstRef &val) { + return val.isArray() || val.isObject(); + }); +} + +static bool isPlainObject(const QJsonValueConstRef &value) +{ + if (!value.isObject()) + return false; + return isPlainObject(value.toObject()); +} + +static QJsonArray parsePlainObject(const QJsonObject &jsonObj) +{ + QJsonObject result; + auto item = jsonObj.constBegin(); + const auto itemEnd = jsonObj.constEnd(); + while (item != itemEnd) { + QJsonValueConstRef ref = item.value(); + if (!ref.isArray() && !ref.isObject()) + result.insert(item.key(), ref); + ++item; + } + if (!result.isEmpty()) + return QJsonArray{result}; + + return {}; +} + +static QJsonArray parseArray(const QJsonArray &array, + QList &plainCollections, + JsonKeyChain &chainTracker) +{ + chainTracker.append(0); + QJsonArray plainArray; + int i = -1; + for (const QJsonValueConstRef &item : array) { + chainTracker.last() = ++i; + if (isPlainObject(item)) { + const QJsonObject plainObject = item.toObject(); + if (plainObject.count()) + plainArray.append(plainObject); + } else if (item.isArray()) { + parseArray(item.toArray(), plainCollections, chainTracker); + } + } + chainTracker.removeLast(); + return plainArray; +} + +static void parseObject(const QJsonObject &jsonObj, + QList &plainCollections, + JsonKeyChain &chainTracker) +{ + chainTracker.append(QString{}); + auto item = jsonObj.constBegin(); + const auto itemEnd = jsonObj.constEnd(); + while (item != itemEnd) { + chainTracker.last() = item.key(); + QJsonValueConstRef ref = item.value(); + QJsonArray parsedArray; + if (ref.isArray()) { + parsedArray = parseArray(ref.toArray(), plainCollections, chainTracker); + } else if (ref.isObject()) { + if (isPlainObject(ref)) + parsedArray = parsePlainObject(ref.toObject()); + else + parseObject(ref.toObject(), plainCollections, chainTracker); + } + if (!parsedArray.isEmpty()) + plainCollections.append({item.key(), parsedArray, chainTracker}); + ++item; + } + chainTracker.removeLast(); +} + +static QList parseDocument(const QJsonDocument &document, + const QString &defaultName = "Model") +{ + QList plainCollections; + JsonKeyChain chainTracker; + if (document.isObject()) { + const QJsonObject documentObject = document.object(); + if (isPlainObject(documentObject)) { + QJsonArray parsedArray = parsePlainObject(documentObject); + if (!parsedArray.isEmpty()) + plainCollections.append({defaultName, parsedArray}); + } else { + parseObject(document.object(), plainCollections, chainTracker); + } + } else { + QJsonArray parsedArray = parseArray(document.array(), plainCollections, chainTracker); + if (!parsedArray.isEmpty()) + plainCollections.append({defaultName, parsedArray, {0}}); + } + return plainCollections; +} + +QList JsonCollectionParser::parseCollectionObjects(const QByteArray &json, + QJsonParseError *error) +{ + QJsonParseError parseError; + QJsonDocument document = QJsonDocument::fromJson(json, &parseError); + if (error) + *error = parseError; + + if (parseError.error != QJsonParseError::NoError) + return {}; + + QList allCollections = parseDocument(document); + QList keyChains = Utils::transform(allCollections, [](const CollectionObject &obj) { + return obj.keyChain; + }); + + JsonCollectionParser jsonVisitor(QString::fromLatin1(json), keyChains); + + for (CollectionObject &collection : allCollections) + collection.propertyOrder = jsonVisitor.collectionPaths.value(collection.keyChain); + + return allCollections; +} + +JsonCollectionParser::JsonCollectionParser(const QString &jsonContent, + const QList &keyChains) +{ + for (const JsonKeyChain &chain : keyChains) + collectionPaths.insert(chain, {}); + + QmlJS::Document::MutablePtr newDoc = QmlJS::Document::create(Utils::FilePath::fromString( + ""), + QmlJS::Dialect::Json); + + newDoc->setSource(jsonContent); + newDoc->parseExpression(); + + if (!newDoc->isParsedCorrectly()) + return; + + newDoc->ast()->accept(this); +} + +bool JsonCollectionParser::visit([[maybe_unused]] QmlJS::AST::ObjectPattern *objectPattern) +{ + propertyOrderStack.push({}); + return true; +} + +void JsonCollectionParser::endVisit([[maybe_unused]] QmlJS::AST::ObjectPattern *objectPattern) + +{ + if (!propertyOrderStack.isEmpty()) { + QStringList objectProperties = propertyOrderStack.top(); + propertyOrderStack.pop(); + checkPropertyUpdates(keyStack, objectProperties); + } +} + +bool JsonCollectionParser::visit(QmlJS::AST::PatternProperty *patternProperty) +{ + const QString propertyName = patternProperty->name->asString(); + if (!propertyOrderStack.isEmpty()) + propertyOrderStack.top().append(propertyName); + + keyStack.push(propertyName); + return true; +} + +void JsonCollectionParser::endVisit(QmlJS::AST::PatternProperty *patternProperty) +{ + const QString propertyName = patternProperty->name->asString(); + + if (auto curIndex = std::get_if(&keyStack.top())) { + if (*curIndex == propertyName) + keyStack.pop(); + } +} + +bool JsonCollectionParser::visit([[maybe_unused]] QmlJS::AST::PatternElementList *patternElementList) +{ + keyStack.push(-1); + return true; +} + +void JsonCollectionParser::endVisit([[maybe_unused]] QmlJS::AST::PatternElementList *patternElementList) +{ + if (auto curIndex = std::get_if(&keyStack.top())) + keyStack.pop(); +} + +bool JsonCollectionParser::visit([[maybe_unused]] QmlJS::AST::PatternElement *patternElement) +{ + if (auto curIndex = std::get_if(&keyStack.top())) + *curIndex += 1; + return true; +} + +void JsonCollectionParser::checkPropertyUpdates(QStack stack, + const QStringList &objectProperties) +{ + bool shouldUpdate = collectionPaths.contains(stack); + if (!shouldUpdate && !stack.isEmpty()) { + if (auto lastIndex = std::get_if(&stack.top())) { + stack.pop(); + shouldUpdate = collectionPaths.contains(stack); + } + } + if (!shouldUpdate) + return; + + QStringList propertyList = collectionPaths.value(stack); + QSet allKeys; + for (const QString &val : std::as_const(propertyList)) + allKeys.insert(val); + + std::optional prevVal; + for (const QString &val : objectProperties) { + if (!allKeys.contains(val)) { + if (prevVal.has_value()) { + const int idx = propertyList.indexOf(prevVal); + propertyList.insert(idx + 1, val); + } else { + propertyList.append(val); + } + allKeys.insert(val); + } + prevVal = val; + } + collectionPaths.insert(stack, propertyList); +} + +void JsonCollectionParser::throwRecursionDepthError() +{ + qWarning() << __FUNCTION__ << "Recursion Depth Error"; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionjsonparser.h b/src/plugins/qmldesigner/components/collectioneditor/collectionjsonparser.h new file mode 100644 index 00000000000..16069d3f4cc --- /dev/null +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionjsonparser.h @@ -0,0 +1,58 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +#include +#include + +QT_BEGIN_NAMESPACE +struct QJsonParseError; +QT_END_NAMESPACE + +using JsonKey = std::variant; // Key can be either int (index) or string (property name) + +using JsonKeyChain = QList; // A chain of keys leading to a specific json value + +namespace QmlDesigner { + +struct CollectionObject +{ + QString name; + QJsonArray array = {}; + JsonKeyChain keyChain = {}; + QStringList propertyOrder = {}; +}; + +class JsonCollectionParser : public QmlJS::AST::Visitor +{ +public: + static QList parseCollectionObjects(const QByteArray &json, + QJsonParseError *error = nullptr); + +private: + JsonCollectionParser(const QString &jsonContent, const QList &keyChains); + + bool visit(QmlJS::AST::ObjectPattern *objectPattern) override; + void endVisit(QmlJS::AST::ObjectPattern *objectPattern) override; + + bool visit(QmlJS::AST::PatternProperty *patternProperty) override; + void endVisit(QmlJS::AST::PatternProperty *patternProperty) override; + + bool visit(QmlJS::AST::PatternElementList *patternElementList) override; + void endVisit(QmlJS::AST::PatternElementList *patternElementList) override; + + bool visit(QmlJS::AST::PatternElement *patternElement) override; + + void checkPropertyUpdates(QStack stack, const QStringList &objectProperties); + + void throwRecursionDepthError() override; + + QStack keyStack; + QStack propertyOrderStack; + QMap collectionPaths; // Key chains, Priorities +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp index 7a90264145c..f6bc1829ee8 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp @@ -212,7 +212,6 @@ bool CollectionWidget::importFile(const QString &collectionName, FilePath fileInfo = FilePath::fromUserInput(url.isLocalFile() ? url.toLocalFile() : url.toString()); - CollectionDetails loadedCollection; QByteArray fileContent; auto loadUrlContent = [&]() -> bool { @@ -231,24 +230,40 @@ bool CollectionWidget::importFile(const QString &collectionName, return false; QJsonParseError parseError; - loadedCollection = CollectionDetails::fromImportedJson(fileContent, &parseError); + const QList loadedCollections = CollectionDetails::fromImportedJson( + fileContent, &parseError); if (parseError.error != QJsonParseError::NoError) { warn(tr("Json file Import error"), tr("Cannot parse json content\n%1").arg(parseError.errorString())); + return false; + } + if (loadedCollections.size() > 1) { + for (const CollectionDetails &loadedCollection : loadedCollections) { + m_view->addNewCollection(loadedCollection.reference().name, + loadedCollection.toLocalJson()); + } + return true; + } else if (loadedCollections.size() == 1) { + m_view->addNewCollection(collectionName, loadedCollections.first().toLocalJson()); + return true; + } else { + warn(tr("Can not add a model to the JSON file"), + tr("The imported model is empty or is not supported.")); } } else if (fileInfo.suffix() == "csv") { + CollectionDetails loadedCollection; if (!loadUrlContent()) return false; loadedCollection = CollectionDetails::fromImportedCsv(fileContent, firstRowIsHeader); + if (loadedCollection.columns()) { + m_view->addNewCollection(collectionName, loadedCollection.toLocalJson()); + return true; + } else { + warn(tr("Can not add a model to the JSON file"), + tr("The imported model is empty or is not supported.")); + } } - if (loadedCollection.columns()) { - m_view->addNewCollection(collectionName, loadedCollection.toLocalJson()); - return true; - } else { - warn(tr("Can not add a model to the JSON file"), - tr("The imported model is empty or is not supported.")); - } return false; }