QmlDesigner: Allow exporting multiple items to a bundle

Fixes: QDS-13201
Change-Id: Id9a1981b91d9cbc3e98bd21fc01b76d89126c167
Reviewed-by: Shrief Gabr <shrief.gabr@qt.io>
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
This commit is contained in:
Mahmoud Badri
2025-02-27 12:31:54 +02:00
parent ea53994ff0
commit 5db5a5bd0d
6 changed files with 94 additions and 85 deletions

View File

@@ -182,7 +182,6 @@ void BundleHelper::importBundleToProject()
// TODO: before overwriting remove old item's dependencies (not harmful but for cleanup) // TODO: before overwriting remove old item's dependencies (not harmful but for cleanup)
} }
// add entry to model
QStringList files = itemObj.value("files").toVariant().toStringList(); QStringList files = itemObj.value("files").toVariant().toStringList();
QString icon = itemObj.value("icon").toString(); QString icon = itemObj.value("icon").toString();
@@ -207,28 +206,57 @@ void BundleHelper::importBundleToProject()
zipReader.close(); zipReader.close();
} }
void BundleHelper::exportBundle(const ModelNode &node, const QPixmap &iconPixmap) void BundleHelper::exportBundle(const QList<ModelNode> &nodes, const QPixmap &iconPixmap)
{ {
if (node.isComponent()) QTC_ASSERT(!nodes.isEmpty(), return);
exportComponent(node);
else
exportNode(node, iconPixmap);
}
void BundleHelper::exportComponent(const ModelNode &node) QString exportPath = getExportPath(nodes.at(0));
{
QString exportPath = getExportPath(node);
if (exportPath.isEmpty()) if (exportPath.isEmpty())
return; return;
m_zipWriter = std::make_unique<ZipWriter>(exportPath); m_zipWriter = std::make_unique<ZipWriter>(exportPath);
Utils::FilePath compFilePath = componentPath(node); m_tempDir = std::make_unique<QTemporaryDir>();
QTC_ASSERT(m_tempDir->isValid(), return);
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
QJsonObject jsonObj;
jsonObj["id"] = compUtils.user3DBundleId();
jsonObj["version"] = BUNDLE_VERSION;
QJsonArray itemsArr;
// remove nested nodes (they will be exported anyway as dependency of the parent)
QList<ModelNode> nodesToExport;
for (const ModelNode &node : nodes) {
bool isChild = std::ranges::any_of(nodes, [&](const ModelNode &possibleParent) {
return &node != &possibleParent && possibleParent.isAncestorOf(node);
});
if (!isChild)
nodesToExport.append(node);
}
m_remainingIcons = nodesToExport.size();
for (const ModelNode &node : std::as_const(nodesToExport)) {
if (node.isComponent())
itemsArr.append(exportComponent(node));
else
itemsArr.append(exportNode(node, iconPixmap));
}
jsonObj["items"] = itemsArr;
m_zipWriter->addFile(Constants::BUNDLE_JSON_FILENAME, QJsonDocument(jsonObj).toJson());
}
QJsonObject BundleHelper::exportComponent(const ModelNode &node)
{
Utils::FilePath compFilePath = Utils::FilePath::fromString(ModelUtils::componentFilePath(node));
Utils::FilePath compDir = compFilePath.parentDir(); Utils::FilePath compDir = compFilePath.parentDir();
QString compBaseName = compFilePath.completeBaseName(); QString compBaseName = compFilePath.completeBaseName();
QString compFileName = compFilePath.fileName(); QString compFileName = compFilePath.fileName();
m_iconPath = QLatin1String("icons/%1").arg(UniqueName::generateId(compBaseName) + ".png"); QString iconPath = QLatin1String("icons/%1").arg(UniqueName::generateId(compBaseName) + ".png");
const QSet<AssetPath> compDependencies = getComponentDependencies(compFilePath, compDir); const QSet<AssetPath> compDependencies = getComponentDependencies(compFilePath, compDir);
@@ -250,43 +278,25 @@ void BundleHelper::exportComponent(const ModelNode &node)
filesList.append(asset.relativePath); filesList.append(asset.relativePath);
} }
// add the item to the bundle json // add icon
QJsonObject jsonObj; QString filePath = compFilePath.path();
QJsonArray itemsArr; getImageFromCache(filePath, [this, iconPath](const QImage &image) {
itemsArr.append(QJsonObject { addIconAndCloseZip(iconPath, image);
});
return {
{"name", node.simplifiedTypeName()}, {"name", node.simplifiedTypeName()},
{"qml", compFileName}, {"qml", compFileName},
{"icon", m_iconPath}, {"icon", iconPath},
{"files", QJsonArray::fromStringList(filesList)} {"files", QJsonArray::fromStringList(filesList)}
}); };
jsonObj["items"] = itemsArr;
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
jsonObj["id"] = compUtils.user3DBundleId();
jsonObj["version"] = BUNDLE_VERSION;
m_zipWriter->addFile(Constants::BUNDLE_JSON_FILENAME, QJsonDocument(jsonObj).toJson());
// add icon
getImageFromCache(compFilePath.path(), [&](const QImage &image) {
addIconAndCloseZip(image);
});
} }
void BundleHelper::exportNode(const ModelNode &node, const QPixmap &iconPixmap) QJsonObject BundleHelper::exportNode(const ModelNode &node, const QPixmap &iconPixmap)
{ {
QString exportPath = getExportPath(node); // tempPath is a temp path for collecting and zipping assets, actual export target is where
if (exportPath.isEmpty())
return;
// targetPath is a temp path for collecting and zipping assets, actual export target is where
// the user chose to export (i.e. exportPath) // the user chose to export (i.e. exportPath)
m_tempDir = std::make_unique<QTemporaryDir>(); auto tempPath = Utils::FilePath::fromString(m_tempDir->path());
QTC_ASSERT(m_tempDir->isValid(), return);
auto targetPath = Utils::FilePath::fromString(m_tempDir->path());
m_zipWriter = std::make_unique<ZipWriter>(exportPath);
QString name = node.variantProperty("objectName").value().toString(); QString name = node.variantProperty("objectName").value().toString();
if (name.isEmpty()) if (name.isEmpty())
@@ -294,7 +304,7 @@ void BundleHelper::exportNode(const ModelNode &node, const QPixmap &iconPixmap)
QString qml = nodeNameToComponentFileName(name); QString qml = nodeNameToComponentFileName(name);
QString iconBaseName = UniqueName::generateId(name); QString iconBaseName = UniqueName::generateId(name);
m_iconPath = QLatin1String("icons/%1.png").arg(iconBaseName); QString iconPath = QLatin1String("icons/%1.png").arg(iconBaseName);
// generate and save Qml file // generate and save Qml file
auto [qmlString, depAssets] = modelNodeToQmlString(node); auto [qmlString, depAssets] = modelNodeToQmlString(node);
@@ -304,37 +314,17 @@ void BundleHelper::exportNode(const ModelNode &node, const QPixmap &iconPixmap)
for (const AssetPath &assetPath : depAssetsList) for (const AssetPath &assetPath : depAssetsList)
depAssetsRelativePaths.append(assetPath.relativePath); depAssetsRelativePaths.append(assetPath.relativePath);
auto qmlFilePath = targetPath.pathAppended(qml); auto qmlFilePath = tempPath.pathAppended(qml);
auto result = qmlFilePath.writeFileContents(qmlString.toUtf8()); auto result = qmlFilePath.writeFileContents(qmlString.toUtf8());
QTC_ASSERT_EXPECTED(result, return); QTC_ASSERT_EXPECTED(result, return {});
m_zipWriter->addFile(qmlFilePath.fileName(), qmlString.toUtf8()); m_zipWriter->addFile(qmlFilePath.fileName(), qmlString.toUtf8());
// add the item to the bundle json
QJsonObject jsonObj;
QJsonArray itemsArr;
itemsArr.append(QJsonObject {
{"name", name},
{"qml", qml},
{"icon", m_iconPath},
{"files", QJsonArray::fromStringList(depAssetsRelativePaths)}
});
jsonObj["items"] = itemsArr;
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
jsonObj["id"] = node.metaInfo().isQtQuick3DMaterial() ? compUtils.userMaterialsBundleId()
: compUtils.user3DBundleId();
jsonObj["version"] = BUNDLE_VERSION;
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 and target path (for icon generation) // 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) {
QByteArray assetContent = assetPath.fileContent(); QByteArray assetContent = assetPath.fileContent();
m_zipWriter->addFile(assetPath.relativePath, assetContent); m_zipWriter->addFile(assetPath.relativePath, assetContent);
Utils::FilePath assetTargetPath = targetPath.pathAppended(assetPath.relativePath); Utils::FilePath assetTargetPath = tempPath.pathAppended(assetPath.relativePath);
assetTargetPath.parentDir().ensureWritableDir(); assetTargetPath.parentDir().ensureWritableDir();
assetTargetPath.writeFileContents(assetContent); assetTargetPath.writeFileContents(assetContent);
} }
@@ -353,12 +343,19 @@ void BundleHelper::exportNode(const ModelNode &node, const QPixmap &iconPixmap)
} }
if (iconPixmapToSave.isNull()) { if (iconPixmapToSave.isNull()) {
getImageFromCache(qmlFilePath.toFSPathString(), [&](const QImage &image) { getImageFromCache(qmlFilePath.toFSPathString(), [this, iconPath](const QImage &image) {
addIconAndCloseZip(image); addIconAndCloseZip(iconPath, image);
}); });
} else { } else {
addIconAndCloseZip(iconPixmapToSave); addIconAndCloseZip(iconPath, iconPixmapToSave);
} }
return {
{"name", name},
{"qml", qml},
{"icon", iconPath},
{"files", QJsonArray::fromStringList(depAssetsRelativePaths)}
};
} }
QPair<QString, QSet<AssetPath>> BundleHelper::modelNodeToQmlString(const ModelNode &node, int depth) QPair<QString, QSet<AssetPath>> BundleHelper::modelNodeToQmlString(const ModelNode &node, int depth)
@@ -544,14 +541,16 @@ void BundleHelper::getImageFromCache(const QString &qmlPath,
}); });
} }
void BundleHelper::addIconAndCloseZip(const auto &image) { // auto: QImage or QPixmap void BundleHelper::addIconAndCloseZip(const QString &iconPath, const auto &image) { // auto: QImage or QPixmap
QByteArray iconByteArray; QByteArray iconByteArray;
QBuffer buffer(&iconByteArray); QBuffer buffer(&iconByteArray);
buffer.open(QIODevice::WriteOnly); buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "PNG"); image.save(&buffer, "PNG");
m_zipWriter->addFile(m_iconPath, iconByteArray); m_zipWriter->addFile(iconPath, iconByteArray);
m_zipWriter->close();
if (--m_remainingIcons <= 0)
m_zipWriter->close();
}; };
QString BundleHelper::getImportPath() const QString BundleHelper::getImportPath() const
@@ -689,6 +688,10 @@ QSet<AssetPath> BundleHelper::getComponentDependencies(const Utils::FilePath &fi
parseNode = [&](const ModelNode &node) { parseNode = [&](const ModelNode &node) {
// workaround node.isComponent() as it is not working here // workaround node.isComponent() as it is not working here
QString nodeType = QString::fromLatin1(node.type()); QString nodeType = QString::fromLatin1(node.type());
#ifdef QDS_USE_PROJECTSTORAGE
// TODO
#else
if (!nodeType.startsWith("QtQuick")) { if (!nodeType.startsWith("QtQuick")) {
Utils::FilePath compFilPath = getComponentFilePath(nodeType, mainCompDir); Utils::FilePath compFilPath = getComponentFilePath(nodeType, mainCompDir);
if (!compFilPath.isEmpty()) { if (!compFilPath.isEmpty()) {
@@ -705,6 +708,7 @@ QSet<AssetPath> BundleHelper::getComponentDependencies(const Utils::FilePath &fi
return; return;
} }
} }
#endif
const QList<AbstractProperty> nodeProps = node.properties(); const QList<AbstractProperty> nodeProps = node.properties();
for (const AbstractProperty &p : nodeProps) { for (const AbstractProperty &p : nodeProps) {

View File

@@ -57,7 +57,7 @@ public:
~BundleHelper(); ~BundleHelper();
void importBundleToProject(); void importBundleToProject();
void exportBundle(const ModelNode &node, const QPixmap &iconPixmap = QPixmap()); void exportBundle(const QList<ModelNode> &nodes, const QPixmap &iconPixmap = QPixmap());
void getImageFromCache(const QString &qmlPath, void getImageFromCache(const QString &qmlPath,
std::function<void(const QImage &image)> successCallback); std::function<void(const QImage &image)> successCallback);
QString nodeNameToComponentFileName(const QString &name) const; QString nodeNameToComponentFileName(const QString &name) const;
@@ -71,18 +71,18 @@ private:
QString getExportPath(const ModelNode &node) const; QString getExportPath(const ModelNode &node) const;
bool isMaterialBundle(const QString &bundleId) const; bool isMaterialBundle(const QString &bundleId) const;
bool isItemBundle(const QString &bundleId) const; bool isItemBundle(const QString &bundleId) const;
void addIconAndCloseZip(const auto &image); void addIconAndCloseZip(const QString &iconPath, const auto &image);
Utils::FilePath componentPath(const ModelNode &node) const; Utils::FilePath componentPath(const ModelNode &node) const;
QSet<AssetPath> getBundleComponentDependencies(const ModelNode &node) const; QSet<AssetPath> getBundleComponentDependencies(const ModelNode &node) const;
void exportComponent(const ModelNode &node); QJsonObject exportComponent(const ModelNode &node);
void exportNode(const ModelNode &node, const QPixmap &iconPixmap = QPixmap()); QJsonObject exportNode(const ModelNode &node, const QPixmap &iconPixmap = QPixmap());
QPointer<AbstractView> m_view; QPointer<AbstractView> m_view;
QPointer<QWidget> m_widget; QPointer<QWidget> m_widget;
Utils::UniqueObjectPtr<BundleImporter> m_importer; Utils::UniqueObjectPtr<BundleImporter> m_importer;
std::unique_ptr<ZipWriter> m_zipWriter; std::unique_ptr<ZipWriter> m_zipWriter;
std::unique_ptr<QTemporaryDir> m_tempDir; std::unique_ptr<QTemporaryDir> m_tempDir;
QString m_iconPath; int m_remainingIcons = 0;
static constexpr char BUNDLE_VERSION[] = "1.0"; static constexpr char BUNDLE_VERSION[] = "1.0";
}; };

View File

@@ -2033,10 +2033,10 @@ void DesignerActionManager::createDefaultDesignerActions()
QKeySequence(), QKeySequence(),
Priorities::ExportComponent, Priorities::ExportComponent,
[&](const SelectionContext &context) { [&](const SelectionContext &context) {
m_bundleHelper->exportBundle(context.currentSingleSelectedNode()); m_bundleHelper->exportBundle(context.selectedModelNodes());
}, },
&is3DNode, &are3DNodes,
&is3DNode)); &are3DNodes));
addDesignerAction(new ModelNodeContextMenuAction( addDesignerAction(new ModelNodeContextMenuAction(
editMaterialCommandId, editMaterialCommandId,

View File

@@ -80,11 +80,15 @@ inline bool enableAddToContentLib(const SelectionContext &selectionState)
return isNode3D && !isInBundle; return isNode3D && !isInBundle;
} }
inline bool is3DNode(const SelectionContext &selectionState) inline bool are3DNodes(const SelectionContext &selectionState)
{ {
ModelNode modelNode = selectionState.currentSingleSelectedNode(); const QList<ModelNode> nodes = selectionState.selectedModelNodes();
if (nodes.isEmpty())
return false;
return modelNode.metaInfo().isQtQuick3DNode(); return std::all_of(nodes.cbegin(), nodes.cend(), [](const ModelNode &node) {
return node.metaInfo().isQtQuick3DNode();
});
} }
inline bool hasEditableMaterial(const SelectionContext &selectionState) inline bool hasEditableMaterial(const SelectionContext &selectionState)

View File

@@ -385,7 +385,7 @@ void Edit3DWidget::createContextMenu()
m_exportBundleAction = m_contextMenu->addAction( m_exportBundleAction = m_contextMenu->addAction(
contextIcon(DesignerIcons::CreateIcon), // TODO: placeholder icon contextIcon(DesignerIcons::CreateIcon), // TODO: placeholder icon
tr("Export Component"), [&] { tr("Export Component"), [&] {
m_bundleHelper->exportBundle(m_contextMenuTarget); m_bundleHelper->exportBundle(m_view->selectedModelNodes());
}); });
m_contextMenu->addSeparator(); m_contextMenu->addSeparator();

View File

@@ -382,10 +382,11 @@ void MaterialBrowserWidget::importMaterial()
{ {
m_bundleHelper->importBundleToProject(); m_bundleHelper->importBundleToProject();
} }
void MaterialBrowserWidget::exportMaterial() void MaterialBrowserWidget::exportMaterial()
{ {
ModelNode mat = m_materialBrowserModel->selectedMaterial(); ModelNode mat = m_materialBrowserModel->selectedMaterial();
m_bundleHelper->exportBundle(mat, m_previewImageProvider->getPixmap(mat)); m_bundleHelper->exportBundle({mat}, m_previewImageProvider->getPixmap(mat));
} }
QString MaterialBrowserWidget::qmlSourcesPath() QString MaterialBrowserWidget::qmlSourcesPath()