From 992da71da09cbd2294dd28f12d423da3f4169778 Mon Sep 17 00:00:00 2001 From: Mahmoud Badri Date: Mon, 2 Sep 2024 17:18:50 +0300 Subject: [PATCH] QmlDesigner: Properly parse components in BundleHelper Instead of taking assets in the same folder as the component (which can cause issues, for example, if the component is in the project rather than an imported component), the component is properly parsed (recursively) and its dependencies are collected. There are some minor limitations due to the inherent complexity of qml. Also, some relevant fixes and tweaks. Fixes: QDS-13369 Fixes: QDS-13539 Fixes: QDS-13538 Change-Id: I6da9ce5cf14fb30c556fd521b6b452c3ab558f64 Reviewed-by: Miikka Heikkinen --- .../components/componentcore/bundlehelper.cpp | 185 ++++++++++++++---- .../components/componentcore/bundlehelper.h | 17 +- .../generatedcomponentutils.cpp | 21 +- 3 files changed, 162 insertions(+), 61 deletions(-) diff --git a/src/plugins/qmldesigner/components/componentcore/bundlehelper.cpp b/src/plugins/qmldesigner/components/componentcore/bundlehelper.cpp index 0c3d52c081c..702a3013757 100644 --- a/src/plugins/qmldesigner/components/componentcore/bundlehelper.cpp +++ b/src/plugins/qmldesigner/components/componentcore/bundlehelper.cpp @@ -33,6 +33,11 @@ namespace QmlDesigner { +Utils::FilePath AssetPath::absFilPath() const +{ + return basePath.pathAppended(relativePath); +} + BundleHelper::BundleHelper(AbstractView *view, QWidget *widget) : m_view(view) , m_widget(widget) @@ -51,7 +56,7 @@ void BundleHelper::createImporter() QObject::connect( m_importer.get(), &BundleImporter::importFinished, - m_widget, + m_view, [&](const QmlDesigner::TypeName &typeName, const QString &bundleId) { QTC_ASSERT(typeName.size(), return); if (isMaterialBundle(bundleId)) { @@ -75,7 +80,7 @@ void BundleHelper::createImporter() } }); #else - QObject::connect(m_importer.get(), &BundleImporter::importFinished, m_widget, + QObject::connect(m_importer.get(), &BundleImporter::importFinished, m_view, [&](const QmlDesigner::NodeMetaInfo &metaInfo, const QString &bundleId) { QTC_ASSERT(metaInfo.isValid(), return); if (isMaterialBundle(bundleId)) { @@ -185,12 +190,12 @@ void BundleHelper::importBundleToProject() void BundleHelper::exportBundle(const ModelNode &node, const QPixmap &iconPixmap) { if (node.isComponent()) - export3DComponent(node); + exportComponent(node); else - exportItem(node, iconPixmap); + exportNode(node, iconPixmap); } -void BundleHelper::export3DComponent(const ModelNode &node) +void BundleHelper::exportComponent(const ModelNode &node) { QString exportPath = getExportPath(node); if (exportPath.isEmpty()) @@ -204,26 +209,22 @@ void BundleHelper::export3DComponent(const ModelNode &node) m_zipWriter = std::make_unique(exportPath); - QString compBaseName = node.simplifiedTypeName(); - QString compFileName = compBaseName + ".qml"; - - auto compDir = Utils::FilePath::fromString(ModelUtils::componentFilePath(node)).parentDir(); + Utils::FilePath compFilePath = Utils::FilePath::fromString(ModelUtils::componentFilePath(node)); + Utils::FilePath compDir = compFilePath.parentDir(); + QString compBaseName = compFilePath.completeBaseName(); + QString compFileName = compFilePath.fileName(); QString iconPath = QLatin1String("icons/%1").arg(UniqueName::generateId(compBaseName) + ".png"); - const Utils::FilePaths sourceFiles = compDir.dirEntries({{}, QDir::Files, QDirIterator::Subdirectories}); - const QStringList ignoreList {"_importdata.json", "qmldir", compBaseName + ".hints"}; - QStringList filesList; // 3D component's assets (dependencies) + const QSet compDependencies = getComponentDependencies(compFilePath, compDir); - for (const Utils::FilePath &sourcePath : sourceFiles) { - Utils::FilePath relativePath = sourcePath.relativePathFrom(compDir); - if (ignoreList.contains(sourcePath.fileName()) || relativePath.startsWith("source scene")) - continue; + QStringList filesList; + for (const AssetPath &asset : compDependencies) { + Utils::FilePath assetAbsPath = asset.absFilPath(); + m_zipWriter->addFile(asset.relativePath, assetAbsPath.fileContents().value_or("")); - m_zipWriter->addFile(relativePath.toFSPathString(), sourcePath.fileContents().value_or("")); - - if (sourcePath.fileName() != compFileName) // skip component file (only collect dependencies) - filesList.append(relativePath.path()); + if (assetAbsPath.fileName() != compFileName) // skip component file (only collect dependencies) + filesList.append(asset.relativePath); } // add the item to the bundle json @@ -248,12 +249,12 @@ void BundleHelper::export3DComponent(const ModelNode &node) // add icon m_iconSavePath = targetPath.pathAppended(iconPath); m_iconSavePath.parentDir().ensureWritableDir(); - getImageFromCache(compDir.pathAppended(compFileName).path(), [&](const QImage &image) { + getImageFromCache(compFilePath.path(), [&](const QImage &image) { addIconAndCloseZip(image); }); } -void BundleHelper::exportItem(const ModelNode &node, const QPixmap &iconPixmap) +void BundleHelper::exportNode(const ModelNode &node, const QPixmap &iconPixmap) { QString exportPath = getExportPath(node); if (exportPath.isEmpty()) @@ -309,9 +310,15 @@ void BundleHelper::exportItem(const ModelNode &node, const QPixmap &iconPixmap) Utils::FilePath jsonFilePath = targetPath.pathAppended(Constants::BUNDLE_JSON_FILENAME); m_zipWriter->addFile(jsonFilePath.fileName(), QJsonDocument(jsonObj).toJson()); - // add item's dependency assets to the bundle zip - for (const AssetPath &assetPath : depAssetsList) - m_zipWriter->addFile(assetPath.relativePath, assetPath.absFilPath().fileContents().value_or("")); + // add item's dependency assets to the bundle zip and target path (for icon generation) + for (const AssetPath &assetPath : depAssetsList) { + auto assetContent = assetPath.absFilPath().fileContents().value_or(""); + m_zipWriter->addFile(assetPath.relativePath, assetContent); + + Utils::FilePath assetTargetPath = targetPath.pathAppended(assetPath.relativePath); + assetTargetPath.parentDir().ensureWritableDir(); + assetTargetPath.writeFileContents(assetContent); + } // add icon QPixmap iconPixmapToSave; @@ -359,8 +366,8 @@ QPair> BundleHelper::modelNodeToQmlString(const ModelNo const QList excludedProps = {"x", "y", "z", "eulerRotation.x", "eulerRotation.y", "eulerRotation.z", "scale.x", "scale.y", "scale.z", "pivot.x", "pivot.y", "pivot.z"}; - const QList matProps = node.properties(); - for (const AbstractProperty &p : matProps) { + const QList nodeProps = node.properties(); + for (const AbstractProperty &p : nodeProps) { if (excludedProps.contains(p.name())) continue; @@ -372,13 +379,11 @@ QPair> BundleHelper::modelNodeToQmlString(const ModelNo // dynamic property with no value assigned } else if (strcmp(pValue.typeName(), "QString") == 0 || strcmp(pValue.typeName(), "QColor") == 0) { val = QLatin1String("\"%1\"").arg(pValue.toString()); - } else if (strcmp(pValue.typeName(), "QUrl") == 0) { + } else if (std::string_view{pValue.typeName()} == "QUrl") { QString pValueStr = pValue.toString(); val = QLatin1String("\"%1\"").arg(pValueStr); - if (!pValueStr.startsWith("#")) { - assets.insert({DocumentManager::currentResourcePath().toFSPathString(), - pValue.toString()}); - } + if (!pValueStr.startsWith("#")) + assets.insert({DocumentManager::currentFilePath().parentDir(), pValue.toString()}); } else if (strcmp(pValue.typeName(), "QmlDesigner::Enumeration") == 0) { val = pValue.value().toString(); } else { @@ -391,19 +396,25 @@ QPair> BundleHelper::modelNodeToQmlString(const ModelNo qml += indent + p.name() + ": " + val + "\n"; } } else if (p.isBindingProperty()) { - ModelNode depNode = m_view->modelNodeForId(p.toBindingProperty().expression()); - QTC_ASSERT(depNode.isValid(), continue); + const QString pExp = p.toBindingProperty().expression(); + const QStringList depNodesIds = ModelUtils::expressionToList(pExp); + QTC_ASSERT(!depNodesIds.isEmpty(), continue); if (p.isDynamic()) - qml += indent + "property " + p.dynamicTypeName() + " " + p.name() + ": " + depNode.id() + "\n"; + qml += indent + "property " + p.dynamicTypeName() + " " + p.name() + ": " + pExp + "\n"; else - qml += indent + p.name() + ": " + depNode.id() + "\n"; + qml += indent + p.name() + ": " + pExp + "\n"; - if (depNode && !depListIds.contains(depNode.id())) { - depListIds.append(depNode.id()); - auto [depQml, depAssets] = modelNodeToQmlString(depNode, depth + 1); - qml += "\n" + depQml + "\n"; - assets.unite(depAssets); + for (const QString &id : depNodesIds) { + ModelNode depNode = m_view->modelNodeForId(id); + QTC_ASSERT(depNode.isValid(), continue); + + if (depNode && !depListIds.contains(depNode.id())) { + depListIds.append(depNode.id()); + auto [depQml, depAssets] = modelNodeToQmlString(depNode, depth + 1); + qml += "\n" + depQml + "\n"; + assets.unite(depAssets); + } } } } @@ -430,9 +441,10 @@ QPair> BundleHelper::modelNodeToQmlString(const ModelNo if (depth > 0) { // add component file to the dependency assets Utils::FilePath compFilePath = componentPath(node.metaInfo()); - assets.insert({compFilePath.parentDir().path(), compFilePath.fileName()}); + assets.insert({compFilePath.parentDir(), compFilePath.fileName()}); } + // TODO: use getComponentDependencies() and remove getBundleComponentDependencies() if (isBundle) assets.unite(getBundleComponentDependencies(node)); } @@ -469,7 +481,7 @@ QSet BundleHelper::getBundleComponentDependencies(const ModelNode &no for (const QString &asset : bundleAssets) { if (rootObj.value(asset).toArray().contains(compFileName)) - depList.insert({compPath.toFSPathString(), asset}); + depList.insert({compPath, asset}); } return depList; @@ -575,4 +587,91 @@ bool BundleHelper::isItemBundle(const QString &bundleId) const || bundleId == compUtils.user3DBundleId(); } +namespace { + +// library imported Components won't be detected. TODO: find a feasible solution for detecting them +// and either add them as dependencies or warn the user +Utils::FilePath getComponentFilePath(const QString &nodeType, const Utils::FilePath &compDir) +{ + Utils::FilePath compFilePath = compDir.pathAppended(QLatin1String("%1.qml").arg(nodeType)); + if (compFilePath.exists()) + return compFilePath; + + compFilePath = compDir.pathAppended(QLatin1String("%1.ui.qml").arg(nodeType)); + if (compFilePath.exists()) + return compFilePath; + + const Utils::FilePaths subDirs = compDir.dirEntries(QDir::Dirs | QDir::NoDotAndDotDot); + for (const Utils::FilePath &dir : subDirs) { + compFilePath = getComponentFilePath(nodeType, dir); + if (compFilePath.exists()) + return compFilePath; + } + + return {}; +} + +} // namespace + +QSet BundleHelper::getComponentDependencies(const Utils::FilePath &filePath, + const Utils::FilePath &mainCompDir) +{ + QSet depList; + + depList.insert({mainCompDir, filePath.relativePathFrom(mainCompDir).toFSPathString()}); + + ModelPointer model = Model::create("Item"); + Utils::FileReader reader; + QTC_ASSERT(reader.fetch(filePath), return {}); + + QPlainTextEdit textEdit; + textEdit.setPlainText(QString::fromUtf8(reader.data())); + NotIndentingTextEditModifier modifier(&textEdit); + modifier.setParent(model.get()); + RewriterView rewriterView(m_view->externalDependencies(), RewriterView::Validate); + rewriterView.setCheckSemanticErrors(false); + rewriterView.setTextModifier(&modifier); + model->attachView(&rewriterView); + rewriterView.restoreAuxiliaryData(); + ModelNode rootNode = rewriterView.rootModelNode(); + QTC_ASSERT(rootNode.isValid(), return {}); + + std::function parseNode; + parseNode = [&](const ModelNode &node) { + // workaround node.isComponent() as it is not working here + QString nodeType = QString::fromLatin1(node.type()); + if (!nodeType.startsWith("QtQuick")) { + Utils::FilePath compFilPath = getComponentFilePath(nodeType, mainCompDir); + if (!compFilPath.isEmpty()) { + depList.unite(getComponentDependencies(compFilPath, mainCompDir)); + return; + } + } + + const QList nodeProps = node.properties(); + for (const AbstractProperty &p : nodeProps) { + if (p.isVariantProperty()) { + QVariant pValue = p.toVariantProperty().value(); + if (std::string_view{pValue.typeName()} == "QUrl") { + QString pValueStr = pValue.toString(); + if (!pValueStr.isEmpty() && !pValueStr.startsWith("#")) { + Utils::FilePath assetPath = filePath.parentDir().resolvePath(pValueStr); + Utils::FilePath assetPathRelative = assetPath.relativePathFrom(mainCompDir); + depList.insert({mainCompDir, assetPathRelative.toFSPathString()}); + } + } + } + } + + // parse child nodes + const QList childNodes = node.directSubModelNodes(); + for (const ModelNode &childNode : childNodes) + parseNode(childNode); + }; + + parseNode(rootNode); + + return depList; +} + } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/componentcore/bundlehelper.h b/src/plugins/qmldesigner/components/componentcore/bundlehelper.h index de70dfc60f1..270109cd0b4 100644 --- a/src/plugins/qmldesigner/components/componentcore/bundlehelper.h +++ b/src/plugins/qmldesigner/components/componentcore/bundlehelper.h @@ -22,21 +22,20 @@ class BundleImporter; class ModelNode; class NodeMetaInfo; -struct AssetPath +class AssetPath { - QString basePath; +public: + Utils::FilePath basePath; QString relativePath; - Utils::FilePath absFilPath() const - { - return Utils::FilePath::fromString(basePath).pathAppended(relativePath); - } + Utils::FilePath absFilPath() const; bool operator==(const AssetPath &other) const { return basePath == other.basePath && relativePath == other.relativePath; } +private: friend size_t qHash(const AssetPath &asset) { return ::qHash(asset.relativePath); @@ -65,8 +64,10 @@ private: void addIconAndCloseZip(const auto &image); Utils::FilePath componentPath(const NodeMetaInfo &metaInfo) const; QSet getBundleComponentDependencies(const ModelNode &node) const; - void export3DComponent(const ModelNode &node); - void exportItem(const ModelNode &node, const QPixmap &iconPixmap = QPixmap()); + QSet getComponentDependencies(const Utils::FilePath &filePath, + const Utils::FilePath &mainCompDir); + void exportComponent(const ModelNode &node); + void exportNode(const ModelNode &node, const QPixmap &iconPixmap = QPixmap()); QPointer m_view; QPointer m_widget; diff --git a/src/plugins/qmldesigner/libs/designercore/designercoreutils/generatedcomponentutils.cpp b/src/plugins/qmldesigner/libs/designercore/designercoreutils/generatedcomponentutils.cpp index bd628ea9bfe..b1b7161c31c 100644 --- a/src/plugins/qmldesigner/libs/designercore/designercoreutils/generatedcomponentutils.cpp +++ b/src/plugins/qmldesigner/libs/designercore/designercoreutils/generatedcomponentutils.cpp @@ -8,6 +8,7 @@ namespace QmlDesigner { constexpr QLatin1String componentBundlesMaterialBundleType{"Materials"}; +constexpr QLatin1String componentBundlesEffectBundleType{"Effects"}; constexpr QLatin1String componentBundlesType{"Bundles"}; constexpr QLatin1String componentBundlesUser3DBundleType{"User3D"}; constexpr QLatin1String componentBundlesUserEffectsBundleType{"UserEffects"}; @@ -16,7 +17,8 @@ constexpr QLatin1String composedEffectType{"Effects"}; constexpr QLatin1String generatedComponentsFolder{"Generated"}; constexpr QLatin1String oldAssetImportFolder{"asset_imports"}; constexpr QLatin1String oldComponentBundleType{"ComponentBundles"}; -constexpr QLatin1String oldComponentsBundlesMaterialBundleType{"MaterialBundle"}; +constexpr QLatin1String oldComponentBundlesMaterialBundleType{"MaterialBundle"}; +constexpr QLatin1String oldComponentBundlesEffectBundleType{"EffectBundle"}; constexpr QLatin1String oldEffectFolder{"Effects"}; namespace Constants {} // namespace Constants @@ -128,9 +130,9 @@ Utils::FilePath GeneratedComponentUtils::materialBundlePath() const return {}; if (basePath.endsWith(Constants::quick3DComponentsFolder)) - return basePath.resolvePath(oldComponentsBundlesMaterialBundleType); + return basePath.resolvePath(oldComponentBundlesMaterialBundleType); - return basePath.resolvePath(QLatin1String(componentBundlesMaterialBundleType)); + return basePath.resolvePath(componentBundlesMaterialBundleType); } Utils::FilePath GeneratedComponentUtils::effectBundlePath() const @@ -141,9 +143,9 @@ Utils::FilePath GeneratedComponentUtils::effectBundlePath() const return {}; if (basePath.endsWith(Constants::quick3DComponentsFolder)) - return basePath.resolvePath(componentBundlesMaterialBundleType); + return basePath.resolvePath(oldComponentBundlesEffectBundleType); - return basePath.resolvePath(componentBundlesMaterialBundleType); + return basePath.resolvePath(componentBundlesEffectBundleType); } Utils::FilePath GeneratedComponentUtils::userBundlePath(const QString &bundleId) const @@ -218,7 +220,6 @@ bool GeneratedComponentUtils::isGeneratedPath(const QString &path) const return path.startsWith(generatedComponentsPath().toFSPathString()); } - QString GeneratedComponentUtils::generatedComponentTypePrefix() const { Utils::FilePath basePath = generatedComponentsPath(); @@ -270,20 +271,20 @@ QString GeneratedComponentUtils::materialsBundleId() const bool isNewImportDir = generatedComponentTypePrefix().endsWith(generatedComponentsFolder); return isNewImportDir ? componentBundlesMaterialBundleType - : oldComponentsBundlesMaterialBundleType; + : oldComponentBundlesMaterialBundleType; } QString GeneratedComponentUtils::effectsBundleId() const { bool isNewImportDir = generatedComponentTypePrefix().endsWith(generatedComponentsFolder); - return QLatin1String(isNewImportDir ? componentBundlesMaterialBundleType - : componentBundlesMaterialBundleType); + return isNewImportDir ? componentBundlesEffectBundleType + : oldComponentBundlesEffectBundleType; } QString GeneratedComponentUtils::userMaterialsBundleId() const { - return componentBundlesMaterialBundleType; + return componentBundlesUserMaterialBundleType; } QString GeneratedComponentUtils::userEffectsBundleId() const