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)
}
// add entry to model
QStringList files = itemObj.value("files").toVariant().toStringList();
QString icon = itemObj.value("icon").toString();
@@ -207,28 +206,57 @@ void BundleHelper::importBundleToProject()
zipReader.close();
}
void BundleHelper::exportBundle(const ModelNode &node, const QPixmap &iconPixmap)
void BundleHelper::exportBundle(const QList<ModelNode> &nodes, const QPixmap &iconPixmap)
{
if (node.isComponent())
exportComponent(node);
else
exportNode(node, iconPixmap);
}
QTC_ASSERT(!nodes.isEmpty(), return);
void BundleHelper::exportComponent(const ModelNode &node)
{
QString exportPath = getExportPath(node);
QString exportPath = getExportPath(nodes.at(0));
if (exportPath.isEmpty())
return;
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();
QString compBaseName = compFilePath.completeBaseName();
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);
@@ -250,43 +278,25 @@ void BundleHelper::exportComponent(const ModelNode &node)
filesList.append(asset.relativePath);
}
// add the item to the bundle json
QJsonObject jsonObj;
QJsonArray itemsArr;
itemsArr.append(QJsonObject {
// add icon
QString filePath = compFilePath.path();
getImageFromCache(filePath, [this, iconPath](const QImage &image) {
addIconAndCloseZip(iconPath, image);
});
return {
{"name", node.simplifiedTypeName()},
{"qml", compFileName},
{"icon", m_iconPath},
{"icon", iconPath},
{"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);
if (exportPath.isEmpty())
return;
// targetPath is a temp path for collecting and zipping assets, actual export target is where
// tempPath is a temp path for collecting and zipping assets, actual export target is where
// the user chose to export (i.e. exportPath)
m_tempDir = std::make_unique<QTemporaryDir>();
QTC_ASSERT(m_tempDir->isValid(), return);
auto targetPath = Utils::FilePath::fromString(m_tempDir->path());
m_zipWriter = std::make_unique<ZipWriter>(exportPath);
auto tempPath = Utils::FilePath::fromString(m_tempDir->path());
QString name = node.variantProperty("objectName").value().toString();
if (name.isEmpty())
@@ -294,7 +304,7 @@ void BundleHelper::exportNode(const ModelNode &node, const QPixmap &iconPixmap)
QString qml = nodeNameToComponentFileName(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
auto [qmlString, depAssets] = modelNodeToQmlString(node);
@@ -304,37 +314,17 @@ void BundleHelper::exportNode(const ModelNode &node, const QPixmap &iconPixmap)
for (const AssetPath &assetPath : depAssetsList)
depAssetsRelativePaths.append(assetPath.relativePath);
auto qmlFilePath = targetPath.pathAppended(qml);
auto qmlFilePath = tempPath.pathAppended(qml);
auto result = qmlFilePath.writeFileContents(qmlString.toUtf8());
QTC_ASSERT_EXPECTED(result, return);
QTC_ASSERT_EXPECTED(result, return {});
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)
for (const AssetPath &assetPath : depAssetsList) {
QByteArray assetContent = assetPath.fileContent();
m_zipWriter->addFile(assetPath.relativePath, assetContent);
Utils::FilePath assetTargetPath = targetPath.pathAppended(assetPath.relativePath);
Utils::FilePath assetTargetPath = tempPath.pathAppended(assetPath.relativePath);
assetTargetPath.parentDir().ensureWritableDir();
assetTargetPath.writeFileContents(assetContent);
}
@@ -353,12 +343,19 @@ void BundleHelper::exportNode(const ModelNode &node, const QPixmap &iconPixmap)
}
if (iconPixmapToSave.isNull()) {
getImageFromCache(qmlFilePath.toFSPathString(), [&](const QImage &image) {
addIconAndCloseZip(image);
getImageFromCache(qmlFilePath.toFSPathString(), [this, iconPath](const QImage &image) {
addIconAndCloseZip(iconPath, image);
});
} 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)
@@ -544,13 +541,15 @@ 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;
QBuffer buffer(&iconByteArray);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "PNG");
m_zipWriter->addFile(m_iconPath, iconByteArray);
m_zipWriter->addFile(iconPath, iconByteArray);
if (--m_remainingIcons <= 0)
m_zipWriter->close();
};
@@ -689,6 +688,10 @@ QSet<AssetPath> BundleHelper::getComponentDependencies(const Utils::FilePath &fi
parseNode = [&](const ModelNode &node) {
// workaround node.isComponent() as it is not working here
QString nodeType = QString::fromLatin1(node.type());
#ifdef QDS_USE_PROJECTSTORAGE
// TODO
#else
if (!nodeType.startsWith("QtQuick")) {
Utils::FilePath compFilPath = getComponentFilePath(nodeType, mainCompDir);
if (!compFilPath.isEmpty()) {
@@ -705,6 +708,7 @@ QSet<AssetPath> BundleHelper::getComponentDependencies(const Utils::FilePath &fi
return;
}
}
#endif
const QList<AbstractProperty> nodeProps = node.properties();
for (const AbstractProperty &p : nodeProps) {

View File

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

View File

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

View File

@@ -80,11 +80,15 @@ inline bool enableAddToContentLib(const SelectionContext &selectionState)
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)

View File

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

View File

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