forked from qt-creator/qt-creator
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:
@@ -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
|
||||
|
@@ -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,
|
||||
|
@@ -127,7 +127,7 @@ public:
|
||||
|
||||
static CollectionDetails fromImportedCsv(const QByteArray &document,
|
||||
const bool &firstRowIsHeader = true);
|
||||
static CollectionDetails fromImportedJson(const QByteArray &json,
|
||||
static QList<CollectionDetails> fromImportedJson(const QByteArray &jsonContent,
|
||||
QJsonParseError *error = nullptr);
|
||||
static CollectionDetails fromLocalJson(const QJsonDocument &document,
|
||||
const QString &collectionName,
|
||||
|
@@ -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(),
|
||||
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();
|
||||
}
|
||||
|
||||
|
@@ -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
|
@@ -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
|
@@ -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,17 +230,31 @@ 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;
|
||||
@@ -249,6 +262,8 @@ bool CollectionWidget::importFile(const QString &collectionName,
|
||||
warn(tr("Can not add a model to the JSON file"),
|
||||
tr("The imported model is empty or is not supported."));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user