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 <mahmoud.badri@qt.io>
This commit is contained in:
Ali Kianian
2024-05-03 18:03:11 +03:00
parent 542520e31c
commit 18febc9d76
7 changed files with 368 additions and 111 deletions

View File

@@ -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

View File

@@ -5,12 +5,11 @@
#include "collectiondatatypemodel.h"
#include "collectioneditorutils.h"
#include "collectionjsonparser.h"
#include <utils/span.h>
#include <qmljs/parser/qmljsast_p.h>
#include <qmljs/parser/qmljsastvisitor_p.h>
#include <qmljs/qmljsdocument.h>
#include <qqml.h>
#include <utils/algorithm.h>
#include <utils/qtcassert.h>
#include <QJsonArray>
#include <QJsonDocument>
@@ -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(
"<expression>"),
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<QString> 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> 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<CollectionObject> 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,

View File

@@ -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<CollectionDetails> fromImportedJson(const QByteArray &jsonContent,
QJsonParseError *error = nullptr);
static CollectionDetails fromLocalJson(const QJsonDocument &document,
const QString &collectionName,
CollectionParseError *error = nullptr);

View File

@@ -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<CollectionDetails> 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();
}

View File

@@ -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 <qmljs/parser/qmljsast_p.h>
#include <qmljs/qmljsdocument.h>
#include <utils/algorithm.h>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
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<CollectionObject> &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<CollectionObject> &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<CollectionObject> parseDocument(const QJsonDocument &document,
const QString &defaultName = "Model")
{
QList<CollectionObject> 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<CollectionObject> 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<CollectionObject> allCollections = parseDocument(document);
QList<JsonKeyChain> 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<JsonKeyChain> &keyChains)
{
for (const JsonKeyChain &chain : keyChains)
collectionPaths.insert(chain, {});
QmlJS::Document::MutablePtr newDoc = QmlJS::Document::create(Utils::FilePath::fromString(
"<expression>"),
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<QString>(&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<int>(&keyStack.top()))
keyStack.pop();
}
bool JsonCollectionParser::visit([[maybe_unused]] QmlJS::AST::PatternElement *patternElement)
{
if (auto curIndex = std::get_if<int>(&keyStack.top()))
*curIndex += 1;
return true;
}
void JsonCollectionParser::checkPropertyUpdates(QStack<JsonKey> stack,
const QStringList &objectProperties)
{
bool shouldUpdate = collectionPaths.contains(stack);
if (!shouldUpdate && !stack.isEmpty()) {
if (auto lastIndex = std::get_if<int>(&stack.top())) {
stack.pop();
shouldUpdate = collectionPaths.contains(stack);
}
}
if (!shouldUpdate)
return;
QStringList propertyList = collectionPaths.value(stack);
QSet<QString> allKeys;
for (const QString &val : std::as_const(propertyList))
allKeys.insert(val);
std::optional<QString> 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

View File

@@ -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 <qmljs/parser/qmljsastvisitor_p.h>
#include <QJsonArray>
#include <QStack>
QT_BEGIN_NAMESPACE
struct QJsonParseError;
QT_END_NAMESPACE
using JsonKey = std::variant<int, QString>; // Key can be either int (index) or string (property name)
using JsonKeyChain = QList<JsonKey>; // 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<CollectionObject> parseCollectionObjects(const QByteArray &json,
QJsonParseError *error = nullptr);
private:
JsonCollectionParser(const QString &jsonContent, const QList<JsonKeyChain> &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<JsonKey> stack, const QStringList &objectProperties);
void throwRecursionDepthError() override;
QStack<JsonKey> keyStack;
QStack<QStringList> propertyOrderStack;
QMap<JsonKeyChain, QStringList> collectionPaths; // Key chains, Priorities
};
} // namespace QmlDesigner

View File

@@ -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<CollectionDetails> 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;
}