QmlDesigner: Refactor .qdsbundle exporting out of content lib

Task-number: QDS-13389
Change-Id: Ie9e3edb634f94973e4a6157b991bc4af9608e19b
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
This commit is contained in:
Mahmoud Badri
2024-08-29 10:51:12 +03:00
parent 6ecb6f1e1c
commit 6d2d3a4ece
9 changed files with 429 additions and 440 deletions

View File

@@ -4,19 +4,26 @@
#include "bundlehelper.h"
#include "bundleimporter.h"
#include "utils3d.h"
#include <abstractview.h>
#include <asynchronousimagecache.h>
#include <modelutils.h>
#include <nodelistproperty.h>
#include <nodemetainfo.h>
#include <qmldesignerconstants.h>
#include <qmldesignerplugin.h>
#include <utils3d.h>
#include <uniquename.h>
#include <variantproperty.h>
#include <coreplugin/icore.h>
#include <solutions/zip/zipreader.h>
#include <solutions/zip/zipwriter.h>
#include <utils/qtcassert.h>
#include <QBuffer>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
@@ -172,6 +179,353 @@ void BundleHelper::importBundleToProject()
zipReader.close();
}
void BundleHelper::exportBundle(const ModelNode &node, const QPixmap &iconPixmap)
{
if (node.isComponent())
export3DComponent(node);
else
exportItem(node, iconPixmap);
}
void BundleHelper::export3DComponent(const ModelNode &node)
{
QString exportPath = getExportPath(node);
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)
QTemporaryDir tempDir;
QTC_ASSERT(tempDir.isValid(), return);
auto targetPath = Utils::FilePath::fromString(tempDir.path());
m_zipWriter = std::make_unique<ZipWriter>(exportPath);
QString compBaseName = node.simplifiedTypeName();
QString compFileName = compBaseName + ".qml";
auto compDir = Utils::FilePath::fromString(ModelUtils::componentFilePath(node)).parentDir();
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)
for (const Utils::FilePath &sourcePath : sourceFiles) {
Utils::FilePath relativePath = sourcePath.relativePathFrom(compDir);
if (ignoreList.contains(sourcePath.fileName()) || relativePath.startsWith("source scene"))
continue;
m_zipWriter->addFile(relativePath.toFSPathString(), sourcePath.fileContents().value_or(""));
if (sourcePath.fileName() != compFileName) // skip component file (only collect dependencies)
filesList.append(relativePath.path());
}
// add the item to the bundle json
QJsonObject jsonObj;
QJsonArray itemsArr;
itemsArr.append(QJsonObject {
{"name", node.simplifiedTypeName()},
{"qml", compFileName},
{"icon", iconPath},
{"files", QJsonArray::fromStringList(filesList)}
});
jsonObj["items"] = itemsArr;
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
jsonObj["id"] = compUtils.user3DBundleId();
jsonObj["version"] = BUNDLE_VERSION;
Utils::FilePath jsonFilePath = targetPath.pathAppended(Constants::BUNDLE_JSON_FILENAME);
m_zipWriter->addFile(jsonFilePath.fileName(), QJsonDocument(jsonObj).toJson());
// add icon
m_iconSavePath = targetPath.pathAppended(iconPath);
m_iconSavePath.parentDir().ensureWritableDir();
getImageFromCache(compDir.pathAppended(compFileName).path(), [&](const QImage &image) {
addIconAndCloseZip(image);
});
}
void BundleHelper::exportItem(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
// 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);
QString name = node.variantProperty("objectName").value().toString();
if (name.isEmpty())
name = node.displayName();
QString qml = nodeNameToComponentFileName(name);
QString iconBaseName = UniqueName::generateId(name);
// generate and save Qml file
auto [qmlString, depAssets] = modelNodeToQmlString(node);
const QList<AssetPath> depAssetsList = depAssets.values();
QStringList depAssetsRelativePaths;
for (const AssetPath &assetPath : depAssetsList)
depAssetsRelativePaths.append(assetPath.relativePath);
auto qmlFilePath = targetPath.pathAppended(qml);
auto result = qmlFilePath.writeFileContents(qmlString.toUtf8());
QTC_ASSERT_EXPECTED(result, return);
m_zipWriter->addFile(qmlFilePath.fileName(), qmlString.toUtf8());
QString iconPath = QLatin1String("icons/%1.png").arg(iconBaseName);
// add the item to the bundle json
QJsonObject jsonObj;
QJsonArray itemsArr;
itemsArr.append(QJsonObject {
{"name", name},
{"qml", qml},
{"icon", 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
for (const AssetPath &assetPath : depAssetsList)
m_zipWriter->addFile(assetPath.relativePath, assetPath.absFilPath().fileContents().value_or(""));
// add icon
QPixmap iconPixmapToSave;
if (node.metaInfo().isQtQuick3DCamera()) {
iconPixmapToSave = Core::ICore::resourcePath("qmldesigner/contentLibraryImages/camera.png")
.toFSPathString();
} else if (node.metaInfo().isQtQuick3DLight()) {
iconPixmapToSave = Core::ICore::resourcePath("qmldesigner/contentLibraryImages/light.png")
.toFSPathString();
} else {
iconPixmapToSave = iconPixmap;
}
m_iconSavePath = targetPath.pathAppended(iconPath);
if (iconPixmapToSave.isNull()) {
getImageFromCache(qmlFilePath.toFSPathString(), [&](const QImage &image) {
addIconAndCloseZip(image);
});
} else {
addIconAndCloseZip(iconPixmapToSave);
}
}
QPair<QString, QSet<AssetPath>> BundleHelper::modelNodeToQmlString(const ModelNode &node, int depth)
{
static QStringList depListIds;
QString qml;
QSet<AssetPath> assets;
if (depth == 0) {
qml.append("import QtQuick\nimport QtQuick3D\n\n");
depListIds.clear();
}
QString indent = QString(" ").repeated(depth * 4);
qml += indent + node.simplifiedTypeName() + " {\n";
indent = QString(" ").repeated((depth + 1) * 4);
qml += indent + "id: " + (depth == 0 ? "root" : node.id()) + " \n\n";
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) {
if (excludedProps.contains(p.name()))
continue;
if (p.isVariantProperty()) {
QVariant pValue = p.toVariantProperty().value();
QString val;
if (!pValue.typeName()) {
// 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) {
QString pValueStr = pValue.toString();
val = QLatin1String("\"%1\"").arg(pValueStr);
if (!pValueStr.startsWith("#")) {
assets.insert({DocumentManager::currentResourcePath().toFSPathString(),
pValue.toString()});
}
} else if (strcmp(pValue.typeName(), "QmlDesigner::Enumeration") == 0) {
val = pValue.value<QmlDesigner::Enumeration>().toString();
} else {
val = pValue.toString();
}
if (p.isDynamic()) {
QString valWithColon = val.isEmpty() ? QString() : (": " + val);
qml += indent + "property " + p.dynamicTypeName() + " " + p.name() + valWithColon + "\n";
} else {
qml += indent + p.name() + ": " + val + "\n";
}
} else if (p.isBindingProperty()) {
ModelNode depNode = m_view->modelNodeForId(p.toBindingProperty().expression());
QTC_ASSERT(depNode.isValid(), continue);
if (p.isDynamic())
qml += indent + "property " + p.dynamicTypeName() + " " + p.name() + ": " + depNode.id() + "\n";
else
qml += indent + p.name() + ": " + depNode.id() + "\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);
}
}
}
// add child nodes
const ModelNodes nodeChildren = node.directSubModelNodes();
for (const ModelNode &childNode : nodeChildren) {
if (childNode && !depListIds.contains(childNode.id())) {
depListIds.append(childNode.id());
auto [depQml, depAssets] = modelNodeToQmlString(childNode, depth + 1);
qml += "\n" + depQml + "\n";
assets.unite(depAssets);
}
}
indent = QString(" ").repeated(depth * 4);
qml += indent + "}\n";
if (node.isComponent()) {
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
bool isBundle = node.type().startsWith(compUtils.componentBundlesTypePrefix().toLatin1());
if (depth > 0) {
// add component file to the dependency assets
Utils::FilePath compFilePath = componentPath(node.metaInfo());
assets.insert({compFilePath.parentDir().path(), compFilePath.fileName()});
}
if (isBundle)
assets.unite(getBundleComponentDependencies(node));
}
return {qml, assets};
}
QSet<AssetPath> BundleHelper::getBundleComponentDependencies(const ModelNode &node) const
{
const QString compFileName = node.simplifiedTypeName() + ".qml";
Utils::FilePath compPath = componentPath(node.metaInfo()).parentDir();
QTC_ASSERT(compPath.exists(), return {});
QSet<AssetPath> depList;
Utils::FilePath assetRefPath = compPath.pathAppended(Constants::COMPONENT_BUNDLES_ASSET_REF_FILE);
Utils::expected_str<QByteArray> assetRefContents = assetRefPath.fileContents();
if (!assetRefContents.has_value()) {
qWarning() << __FUNCTION__ << assetRefContents.error();
return {};
}
QJsonDocument jsonDoc = QJsonDocument::fromJson(assetRefContents.value());
if (jsonDoc.isNull()) {
qWarning() << __FUNCTION__ << "Invalid json file" << assetRefPath;
return {};
}
const QJsonObject rootObj = jsonDoc.object();
const QStringList bundleAssets = rootObj.keys();
for (const QString &asset : bundleAssets) {
if (rootObj.value(asset).toArray().contains(compFileName))
depList.insert({compPath.toFSPathString(), asset});
}
return depList;
}
Utils::FilePath BundleHelper::componentPath([[maybe_unused]] const NodeMetaInfo &metaInfo) const
{
#ifdef QDS_USE_PROJECTSTORAGE
// TODO
return {};
#else
return Utils::FilePath::fromString(metaInfo.componentFileName());
#endif
}
QString BundleHelper::nodeNameToComponentFileName(const QString &name) const
{
QString fileName = UniqueName::generateId(name, "Component");
fileName[0] = fileName.at(0).toUpper();
fileName.prepend("My");
return fileName + ".qml";
}
/**
* @brief Generates an icon image from a qml component
* @param qmlPath path to the qml component file to be rendered
* @param iconPath output save path of the generated icon
*/
void BundleHelper::getImageFromCache(const QString &qmlPath,
std::function<void(const QImage &image)> successCallback)
{
QmlDesignerPlugin::imageCache().requestSmallImage(
Utils::PathString{qmlPath},
successCallback,
[&](ImageCache::AbortReason abortReason) {
if (abortReason == ImageCache::AbortReason::Abort) {
qWarning() << QLatin1String("ContentLibraryView::getImageFromCache(): icon generation "
"failed for path %1, reason: Abort").arg(qmlPath);
} else if (abortReason == ImageCache::AbortReason::Failed) {
qWarning() << QLatin1String("ContentLibraryView::getImageFromCache(): icon generation "
"failed for path %1, reason: Failed").arg(qmlPath);
} else if (abortReason == ImageCache::AbortReason::NoEntry) {
qWarning() << QLatin1String("ContentLibraryView::getImageFromCache(): icon generation "
"failed for path %1, reason: NoEntry").arg(qmlPath);
}
});
}
void BundleHelper::addIconAndCloseZip(const auto &image) { // auto: QImage or QPixmap
QByteArray iconByteArray;
QBuffer buffer(&iconByteArray);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "PNG");
m_zipWriter->addFile("icons/" + m_iconSavePath.fileName(), iconByteArray);
m_zipWriter->close();
};
QString BundleHelper::getImportPath() const
{
Utils::FilePath projectFP = DocumentManager::currentProjectDirPath();
@@ -186,6 +540,24 @@ QString BundleHelper::getImportPath() const
.arg(Constants::BUNDLE_SUFFIX));
}
QString BundleHelper::getExportPath(const ModelNode &node) const
{
QString defaultExportFileName = QLatin1String("%1.%2").arg(node.displayName(),
Constants::BUNDLE_SUFFIX);
Utils::FilePath projectFP = DocumentManager::currentProjectDirPath();
if (projectFP.isEmpty()) {
projectFP = QmlDesignerPlugin::instance()->documentManager()
.currentDesignDocument()->fileName().parentDir();
}
QString dialogTitle = node.metaInfo().isQtQuick3DMaterial() ? QObject::tr("Export Material")
: QObject::tr("Export Component");
return QFileDialog::getSaveFileName(m_widget, dialogTitle,
projectFP.pathAppended(defaultExportFileName).toFSPathString(),
QObject::tr("Qt Design Studio Bundle Files (*.%1)")
.arg(Constants::BUNDLE_SUFFIX));
}
bool BundleHelper::isMaterialBundle(const QString &bundleId) const
{
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();

View File

@@ -3,17 +3,45 @@
#pragma once
#include <utils/filepath.h>
#include <utils/uniqueobjectptr.h>
#include <QPixmap>
#include <QPointer>
QT_FORWARD_DECLARE_CLASS(QString)
QT_FORWARD_DECLARE_CLASS(QTemporaryDir)
QT_FORWARD_DECLARE_CLASS(QWidget)
class ZipWriter;
namespace QmlDesigner {
class AbstractView;
class BundleImporter;
class ModelNode;
class NodeMetaInfo;
struct AssetPath
{
QString basePath;
QString relativePath;
Utils::FilePath absFilPath() const
{
return Utils::FilePath::fromString(basePath).pathAppended(relativePath);
}
bool operator==(const AssetPath &other) const
{
return basePath == other.basePath && relativePath == other.relativePath;
}
friend size_t qHash(const AssetPath &asset)
{
return ::qHash(asset.relativePath);
}
};
class BundleHelper
{
@@ -22,16 +50,30 @@ public:
~BundleHelper();
void importBundleToProject();
void exportBundle(const ModelNode &node, const QPixmap &iconPixmap = QPixmap());
void getImageFromCache(const QString &qmlPath,
std::function<void(const QImage &image)> successCallback);
QString nodeNameToComponentFileName(const QString &name) const;
QPair<QString, QSet<AssetPath>> modelNodeToQmlString(const ModelNode &node, int depth = 0);
QString getImportPath() const;
private:
void createImporter();
QString getExportPath(const ModelNode &node) const;
bool isMaterialBundle(const QString &bundleId) const;
bool isItemBundle(const QString &bundleId) const;
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());
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;
Utils::FilePath m_iconSavePath;
static constexpr char BUNDLE_VERSION[] = "1.0";
};

View File

@@ -2009,7 +2009,9 @@ void DesignerActionManager::createDefaultDesignerActions()
rootCategory,
QKeySequence(),
Priorities::ExportComponent,
&exportComponent,
[&](const SelectionContext &context) {
m_bundleHelper->exportBundle(context.currentSingleSelectedNode());
},
&is3DNode,
&is3DNode));

