// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "collectionview.h" #include "collectionmodel.h" #include "collectionwidget.h" #include "designmodecontext.h" #include "nodelistproperty.h" #include "nodemetainfo.h" #include "qmldesignerconstants.h" #include "qmldesignerplugin.h" #include "variantproperty.h" #include #include #include #include #include #include namespace { using Data = std::variant; using DataRecord = QMap; struct DataHeader { enum class Type { Unknown, Bool, Numeric, String, DateTime }; Type type; QString name; }; using DataHeaderMap = QMap; // Lowercase Name - Header Data inline constexpr QStringView BoolDataType{u"Bool"}; inline constexpr QStringView NumberDataType{u"Number"}; inline constexpr QStringView StringDataType{u"String"}; inline constexpr QStringView DateTimeDataType{u"Date/Time"}; QString removeSpaces(QString string) { string.replace(" ", "_"); string.replace("-", "_"); return string; } DataHeader getDataType(const QString &type, const QString &name) { static const QMap typeMap = { {BoolDataType.toString().toLower(), DataHeader::Type::Bool}, {NumberDataType.toString().toLower(), DataHeader::Type::Numeric}, {StringDataType.toString().toLower(), DataHeader::Type::String}, {DateTimeDataType.toString().toLower(), DataHeader::Type::DateTime}}; if (name.isEmpty()) return {}; if (type.isEmpty()) return {DataHeader::Type::String, removeSpaces(name)}; return {typeMap.value(type.toLower(), DataHeader::Type::Unknown), removeSpaces(name)}; } struct JsonDocumentError : public std::exception { enum Error { InvalidDocumentType, InvalidCollectionName, InvalidCollectionId, InvalidCollectionObject, InvalidArrayPosition, InvalidLiteralType, InvalidCollectionHeader, IsNotJsonArray, CollectionHeaderNotFound }; const Error error; JsonDocumentError(Error error) : error(error) {} const char *what() const noexcept override { switch (error) { case InvalidDocumentType: return "Current JSON document contains errors."; case InvalidCollectionName: return "Invalid collection name."; case InvalidCollectionId: return "Invalid collection Id."; case InvalidCollectionObject: return "A collection should be a json object."; case InvalidArrayPosition: return "Arrays are not supported inside the collection."; case InvalidLiteralType: return "Invalid literal type for collection items"; case InvalidCollectionHeader: return "Invalid Collection Header"; case IsNotJsonArray: return "Json file should be an array"; case CollectionHeaderNotFound: return "Collection Header not found"; default: return "Unknown Json Error"; } } }; struct CsvDocumentError : public std::exception { enum Error { HeaderNotFound, DataNotFound, }; const Error error; CsvDocumentError(Error error) : error(error) {} const char *what() const noexcept override { switch (error) { case HeaderNotFound: return "CSV Header not found"; case DataNotFound: return "CSV data not found"; default: return "Unknown CSV Error"; } } }; Data getLiteralDataValue(const QVariant &value, const DataHeader &header, bool *typeWarningCheck = nullptr) { if (header.type == DataHeader::Type::Bool) return value.toBool(); if (header.type == DataHeader::Type::Numeric) return value.toDouble(); if (header.type == DataHeader::Type::String) return value.toString(); if (header.type == DataHeader::Type::DateTime) { QDateTime dateTimeStr = QDateTime::fromString(value.toString()); if (dateTimeStr.isValid()) return dateTimeStr; } if (typeWarningCheck) *typeWarningCheck = true; return value.toString(); } void loadJsonHeaders(QList &collectionHeaders, DataHeaderMap &headerDataMap, const QJsonObject &collectionJsonObject) { const QJsonArray collectionHeader = collectionJsonObject.value("headers").toArray(); for (const QJsonValue &headerValue : collectionHeader) { const QJsonObject headerJsonObject = headerValue.toObject(); DataHeader dataHeader = getDataType(headerJsonObject.value("type").toString(), headerJsonObject.value("name").toString()); if (dataHeader.type == DataHeader::Type::Unknown) throw JsonDocumentError{JsonDocumentError::InvalidCollectionHeader}; collectionHeaders.append(dataHeader); headerDataMap.insert(dataHeader.name.toLower(), dataHeader); } if (collectionHeaders.isEmpty()) throw JsonDocumentError{JsonDocumentError::CollectionHeaderNotFound}; } void loadJsonRecords(QList &collectionItems, DataHeaderMap &headerDataMap, const QJsonObject &collectionJsonObject) { auto addItemFromValue = [&headerDataMap, &collectionItems](const QJsonValue &jsonValue) { const QVariantMap dataMap = jsonValue.toObject().toVariantMap(); DataRecord recordData; for (const auto &dataPair : dataMap.asKeyValueRange()) { const DataHeader correspondingHeader = headerDataMap.value(removeSpaces( dataPair.first.toLower()), {}); const QString &fieldName = correspondingHeader.name; if (fieldName.size()) recordData.insert(fieldName, getLiteralDataValue(dataPair.second, correspondingHeader)); } if (!recordData.isEmpty()) collectionItems.append(recordData); }; const QJsonValue jsonDataValue = collectionJsonObject.value("data"); if (jsonDataValue.isObject()) { addItemFromValue(jsonDataValue); } else if (jsonDataValue.isArray()) { const QJsonArray jsonDataArray = jsonDataValue.toArray(); for (const QJsonValue &jsonItem : jsonDataArray) { if (jsonItem.isObject()) addItemFromValue(jsonItem); } } } inline bool isCollectionLib(const QmlDesigner::ModelNode &node) { return node.parentProperty().parentModelNode().isRootNode() && node.id() == QmlDesigner::Constants::COLLECTION_LIB_ID; } inline bool isListModel(const QmlDesigner::ModelNode &node) { return node.metaInfo().isQtQuickListModel(); } inline bool isListElement(const QmlDesigner::ModelNode &node) { return node.metaInfo().isQtQuickListElement(); } inline bool isCollection(const QmlDesigner::ModelNode &node) { return isCollectionLib(node.parentProperty().parentModelNode()) && isListModel(node); } inline bool isCollectionElement(const QmlDesigner::ModelNode &node) { return isListElement(node) && isCollection(node.parentProperty().parentModelNode()); } } // namespace namespace QmlDesigner { struct Collection { QString name; QString id; QList headers; QList items; }; CollectionView::CollectionView(ExternalDependenciesInterface &externalDependencies) : AbstractView(externalDependencies) {} bool CollectionView::loadJson(const QByteArray &data) { try { QJsonParseError parseError; QJsonDocument document = QJsonDocument::fromJson(data, &parseError); if (parseError.error != QJsonParseError::NoError) throw JsonDocumentError{JsonDocumentError::InvalidDocumentType}; QList collections; if (document.isArray()) { const QJsonArray collectionsJsonArray = document.array(); for (const QJsonValue &collectionJson : collectionsJsonArray) { Collection collection; if (!collectionJson.isObject()) throw JsonDocumentError{JsonDocumentError::InvalidCollectionObject}; QJsonObject collectionJsonObject = collectionJson.toObject(); const QString &collectionName = collectionJsonObject.value(u"name").toString(); if (!collectionName.size()) throw JsonDocumentError{JsonDocumentError::InvalidCollectionName}; const QString &collectionId = collectionJsonObject.value(u"id").toString(); if (!collectionId.size()) throw JsonDocumentError{JsonDocumentError::InvalidCollectionId}; DataHeaderMap headerDataMap; loadJsonHeaders(collection.headers, headerDataMap, collectionJsonObject); loadJsonRecords(collection.items, headerDataMap, collectionJsonObject); if (collection.items.count()) collections.append(collection); } } else { throw JsonDocumentError{JsonDocumentError::InvalidDocumentType}; } addLoadedModel(collections); } catch (const std::exception &error) { m_widget->warn("Json Import Problem", QString::fromLatin1(error.what())); return false; } return true; } bool CollectionView::loadCsv(const QString &collectionName, const QByteArray &data) { QTextStream stream(data); Collection collection; collection.name = collectionName; try { if (!stream.atEnd()) { const QStringList recordData = stream.readLine().split(','); for (const QString &name : recordData) collection.headers.append(getDataType({}, name)); } if (collection.headers.isEmpty()) throw CsvDocumentError{CsvDocumentError::HeaderNotFound}; while (!stream.atEnd()) { const QStringList recordDataList = stream.readLine().split(','); DataRecord recordData; int column = -1; for (const QString &cellData : recordDataList) { if (++column == collection.headers.size()) break; recordData.insert(collection.headers.at(column).name, cellData); } if (recordData.count()) collection.items.append(recordData); } if (collection.items.isEmpty()) throw CsvDocumentError{CsvDocumentError::DataNotFound}; addLoadedModel({collection}); } catch (const std::exception &error) { m_widget->warn("Json Import Problem", QString::fromLatin1(error.what())); return false; } return true; } bool CollectionView::hasWidget() const { return true; } QmlDesigner::WidgetInfo CollectionView::widgetInfo() { if (m_widget.isNull()) { m_widget = new CollectionWidget(this); auto collectionEditorContext = new Internal::CollectionEditorContext(m_widget.data()); Core::ICore::addContextObject(collectionEditorContext); } return createWidgetInfo(m_widget.data(), "CollectionEditor", WidgetInfo::LeftPane, 0, tr("Collection Editor"), tr("Collection Editor view")); } void CollectionView::modelAttached(Model *model) { AbstractView::modelAttached(model); refreshModel(); } void CollectionView::nodeReparented(const ModelNode &node, const NodeAbstractProperty &newPropertyParent, const NodeAbstractProperty &oldPropertyParent, [[maybe_unused]] PropertyChangeFlags propertyChange) { if (!isListModel(node)) return; ModelNode newParentNode = newPropertyParent.parentModelNode(); ModelNode oldParentNode = oldPropertyParent.parentModelNode(); bool added = isCollectionLib(newParentNode); bool removed = isCollectionLib(oldParentNode); if (!added && !removed) return; refreshModel(); if (isCollection(node)) m_widget->collectionModel()->selectCollection(node); } void CollectionView::nodeAboutToBeRemoved(const ModelNode &removedNode) { // removing the collections lib node if (isCollectionLib(removedNode)) { m_widget->collectionModel()->setCollections({}); return; } if (isCollection(removedNode)) m_widget->collectionModel()->removeCollection(removedNode); } void CollectionView::nodeRemoved([[maybe_unused]] const ModelNode &removedNode, const NodeAbstractProperty &parentProperty, [[maybe_unused]] PropertyChangeFlags propertyChange) { if (parentProperty.parentModelNode().id() != Constants::COLLECTION_LIB_ID) return; m_widget->collectionModel()->updateSelectedCollection(true); } void CollectionView::variantPropertiesChanged(const QList &propertyList, [[maybe_unused]] PropertyChangeFlags propertyChange) { for (const VariantProperty &property : propertyList) { ModelNode node(property.parentModelNode()); if (isCollection(node)) { if (property.name() == "objectName") m_widget->collectionModel()->updateNodeName(node); else if (property.name() == "id") m_widget->collectionModel()->updateNodeId(node); } } } void CollectionView::selectedNodesChanged(const QList &selectedNodeList, [[maybe_unused]] const QList &lastSelectedNodeList) { QList selectedCollections = Utils::filtered(selectedNodeList, &isCollection); // More than one collections are selected. So ignore them if (selectedCollections.size() > 1) return; if (selectedCollections.size() == 1) { // If exactly one collection is selected m_widget->collectionModel()->selectCollection(selectedCollections.first()); return; } // If no collection is selected, check the elements QList selectedElements = Utils::filtered(selectedNodeList, &isCollectionElement); if (selectedElements.size()) { const ModelNode parentElement = selectedElements.first().parentProperty().parentModelNode(); bool haveSameParent = Utils::allOf(selectedElements, [&parentElement](const ModelNode &element) { return element.parentProperty().parentModelNode() == parentElement; }); if (haveSameParent) m_widget->collectionModel()->selectCollection(parentElement); } } void CollectionView::addNewCollection(const QString &name) { executeInTransaction(__FUNCTION__, [&] { ensureCollectionLibraryNode(); ModelNode collectionLib = collectionLibraryNode(); if (!collectionLib.isValid()) return; NodeMetaInfo listModelMetaInfo = model()->qtQmlModelsListModelMetaInfo(); ModelNode collectionNode = createModelNode(listModelMetaInfo.typeName(), listModelMetaInfo.majorVersion(), listModelMetaInfo.minorVersion()); QString collectionName = name.isEmpty() ? "Collection" : name; renameCollection(collectionNode, collectionName); QmlDesignerPlugin::emitUsageStatistics(Constants::EVENT_PROPERTY_ADDED); auto headersProperty = collectionNode.variantProperty("headers"); headersProperty.setDynamicTypeNameAndValue("string", {}); collectionLib.defaultNodeListProperty().reparentHere(collectionNode); }); } void CollectionView::refreshModel() { if (!model()) return; ModelNode collectionLib = modelNodeForId(Constants::COLLECTION_LIB_ID); ModelNodes collections; if (collectionLib.isValid()) { const QList collectionLibNodes = collectionLib.directSubModelNodes(); for (const ModelNode &node : collectionLibNodes) { if (isCollection(node)) collections.append(node); } } m_widget->collectionModel()->setCollections(collections); } ModelNode CollectionView::getNewCollectionNode(const Collection &collection) { QTC_ASSERT(model(), return {}); ModelNode collectionNode; executeInTransaction(__FUNCTION__, [&] { NodeMetaInfo listModelMetaInfo = model()->qtQmlModelsListModelMetaInfo(); collectionNode = createModelNode(listModelMetaInfo.typeName(), listModelMetaInfo.majorVersion(), listModelMetaInfo.minorVersion()); QString collectionName = collection.name.isEmpty() ? "Collection" : collection.name; renameCollection(collectionNode, collectionName); QStringList headers; for (const DataHeader &header : collection.headers) headers.append(header.name); QmlDesignerPlugin::emitUsageStatistics(Constants::EVENT_PROPERTY_ADDED); auto headersProperty = collectionNode.variantProperty("headers"); headersProperty.setDynamicTypeNameAndValue("string", headers.join(",")); NodeMetaInfo listElementMetaInfo = model()->qtQmlModelsListElementMetaInfo(); for (const DataRecord &item : collection.items) { ModelNode elementNode = createModelNode(listElementMetaInfo.typeName(), listElementMetaInfo.majorVersion(), listElementMetaInfo.minorVersion()); for (const auto &headerMapElement : item.asKeyValueRange()) { auto property = elementNode.variantProperty(headerMapElement.first.toLatin1()); QVariant value = std::visit([](const auto &data) -> QVariant { return QVariant::fromValue(data); }, headerMapElement.second); property.setValue(value); } collectionNode.defaultNodeListProperty().reparentHere(elementNode); } }); return collectionNode; } void CollectionView::addLoadedModel(const QList &newCollection) { executeInTransaction(__FUNCTION__, [&] { ensureCollectionLibraryNode(); ModelNode collectionLib = collectionLibraryNode(); if (!collectionLib.isValid()) return; for (const Collection &collection : newCollection) { ModelNode collectionNode = getNewCollectionNode(collection); collectionLib.defaultNodeListProperty().reparentHere(collectionNode); } }); } void CollectionView::renameCollection(ModelNode &collection, const QString &newName) { QTC_ASSERT(collection.isValid(), return); QVariant objName = collection.variantProperty("objectName").value(); if (objName.isValid() && objName.toString() == newName) return; executeInTransaction(__FUNCTION__, [&] { collection.setIdWithRefactoring(model()->generateIdFromName(newName, "collection")); VariantProperty objNameProp = collection.variantProperty("objectName"); objNameProp.setValue(newName); }); } void CollectionView::ensureCollectionLibraryNode() { ModelNode collectionLib = modelNodeForId(Constants::COLLECTION_LIB_ID); if (collectionLib.isValid() || (!rootModelNode().metaInfo().isQtQuick3DNode() && !rootModelNode().metaInfo().isQtQuickItem())) { return; } executeInTransaction(__FUNCTION__, [&] { // Create collection library node #ifdef QDS_USE_PROJECTSTORAGE TypeName nodeTypeName = rootModelNode().metaInfo().isQtQuick3DNode() ? "Node" : "Item"; matLib = createModelNode(nodeTypeName, -1, -1); #else auto nodeType = rootModelNode().metaInfo().isQtQuick3DNode() ? model()->qtQuick3DNodeMetaInfo() : model()->qtQuickItemMetaInfo(); collectionLib = createModelNode(nodeType.typeName(), nodeType.majorVersion(), nodeType.minorVersion()); #endif collectionLib.setIdWithoutRefactoring(Constants::COLLECTION_LIB_ID); rootModelNode().defaultNodeListProperty().reparentHere(collectionLib); }); } ModelNode CollectionView::collectionLibraryNode() { return modelNodeForId(Constants::COLLECTION_LIB_ID); } } // namespace QmlDesigner