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 <miikka.heikkinen@qt.io>
This commit is contained in:
Mahmoud Badri
2024-09-02 17:18:50 +03:00
parent e8d0c613d3
commit 992da71da0
3 changed files with 162 additions and 61 deletions

View File

@@ -33,6 +33,11 @@
namespace QmlDesigner { namespace QmlDesigner {
Utils::FilePath AssetPath::absFilPath() const
{
return basePath.pathAppended(relativePath);
}
BundleHelper::BundleHelper(AbstractView *view, QWidget *widget) BundleHelper::BundleHelper(AbstractView *view, QWidget *widget)
: m_view(view) : m_view(view)
, m_widget(widget) , m_widget(widget)
@@ -51,7 +56,7 @@ void BundleHelper::createImporter()
QObject::connect( QObject::connect(
m_importer.get(), m_importer.get(),
&BundleImporter::importFinished, &BundleImporter::importFinished,
m_widget, m_view,
[&](const QmlDesigner::TypeName &typeName, const QString &bundleId) { [&](const QmlDesigner::TypeName &typeName, const QString &bundleId) {
QTC_ASSERT(typeName.size(), return); QTC_ASSERT(typeName.size(), return);
if (isMaterialBundle(bundleId)) { if (isMaterialBundle(bundleId)) {
@@ -75,7 +80,7 @@ void BundleHelper::createImporter()
} }
}); });
#else #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) { [&](const QmlDesigner::NodeMetaInfo &metaInfo, const QString &bundleId) {
QTC_ASSERT(metaInfo.isValid(), return); QTC_ASSERT(metaInfo.isValid(), return);
if (isMaterialBundle(bundleId)) { if (isMaterialBundle(bundleId)) {
@@ -185,12 +190,12 @@ void BundleHelper::importBundleToProject()
void BundleHelper::exportBundle(const ModelNode &node, const QPixmap &iconPixmap) void BundleHelper::exportBundle(const ModelNode &node, const QPixmap &iconPixmap)
{ {
if (node.isComponent()) if (node.isComponent())
export3DComponent(node); exportComponent(node);
else else
exportItem(node, iconPixmap); exportNode(node, iconPixmap);
} }
void BundleHelper::export3DComponent(const ModelNode &node) void BundleHelper::exportComponent(const ModelNode &node)
{ {
QString exportPath = getExportPath(node); QString exportPath = getExportPath(node);
if (exportPath.isEmpty()) if (exportPath.isEmpty())
@@ -204,26 +209,22 @@ void BundleHelper::export3DComponent(const ModelNode &node)
m_zipWriter = std::make_unique<ZipWriter>(exportPath); m_zipWriter = std::make_unique<ZipWriter>(exportPath);
QString compBaseName = node.simplifiedTypeName(); Utils::FilePath compFilePath = Utils::FilePath::fromString(ModelUtils::componentFilePath(node));
QString compFileName = compBaseName + ".qml"; Utils::FilePath compDir = compFilePath.parentDir();
QString compBaseName = compFilePath.completeBaseName();
auto compDir = Utils::FilePath::fromString(ModelUtils::componentFilePath(node)).parentDir(); QString compFileName = compFilePath.fileName();
QString iconPath = QLatin1String("icons/%1").arg(UniqueName::generateId(compBaseName) + ".png"); QString iconPath = QLatin1String("icons/%1").arg(UniqueName::generateId(compBaseName) + ".png");
const Utils::FilePaths sourceFiles = compDir.dirEntries({{}, QDir::Files, QDirIterator::Subdirectories}); const QSet<AssetPath> compDependencies = getComponentDependencies(compFilePath, compDir);
const QStringList ignoreList {"_importdata.json", "qmldir", compBaseName + ".hints"};
QStringList filesList; // 3D component's assets (dependencies)
for (const Utils::FilePath &sourcePath : sourceFiles) { QStringList filesList;
Utils::FilePath relativePath = sourcePath.relativePathFrom(compDir); for (const AssetPath &asset : compDependencies) {
if (ignoreList.contains(sourcePath.fileName()) || relativePath.startsWith("source scene")) Utils::FilePath assetAbsPath = asset.absFilPath();
continue; m_zipWriter->addFile(asset.relativePath, assetAbsPath.fileContents().value_or(""));
m_zipWriter->addFile(relativePath.toFSPathString(), sourcePath.fileContents().value_or("")); if (assetAbsPath.fileName() != compFileName) // skip component file (only collect dependencies)
filesList.append(asset.relativePath);
if (sourcePath.fileName() != compFileName) // skip component file (only collect dependencies)
filesList.append(relativePath.path());
} }
// add the item to the bundle json // add the item to the bundle json
@@ -248,12 +249,12 @@ void BundleHelper::export3DComponent(const ModelNode &node)
// add icon // add icon
m_iconSavePath = targetPath.pathAppended(iconPath); m_iconSavePath = targetPath.pathAppended(iconPath);
m_iconSavePath.parentDir().ensureWritableDir(); m_iconSavePath.parentDir().ensureWritableDir();
getImageFromCache(compDir.pathAppended(compFileName).path(), [&](const QImage &image) { getImageFromCache(compFilePath.path(), [&](const QImage &image) {
addIconAndCloseZip(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); QString exportPath = getExportPath(node);
if (exportPath.isEmpty()) 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); Utils::FilePath jsonFilePath = targetPath.pathAppended(Constants::BUNDLE_JSON_FILENAME);
m_zipWriter->addFile(jsonFilePath.fileName(), QJsonDocument(jsonObj).toJson()); m_zipWriter->addFile(jsonFilePath.fileName(), QJsonDocument(jsonObj).toJson());
// add item's dependency assets to the bundle zip // add item's dependency assets to the bundle zip and target path (for icon generation)
for (const AssetPath &assetPath : depAssetsList) for (const AssetPath &assetPath : depAssetsList) {
m_zipWriter->addFile(assetPath.relativePath, assetPath.absFilPath().fileContents().value_or("")); 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 // add icon
QPixmap iconPixmapToSave; QPixmap iconPixmapToSave;
@@ -359,8 +366,8 @@ QPair<QString, QSet<AssetPath>> BundleHelper::modelNodeToQmlString(const ModelNo
const QList<PropertyName> excludedProps = {"x", "y", "z", "eulerRotation.x", "eulerRotation.y", const QList<PropertyName> excludedProps = {"x", "y", "z", "eulerRotation.x", "eulerRotation.y",
"eulerRotation.z", "scale.x", "scale.y", "scale.z", "eulerRotation.z", "scale.x", "scale.y", "scale.z",
"pivot.x", "pivot.y", "pivot.z"}; "pivot.x", "pivot.y", "pivot.z"};
const QList<AbstractProperty> matProps = node.properties(); const QList<AbstractProperty> nodeProps = node.properties();
for (const AbstractProperty &p : matProps) { for (const AbstractProperty &p : nodeProps) {
if (excludedProps.contains(p.name())) if (excludedProps.contains(p.name()))
continue; continue;
@@ -372,13 +379,11 @@ QPair<QString, QSet<AssetPath>> BundleHelper::modelNodeToQmlString(const ModelNo
// dynamic property with no value assigned // dynamic property with no value assigned
} else if (strcmp(pValue.typeName(), "QString") == 0 || strcmp(pValue.typeName(), "QColor") == 0) { } else if (strcmp(pValue.typeName(), "QString") == 0 || strcmp(pValue.typeName(), "QColor") == 0) {
val = QLatin1String("\"%1\"").arg(pValue.toString()); 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(); QString pValueStr = pValue.toString();
val = QLatin1String("\"%1\"").arg(pValueStr); val = QLatin1String("\"%1\"").arg(pValueStr);
if (!pValueStr.startsWith("#")) { if (!pValueStr.startsWith("#"))
assets.insert({DocumentManager::currentResourcePath().toFSPathString(), assets.insert({DocumentManager::currentFilePath().parentDir(), pValue.toString()});
pValue.toString()});
}
} else if (strcmp(pValue.typeName(), "QmlDesigner::Enumeration") == 0) { } else if (strcmp(pValue.typeName(), "QmlDesigner::Enumeration") == 0) {
val = pValue.value<QmlDesigner::Enumeration>().toString(); val = pValue.value<QmlDesigner::Enumeration>().toString();
} else { } else {
@@ -391,19 +396,25 @@ QPair<QString, QSet<AssetPath>> BundleHelper::modelNodeToQmlString(const ModelNo
qml += indent + p.name() + ": " + val + "\n"; qml += indent + p.name() + ": " + val + "\n";
} }
} else if (p.isBindingProperty()) { } else if (p.isBindingProperty()) {
ModelNode depNode = m_view->modelNodeForId(p.toBindingProperty().expression()); const QString pExp = p.toBindingProperty().expression();
QTC_ASSERT(depNode.isValid(), continue); const QStringList depNodesIds = ModelUtils::expressionToList(pExp);
QTC_ASSERT(!depNodesIds.isEmpty(), continue);
if (p.isDynamic()) if (p.isDynamic())
qml += indent + "property " + p.dynamicTypeName() + " " + p.name() + ": " + depNode.id() + "\n"; qml += indent + "property " + p.dynamicTypeName() + " " + p.name() + ": " + pExp + "\n";
else else
qml += indent + p.name() + ": " + depNode.id() + "\n"; qml += indent + p.name() + ": " + pExp + "\n";
if (depNode && !depListIds.contains(depNode.id())) { for (const QString &id : depNodesIds) {
depListIds.append(depNode.id()); ModelNode depNode = m_view->modelNodeForId(id);
auto [depQml, depAssets] = modelNodeToQmlString(depNode, depth + 1); QTC_ASSERT(depNode.isValid(), continue);
qml += "\n" + depQml + "\n";
assets.unite(depAssets); 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<QString, QSet<AssetPath>> BundleHelper::modelNodeToQmlString(const ModelNo
if (depth > 0) { if (depth > 0) {
// add component file to the dependency assets // add component file to the dependency assets
Utils::FilePath compFilePath = componentPath(node.metaInfo()); 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) if (isBundle)
assets.unite(getBundleComponentDependencies(node)); assets.unite(getBundleComponentDependencies(node));
} }
@@ -469,7 +481,7 @@ QSet<AssetPath> BundleHelper::getBundleComponentDependencies(const ModelNode &no
for (const QString &asset : bundleAssets) { for (const QString &asset : bundleAssets) {
if (rootObj.value(asset).toArray().contains(compFileName)) if (rootObj.value(asset).toArray().contains(compFileName))
depList.insert({compPath.toFSPathString(), asset}); depList.insert({compPath, asset});
} }
return depList; return depList;
@@ -575,4 +587,91 @@ bool BundleHelper::isItemBundle(const QString &bundleId) const
|| bundleId == compUtils.user3DBundleId(); || 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<AssetPath> BundleHelper::getComponentDependencies(const Utils::FilePath &filePath,
const Utils::FilePath &mainCompDir)
{
QSet<AssetPath> 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<void(const ModelNode &node)> 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<AbstractProperty> 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<ModelNode> childNodes = node.directSubModelNodes();
for (const ModelNode &childNode : childNodes)
parseNode(childNode);
};
parseNode(rootNode);
return depList;
}
} // namespace QmlDesigner } // namespace QmlDesigner

View File

@@ -22,21 +22,20 @@ class BundleImporter;
class ModelNode; class ModelNode;
class NodeMetaInfo; class NodeMetaInfo;
struct AssetPath class AssetPath
{ {
QString basePath; public:
Utils::FilePath basePath;
QString relativePath; QString relativePath;
Utils::FilePath absFilPath() const Utils::FilePath absFilPath() const;
{
return Utils::FilePath::fromString(basePath).pathAppended(relativePath);
}
bool operator==(const AssetPath &other) const bool operator==(const AssetPath &other) const
{ {
return basePath == other.basePath && relativePath == other.relativePath; return basePath == other.basePath && relativePath == other.relativePath;
} }
private:
friend size_t qHash(const AssetPath &asset) friend size_t qHash(const AssetPath &asset)
{ {
return ::qHash(asset.relativePath); return ::qHash(asset.relativePath);
@@ -65,8 +64,10 @@ private:
void addIconAndCloseZip(const auto &image); void addIconAndCloseZip(const auto &image);
Utils::FilePath componentPath(const NodeMetaInfo &metaInfo) const; Utils::FilePath componentPath(const NodeMetaInfo &metaInfo) const;
QSet<AssetPath> getBundleComponentDependencies(const ModelNode &node) const; QSet<AssetPath> getBundleComponentDependencies(const ModelNode &node) const;
void export3DComponent(const ModelNode &node); QSet<AssetPath> getComponentDependencies(const Utils::FilePath &filePath,
void exportItem(const ModelNode &node, const QPixmap &iconPixmap = QPixmap()); const Utils::FilePath &mainCompDir);
void exportComponent(const ModelNode &node);
void exportNode(const ModelNode &node, const QPixmap &iconPixmap = QPixmap());
QPointer<AbstractView> m_view; QPointer<AbstractView> m_view;
QPointer<QWidget> m_widget; QPointer<QWidget> m_widget;

View File

@@ -8,6 +8,7 @@
namespace QmlDesigner { namespace QmlDesigner {
constexpr QLatin1String componentBundlesMaterialBundleType{"Materials"}; constexpr QLatin1String componentBundlesMaterialBundleType{"Materials"};
constexpr QLatin1String componentBundlesEffectBundleType{"Effects"};
constexpr QLatin1String componentBundlesType{"Bundles"}; constexpr QLatin1String componentBundlesType{"Bundles"};
constexpr QLatin1String componentBundlesUser3DBundleType{"User3D"}; constexpr QLatin1String componentBundlesUser3DBundleType{"User3D"};
constexpr QLatin1String componentBundlesUserEffectsBundleType{"UserEffects"}; constexpr QLatin1String componentBundlesUserEffectsBundleType{"UserEffects"};
@@ -16,7 +17,8 @@ constexpr QLatin1String composedEffectType{"Effects"};
constexpr QLatin1String generatedComponentsFolder{"Generated"}; constexpr QLatin1String generatedComponentsFolder{"Generated"};
constexpr QLatin1String oldAssetImportFolder{"asset_imports"}; constexpr QLatin1String oldAssetImportFolder{"asset_imports"};
constexpr QLatin1String oldComponentBundleType{"ComponentBundles"}; constexpr QLatin1String oldComponentBundleType{"ComponentBundles"};
constexpr QLatin1String oldComponentsBundlesMaterialBundleType{"MaterialBundle"}; constexpr QLatin1String oldComponentBundlesMaterialBundleType{"MaterialBundle"};
constexpr QLatin1String oldComponentBundlesEffectBundleType{"EffectBundle"};
constexpr QLatin1String oldEffectFolder{"Effects"}; constexpr QLatin1String oldEffectFolder{"Effects"};
namespace Constants {} // namespace Constants namespace Constants {} // namespace Constants
@@ -128,9 +130,9 @@ Utils::FilePath GeneratedComponentUtils::materialBundlePath() const
return {}; return {};
if (basePath.endsWith(Constants::quick3DComponentsFolder)) 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 Utils::FilePath GeneratedComponentUtils::effectBundlePath() const
@@ -141,9 +143,9 @@ Utils::FilePath GeneratedComponentUtils::effectBundlePath() const
return {}; return {};
if (basePath.endsWith(Constants::quick3DComponentsFolder)) 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 Utils::FilePath GeneratedComponentUtils::userBundlePath(const QString &bundleId) const
@@ -218,7 +220,6 @@ bool GeneratedComponentUtils::isGeneratedPath(const QString &path) const
return path.startsWith(generatedComponentsPath().toFSPathString()); return path.startsWith(generatedComponentsPath().toFSPathString());
} }
QString GeneratedComponentUtils::generatedComponentTypePrefix() const QString GeneratedComponentUtils::generatedComponentTypePrefix() const
{ {
Utils::FilePath basePath = generatedComponentsPath(); Utils::FilePath basePath = generatedComponentsPath();
@@ -270,20 +271,20 @@ QString GeneratedComponentUtils::materialsBundleId() const
bool isNewImportDir = generatedComponentTypePrefix().endsWith(generatedComponentsFolder); bool isNewImportDir = generatedComponentTypePrefix().endsWith(generatedComponentsFolder);
return isNewImportDir ? componentBundlesMaterialBundleType return isNewImportDir ? componentBundlesMaterialBundleType
: oldComponentsBundlesMaterialBundleType; : oldComponentBundlesMaterialBundleType;
} }
QString GeneratedComponentUtils::effectsBundleId() const QString GeneratedComponentUtils::effectsBundleId() const
{ {
bool isNewImportDir = generatedComponentTypePrefix().endsWith(generatedComponentsFolder); bool isNewImportDir = generatedComponentTypePrefix().endsWith(generatedComponentsFolder);
return QLatin1String(isNewImportDir ? componentBundlesMaterialBundleType return isNewImportDir ? componentBundlesEffectBundleType
: componentBundlesMaterialBundleType); : oldComponentBundlesEffectBundleType;
} }
QString GeneratedComponentUtils::userMaterialsBundleId() const QString GeneratedComponentUtils::userMaterialsBundleId() const
{ {
return componentBundlesMaterialBundleType; return componentBundlesUserMaterialBundleType;
} }
QString GeneratedComponentUtils::userEffectsBundleId() const QString GeneratedComponentUtils::userEffectsBundleId() const