View File

@@ -798,15 +798,6 @@ void add3DAssetToContentLibrary(const SelectionContext &selectionContext)
selectionContext.view()->emitCustomNotification("add_3d_to_content_lib", {node});
}
void exportComponent(const SelectionContext &selectionContext)
{
#ifdef DETACH_DISABLED_VIEWS
QmlDesignerPlugin::instance()->mainWidget()->showDockWidget("ContentLibrary");
#endif
ModelNode node = selectionContext.currentSingleSelectedNode();
selectionContext.view()->emitCustomNotification("export_item_as_bundle", {node});
}
void goImplementation(const SelectionContext &selectionState)
{
addSignalHandlerOrGotoImplementation(selectionState, false);

View File

@@ -97,7 +97,6 @@ void removeLayout(const SelectionContext &selectionContext);
void removePositioner(const SelectionContext &selectionContext);
void moveToComponent(const SelectionContext &selectionContext);
void add3DAssetToContentLibrary(const SelectionContext &selectionContext);
void exportComponent(const SelectionContext &selectionContext);
PropertyName getIndexPropertyName(const ModelNode &modelNode);
void addItemToStackedContainer(const SelectionContext &selectionContext);
void increaseIndexOfStackedContainer(const SelectionContext &selectionContext);

View File

@@ -15,6 +15,7 @@
#include <asset.h>
#include <bindingproperty.h>
#include <bundlehelper.h>
#include <bundleimporter.h>
#include <designerpaths.h>
#include <documentmanager.h>
@@ -31,7 +32,6 @@
#include <variantproperty.h>
#include <solutions/zip/zipreader.h>
#include <solutions/zip/zipwriter.h>
#include <utils/algorithm.h>
@@ -43,14 +43,12 @@
#include <qtsupport/qtkitaspect.h>
#endif
#include <QBuffer>
#include <QImage>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <QPixmap>
#include <QTemporaryDir>
#include <QVector3D>
namespace QmlDesigner {
@@ -75,6 +73,8 @@ WidgetInfo ContentLibraryView::widgetInfo()
if (m_widget.isNull()) {
m_widget = new ContentLibraryWidget();
m_bundleHelper = std::make_unique<BundleHelper>(this, m_widget);
connect(m_widget, &ContentLibraryWidget::bundleMaterialDragStarted, this,
[&] (QmlDesigner::ContentLibraryMaterial *mat) {
m_draggedBundleMaterial = mat;
@@ -399,14 +399,6 @@ void ContentLibraryView::customNotification(const AbstractView *view,
else
addLibItem(nodeList.first());
m_widget->showTab(ContentLibraryWidget::TabIndex::UserAssetsTab);
} else if (identifier == "export_item_as_bundle") {
// TODO: support exporting 2D items
if (nodeList.first().isComponent())
exportLib3DComponent(nodeList.first());
else
exportLibItem(nodeList.first());
} else if (identifier == "export_material_as_bundle") {
exportLibItem(nodeList.first(), data.first().value<QPixmap>());
}
}
@@ -439,8 +431,6 @@ void ContentLibraryView::modelNodePreviewPixmapChanged(const ModelNode &,
{
if (requestId == ADD_ITEM_REQ_ID)
saveIconToBundle(pixmap);
else if (requestId == EXPORT_ITEM_REQ_ID)
addIconAndCloseZip(pixmap);
}
#ifdef QDS_USE_PROJECTSTORAGE
@@ -502,123 +492,6 @@ void ContentLibraryView::applyBundleMaterialToDropTarget(const ModelNode &bundle
}
#endif
namespace {
Utils::FilePath componentPath([[maybe_unused]] const NodeMetaInfo &metaInfo)
{
#ifdef QDS_USE_PROJECTSTORAGE
// TODO
return {};
#else
return Utils::FilePath::fromString(metaInfo.componentFileName());
#endif
}
} // namespace
QPair<QString, QSet<AssetPath>> ContentLibraryView::modelNodeToQmlString(const ModelNode &node, int depth)
{
static QStringList depListIds;
QString qml;
QSet<AssetPath> assets;
if (depth == 0) {
qml.append("import QtQuick\nimport QtQuick3D\n\n");
depListIds.clear();
}
QString indent = QString(" ").repeated(depth * 4);
qml += indent + node.simplifiedTypeName() + " {\n";
indent = QString(" ").repeated((depth + 1) * 4);
qml += indent + "id: " + (depth == 0 ? "root" : node.id()) + " \n\n";
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) {
if (excludedProps.contains(p.name()))
continue;
if (p.isVariantProperty()) {
QVariant pValue = p.toVariantProperty().value();
QString val;
if (!pValue.typeName()) {
// 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) {
QString pValueStr = pValue.toString();
val = QLatin1String("\"%1\"").arg(pValueStr);
if (!pValueStr.startsWith("#")) {
assets.insert({DocumentManager::currentResourcePath().toFSPathString(),
pValue.toString()});
}
} else if (strcmp(pValue.typeName(), "QmlDesigner::Enumeration") == 0) {
val = pValue.value<QmlDesigner::Enumeration>().toString();
} else {
val = pValue.toString();
}
if (p.isDynamic()) {
QString valWithColon = val.isEmpty() ? QString() : (": " + val);
qml += indent + "property " + p.dynamicTypeName() + " " + p.name() + valWithColon + "\n";
} else {
qml += indent + p.name() + ": " + val + "\n";
}
} else if (p.isBindingProperty()) {
ModelNode depNode = modelNodeForId(p.toBindingProperty().expression());
QTC_ASSERT(depNode.isValid(), continue);
if (p.isDynamic())
qml += indent + "property " + p.dynamicTypeName() + " " + p.name() + ": " + depNode.id() + "\n";
else
qml += indent + p.name() + ": " + depNode.id() + "\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);
}
}
}
// add child nodes
const ModelNodes nodeChildren = node.directSubModelNodes();
for (const ModelNode &childNode : nodeChildren) {
if (childNode && !depListIds.contains(childNode.id())) {
depListIds.append(childNode.id());
auto [depQml, depAssets] = modelNodeToQmlString(childNode, depth + 1);
qml += "\n" + depQml + "\n";
assets.unite(depAssets);
}
}
indent = QString(" ").repeated(depth * 4);
qml += indent + "}\n";
if (node.isComponent()) {
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
bool isBundle = node.type().startsWith(compUtils.componentBundlesTypePrefix().toLatin1());
if (depth > 0) {
// add component file to the dependency assets
Utils::FilePath compFilePath = componentPath(node.metaInfo());
assets.insert({compFilePath.parentDir().path(), compFilePath.fileName()});
}
if (isBundle)
assets.unite(getBundleComponentDependencies(node));
}
return {qml, assets};
}
void ContentLibraryView::addLibAssets(const QStringList &paths)
{
auto bundlePath = Utils::FilePath::fromString(Paths::bundlesPathSetting() + "/User/textures");
@@ -744,83 +617,11 @@ void ContentLibraryView::addLib3DComponent(const ModelNode &node)
filesList);
// generate and save icon
getImageFromCache(compDir.pathAppended(compFileName).path(), [&](const QImage &image) {
m_bundleHelper->getImageFromCache(compDir.pathAppended(compFileName).path(), [&](const QImage &image) {
saveIconToBundle(image);
});
}
void ContentLibraryView::exportLib3DComponent(const ModelNode &node)
{
QString exportPath = getExportPath(node);
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)
QTemporaryDir tempDir;
QTC_ASSERT(tempDir.isValid(), return);
auto targetPath = Utils::FilePath::fromString(tempDir.path());
m_zipWriter = std::make_unique<ZipWriter>(exportPath);
QString compBaseName = node.simplifiedTypeName();
QString compFileName = compBaseName + ".qml";
auto compDir = Utils::FilePath::fromString(ModelUtils::componentFilePath(node)).parentDir();
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)
for (const Utils::FilePath &sourcePath : sourceFiles) {
Utils::FilePath relativePath = sourcePath.relativePathFrom(compDir);
if (ignoreList.contains(sourcePath.fileName()) || relativePath.startsWith("source scene"))
continue;
m_zipWriter->addFile(relativePath.toFSPathString(), sourcePath.fileContents().value_or(""));
if (sourcePath.fileName() != compFileName) // skip component file (only collect dependencies)
filesList.append(relativePath.path());
}
// add the item to the bundle json
QJsonObject jsonObj;
QJsonArray itemsArr;
itemsArr.append(QJsonObject {
{"name", node.simplifiedTypeName()},
{"qml", compFileName},
{"icon", iconPath},
{"files", QJsonArray::fromStringList(filesList)}
});
jsonObj["items"] = itemsArr;
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
jsonObj["id"] = compUtils.user3DBundleId();
jsonObj["version"] = BUNDLE_VERSION;
Utils::FilePath jsonFilePath = targetPath.pathAppended(Constants::BUNDLE_JSON_FILENAME);
m_zipWriter->addFile(jsonFilePath.fileName(), QJsonDocument(jsonObj).toJson());
// add icon
m_iconSavePath = targetPath.pathAppended(iconPath);
m_iconSavePath.parentDir().ensureWritableDir();
getImageFromCache(compDir.pathAppended(compFileName).path(), [&](const QImage &image) {
addIconAndCloseZip(image);
});
}
QString ContentLibraryView::nodeNameToComponentFileName(const QString &name) const
{
QString fileName = UniqueName::generateId(name, "Component");
fileName[0] = fileName.at(0).toUpper();
fileName.prepend("My");
return fileName + ".qml";
}
void ContentLibraryView::addLibItem(const ModelNode &node, const QPixmap &iconPixmap)
{
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
@@ -849,7 +650,7 @@ void ContentLibraryView::addLibItem(const ModelNode &node, const QPixmap &iconPi
QJsonObject &jsonRef = m_widget->userModel()->bundleObjectRef(m_bundleId);
QJsonArray itemsArr = jsonRef.value("items").toArray();
QString qml = nodeNameToComponentFileName(name);
QString qml = m_bundleHelper->nodeNameToComponentFileName(name);
// confirm overwrite if an item with same name exists
if (m_widget->userModel()->jsonPropertyExists("qml", qml, m_bundleId)) {
@@ -866,7 +667,7 @@ void ContentLibraryView::addLibItem(const ModelNode &node, const QPixmap &iconPi
}
// generate and save Qml file
auto [qmlString, depAssets] = modelNodeToQmlString(node);
auto [qmlString, depAssets] = m_bundleHelper->modelNodeToQmlString(node);
const QList<AssetPath> depAssetsList = depAssets.values();
QStringList depAssetsRelativePaths;
@@ -932,123 +733,6 @@ void ContentLibraryView::addLibItem(const ModelNode &node, const QPixmap &iconPi
}
}
QString ContentLibraryView::getExportPath(const ModelNode &node) const
{
QString defaultExportFileName = QLatin1String("%1.%2").arg(node.displayName(),
Constants::BUNDLE_SUFFIX);
Utils::FilePath projectFP = DocumentManager::currentProjectDirPath();
if (projectFP.isEmpty()) {
projectFP = QmlDesignerPlugin::instance()->documentManager()
.currentDesignDocument()->fileName().parentDir();
}
QString dialogTitle = node.metaInfo().isQtQuick3DMaterial() ? tr("Export Material")
: tr("Export Component");
return QFileDialog::getSaveFileName(m_widget, dialogTitle,
projectFP.pathAppended(defaultExportFileName).toFSPathString(),
tr("Qt Design Studio Bundle Files (*.%1)").arg(Constants::BUNDLE_SUFFIX));
}
QString ContentLibraryView::getImportPath() const
{
Utils::FilePath projectFP = DocumentManager::currentProjectDirPath();
if (projectFP.isEmpty()) {
projectFP = QmlDesignerPlugin::instance()->documentManager()
.currentDesignDocument()->fileName().parentDir();
}
return QFileDialog::getOpenFileName(m_widget, tr("Import Component"), projectFP.toFSPathString(),
tr("Qt Design Studio Bundle Files (*.%1)").arg(Constants::BUNDLE_SUFFIX));
}
void ContentLibraryView::exportLibItem(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
// 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);
QString name = node.variantProperty("objectName").value().toString();
if (name.isEmpty())
name = node.displayName();
QString qml = nodeNameToComponentFileName(name);
QString iconBaseName = UniqueName::generateId(name);
// generate and save Qml file
auto [qmlString, depAssets] = modelNodeToQmlString(node);
const QList<AssetPath> depAssetsList = depAssets.values();
QStringList depAssetsRelativePaths;
for (const AssetPath &assetPath : depAssetsList)
depAssetsRelativePaths.append(assetPath.relativePath);
auto qmlFilePath = targetPath.pathAppended(qml);
auto result = qmlFilePath.writeFileContents(qmlString.toUtf8());
QTC_ASSERT_EXPECTED(result, return);
m_zipWriter->addFile(qmlFilePath.fileName(), qmlString.toUtf8());
QString iconPath = QLatin1String("icons/%1.png").arg(iconBaseName);
// add the item to the bundle json
QJsonObject jsonObj;
QJsonArray itemsArr;
itemsArr.append(QJsonObject {
{"name", name},
{"qml", qml},
{"icon", 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
for (const AssetPath &assetPath : depAssetsList)
m_zipWriter->addFile(assetPath.relativePath, assetPath.absFilPath().fileContents().value_or(""));
// add icon
QPixmap iconPixmapToSave;
if (node.metaInfo().isQtQuick3DCamera())
iconPixmapToSave = m_widget->iconProvider()->requestPixmap("camera.png", nullptr, {});
else if (node.metaInfo().isQtQuick3DLight())
iconPixmapToSave = m_widget->iconProvider()->requestPixmap("light.png", nullptr, {});
else
iconPixmapToSave = iconPixmap;
m_iconSavePath = targetPath.pathAppended(iconPath);
if (iconPixmapToSave.isNull()) {
static_cast<const NodeInstanceView *>(model()->nodeInstanceView())
->previewImageDataForGenericNode(node, {}, {}, EXPORT_ITEM_REQ_ID);
} else {
addIconAndCloseZip(iconPixmapToSave);
}
}
void ContentLibraryView::addIconAndCloseZip(const auto &image) { // auto: QImage or QPixmap
QByteArray iconByteArray;
QBuffer buffer(&iconByteArray);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "PNG");
m_zipWriter->addFile("icons/" + m_iconSavePath.fileName(), iconByteArray);
m_zipWriter->close();
};
void ContentLibraryView::saveIconToBundle(const auto &image) { // auto: QImage or QPixmap
bool iconSaved = image.save(m_iconSavePath.toFSPathString());
if (iconSaved)
@@ -1061,7 +745,7 @@ void ContentLibraryView::saveIconToBundle(const auto &image) { // auto: QImage o
void ContentLibraryView::importBundleToContentLib()
{
QString importPath = getImportPath();
QString importPath = m_bundleHelper->getImportPath();
if (importPath.isEmpty())
return;
@@ -1145,66 +829,6 @@ void ContentLibraryView::importBundleToContentLib()
QTC_ASSERT_EXPECTED(result,);
}
/**
* @brief Generates an icon image from a qml component
* @param qmlPath path to the qml component file to be rendered
* @param iconPath output save path of the generated icon
*/
void ContentLibraryView::getImageFromCache(const QString &qmlPath,
std::function<void(const QImage &image)> successCallback)
{
m_imageCache.requestSmallImage(
Utils::PathString{qmlPath},
successCallback,
[&](ImageCache::AbortReason abortReason) {
if (abortReason == ImageCache::AbortReason::Abort) {
qWarning() << QLatin1String("ContentLibraryView::getImageFromCache(): icon generation "
"failed for path %1, reason: Abort").arg(qmlPath);
} else if (abortReason == ImageCache::AbortReason::Failed) {
qWarning() << QLatin1String("ContentLibraryView::getImageFromCache(): icon generation "
"failed for path %1, reason: Failed").arg(qmlPath);
} else if (abortReason == ImageCache::AbortReason::NoEntry) {
qWarning() << QLatin1String("ContentLibraryView::getImageFromCache(): icon generation "
"failed for path %1, reason: NoEntry").arg(qmlPath);
}
});
}
QSet<AssetPath> ContentLibraryView::getBundleComponentDependencies(const ModelNode &node) const
{
const QString compFileName = node.simplifiedTypeName() + ".qml";
Utils::FilePath compPath = componentPath(node.metaInfo()).parentDir();
QTC_ASSERT(compPath.exists(), return {});
QSet<AssetPath> depList;
Utils::FilePath assetRefPath = compPath.pathAppended(Constants::COMPONENT_BUNDLES_ASSET_REF_FILE);
Utils::expected_str<QByteArray> assetRefContents = assetRefPath.fileContents();
if (!assetRefContents.has_value()) {
qWarning() << __FUNCTION__ << assetRefContents.error();
return {};
}
QJsonDocument jsonDoc = QJsonDocument::fromJson(assetRefContents.value());
if (jsonDoc.isNull()) {
qWarning() << __FUNCTION__ << "Invalid json file" << assetRefPath;
return {};
}
const QJsonObject rootObj = jsonDoc.object();
const QStringList bundleAssets = rootObj.keys();
for (const QString &asset : bundleAssets) {
if (rootObj.value(asset).toArray().contains(compFileName))
depList.insert({compPath.toFSPathString(), asset});
}
return depList;
}
ModelNode ContentLibraryView::getBundleMaterialDefaultInstance(const TypeName &type)
{
ModelNode matLib = Utils3D::materialLibraryNode(this);

View File

@@ -13,41 +13,19 @@
#include <QObject>
#include <QPointer>
class ZipWriter;
QT_FORWARD_DECLARE_CLASS(QImage)
QT_FORWARD_DECLARE_CLASS(QPixmap)
QT_FORWARD_DECLARE_CLASS(QTemporaryDir)
namespace QmlDesigner {
class BundleHelper;
class ContentLibraryItem;
class ContentLibraryMaterial;
class ContentLibraryTexture;
class ContentLibraryWidget;
class Model;
struct AssetPath
{
QString basePath;
QString relativePath;
Utils::FilePath absFilPath() const
{
return Utils::FilePath::fromString(basePath).pathAppended(relativePath);
}
bool operator==(const AssetPath &other) const
{
return basePath == other.basePath && relativePath == other.relativePath;
}
friend size_t qHash(const AssetPath &asset)
{
return ::qHash(asset.relativePath);
}
};
class ContentLibraryView : public AbstractView
{
Q_OBJECT
@@ -87,18 +65,8 @@ private:
void updateBundlesQuick3DVersion();
void addLibAssets(const QStringList &paths);
void addLib3DComponent(const ModelNode &node);
void exportLib3DComponent(const ModelNode &node);
void addLibItem(const ModelNode &node, const QPixmap &iconPixmap = {});
void exportLibItem(const ModelNode &node, const QPixmap &iconPixmap = {});
void importBundleToContentLib();
void getImageFromCache(const QString &qmlPath,
std::function<void(const QImage &image)> successCallback);
QSet<AssetPath> getBundleComponentDependencies(const ModelNode &node) const;
QString getExportPath(const ModelNode &node) const;
QString getImportPath() const;
QString nodeNameToComponentFileName(const QString &name) const;
QPair<QString, QSet<AssetPath>> modelNodeToQmlString(const ModelNode &node, int depth = 0);
void addIconAndCloseZip(const auto &image);
void saveIconToBundle(const auto &image);
#ifdef QDS_USE_PROJECTSTORAGE
@@ -117,6 +85,7 @@ private:
ContentLibraryMaterial *m_draggedBundleMaterial = nullptr;
ContentLibraryTexture *m_draggedBundleTexture = nullptr;
ContentLibraryItem *m_draggedBundleItem = nullptr;
std::unique_ptr<BundleHelper> m_bundleHelper;
AsynchronousImageCache &m_imageCache;
bool m_bundleMaterialAddToSelected = false;
bool m_hasQuick3DImport = false;
@@ -125,12 +94,9 @@ private:
Utils::FilePath m_iconSavePath;
QString m_generatedFolderName;
QString m_bundleId;
std::unique_ptr<ZipWriter> m_zipWriter;
std::unique_ptr<QTemporaryDir> m_tempDir;
static constexpr char BUNDLE_VERSION[] = "1.0";
static constexpr char ADD_ITEM_REQ_ID[] = "AddItemReqId";
static constexpr char EXPORT_ITEM_REQ_ID[] = "ExportItemReqId";
};
} // namespace QmlDesigner

View File

@@ -380,10 +380,7 @@ void Edit3DWidget::createContextMenu()
m_exportBundleAction = m_contextMenu->addAction(
contextIcon(DesignerIcons::CreateIcon), // TODO: placeholder icon
tr("Export Component"), [&] {
#ifdef DETACH_DISABLED_VIEWS
QmlDesignerPlugin::instance()->mainWidget()->showDockWidget("ContentLibrary");
#endif
view()->emitCustomNotification("export_item_as_bundle", {m_contextMenuTarget}); // To ContentLibrary
m_bundleHelper->exportBundle(m_contextMenuTarget);
});
m_contextMenu->addSeparator();

View File

@@ -385,12 +385,8 @@ void MaterialBrowserWidget::importMaterial()
}
void MaterialBrowserWidget::exportMaterial()
{
#ifdef DETACH_DISABLED_VIEWS
QmlDesignerPlugin::instance()->mainWidget()->showDockWidget("ContentLibrary");
#endif
ModelNode mat = m_materialBrowserModel->selectedMaterial();
m_materialBrowserView->emitCustomNotification("export_material_as_bundle", {mat},
{m_previewImageProvider->getPixmap(mat)}); // to ContentLibrary
m_bundleHelper->exportBundle(mat, m_previewImageProvider->getPixmap(mat));
}
QString MaterialBrowserWidget::qmlSourcesPath()