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 {
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<ZipWriter>(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<AssetPath> 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<QString, QSet<AssetPath>> BundleHelper::modelNodeToQmlString(const ModelNo
const QList<PropertyName> excludedProps = {"x", "y", "z", "eulerRotation.x", "eulerRotation.y",
"eulerRotation.z", "scale.x", "scale.y", "scale.z",
"pivot.x", "pivot.y", "pivot.z"};
const QList<AbstractProperty> matProps = node.properties();
for (const AbstractProperty &p : matProps) {
const QList<AbstractProperty> nodeProps = node.properties();
for (const AbstractProperty &p : nodeProps) {
if (excludedProps.contains(p.name()))
continue;
@@ -372,13 +379,11 @@ QPair<QString, QSet<AssetPath>> 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<QmlDesigner::Enumeration>().toString();
} else {
@@ -391,13 +396,18 @@ QPair<QString, QSet<AssetPath>> 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";
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());
@@ -407,6 +417,7 @@ QPair<QString, QSet<AssetPath>> BundleHelper::modelNodeToQmlString(const ModelNo
}
}
}
}
// add child nodes
const ModelNodes nodeChildren = node.directSubModelNodes();
@@ -430,9 +441,10 @@ QPair<QString, QSet<AssetPath>> 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<AssetPath> 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<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

View File

@@ -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<AssetPath> getBundleComponentDependencies(const ModelNode &node) const;
void export3DComponent(const ModelNode &node);
void exportItem(const ModelNode &node, const QPixmap &iconPixmap = QPixmap());
QSet<AssetPath> getComponentDependencies(const Utils::FilePath &filePath,
const Utils::FilePath &mainCompDir);
void exportComponent(const ModelNode &node);
void exportNode(const ModelNode &node, const QPixmap &iconPixmap = QPixmap());
QPointer<AbstractView> m_view;
QPointer<QWidget> m_widget;

View File

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