QmlDesigner: Allow saving a 3D node to the content library

Fixes: QDS-12393
Change-Id: I3112244bcbe74ea0f8f2eda8ccb6ad7470ca0e1e
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
Reviewed-by: Qt CI Patch Build Bot <ci_patchbuild_bot@qt.io>
This commit is contained in:
Mahmoud Badri
2024-05-08 18:29:07 +03:00
parent 99fcb5c14b
commit c1172ced98
7 changed files with 188 additions and 33 deletions

View File

@@ -65,7 +65,7 @@ public:
externalDependencies,
true)
, collectionView{externalDependencies}
, contentLibraryView{externalDependencies}
, contentLibraryView{imageCache, externalDependencies}
, componentView{externalDependencies}
#ifndef QTC_USE_QML_DESIGNER_LITE
, edit3DView{externalDependencies}

View File

@@ -98,17 +98,35 @@ void ContentLibraryUserModel::addMaterial(const QString &name, const QString &qm
const QUrl &icon, const QStringList &files)
{
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
QString typePrefix = compUtils.userMaterialsBundleType();
TypeName type = QLatin1String("%1.%2").arg(typePrefix, qml.chopped(4)).toLatin1();
auto libMat = new ContentLibraryMaterial(this, name, qml, type, icon, files,
Paths::bundlesPathSetting().append("/User/materials"));
m_userMaterials.append(libMat);
int matSectionIdx = 0;
emit dataChanged(index(matSectionIdx), index(matSectionIdx));
}
void ContentLibraryUserModel::add3DItem(const QString &name, const QString &qml,
const QUrl &icon, const QStringList &files)
{
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
QString typePrefix = compUtils.user3DBundleType();
TypeName type = QLatin1String("%1.%2").arg(typePrefix, qml.chopped(4)).toLatin1();
m_user3DItems.append(new ContentLibraryItem(this, name, qml, type, icon, files));
}
void ContentLibraryUserModel::refresh3DSection()
{
int sectionIdx = 2;
emit dataChanged(index(sectionIdx), index(sectionIdx));
}
void ContentLibraryUserModel::addTextures(const QStringList &paths)
{
QDir bundleDir{Paths::bundlesPathSetting() + "/User/textures"};
@@ -198,7 +216,7 @@ void ContentLibraryUserModel::removeFromContentLib(ContentLibraryMaterial *mat)
}
// returns unique library material's name and qml component
QPair<QString, QString> ContentLibraryUserModel::getUniqueLibMaterialNameAndQml(const QString &matName) const
QPair<QString, QString> ContentLibraryUserModel::getUniqueLibMaterialNameAndQml(const QString &defaultName) const
{
QTC_ASSERT(!m_bundleObjMaterial.isEmpty(), return {});
@@ -209,10 +227,9 @@ QPair<QString, QString> ContentLibraryUserModel::getUniqueLibMaterialNameAndQml(
for (const QString &matName : matNames)
matQmls.append(matsObj.value(matName).toObject().value("qml").toString().chopped(4)); // remove .qml
QString retName = matName.isEmpty() ? "Material" : matName;
retName = retName.trimmed();
QString retName = defaultName.isEmpty() ? "Material" : defaultName.trimmed();
QString retQml = retName;
retQml.remove(' ');
if (retQml.at(0).isLower())
retQml[0] = retQml.at(0).toUpper();
@@ -232,6 +249,32 @@ QPair<QString, QString> ContentLibraryUserModel::getUniqueLibMaterialNameAndQml(
return {retName, retQml + ".qml"};
}
QString ContentLibraryUserModel::getUniqueLib3DQmlName(const QString &defaultName) const
{
QTC_ASSERT(!m_bundleObj3D.isEmpty(), return {});
const QJsonArray itemsArr = m_bundleObj3D.value("items").toArray();
QStringList itemQmls;
for (const QJsonValueConstRef &itemRef : itemsArr)
itemQmls.append(itemRef.toObject().value("qml").toString().chopped(4)); // remove .qml
QString baseQml = defaultName.isEmpty() ? "Item" : defaultName.trimmed();
baseQml.remove(' ');
baseQml[0] = baseQml.at(0).toUpper();
baseQml.prepend("My");
QString uniqueQml = baseQml;
int counter = 1;
while (itemQmls.contains(uniqueQml)) {
uniqueQml = QString("%1%2").arg(uniqueQml).arg(counter);
++counter;
}
return uniqueQml + ".qml";
}
QHash<int, QByteArray> ContentLibraryUserModel::roleNames() const
{
static const QHash<int, QByteArray> roles {
@@ -242,11 +285,16 @@ QHash<int, QByteArray> ContentLibraryUserModel::roleNames() const
return roles;
}
QJsonObject &ContentLibraryUserModel::bundleJsonObjectRef()
QJsonObject &ContentLibraryUserModel::bundleJsonMaterialObjectRef()
{
return m_bundleObjMaterial;
}
QJsonObject &ContentLibraryUserModel::bundleJson3DObjectRef()
{
return m_bundleObj3D;
}
void ContentLibraryUserModel::loadBundles()
{
loadMaterialBundle();
@@ -356,15 +404,15 @@ void ContentLibraryUserModel::load3DBundle()
int section3DIdx = 2;
m_bundlePath3D = Utils::FilePath::fromString(Paths::bundlesPathSetting() + "/User/3d");
m_bundlePath3D.createDir();
m_bundlePath3D.ensureWritableDir();
m_bundlePath3D.pathAppended("icons").ensureWritableDir();
auto jsonFilePath = m_bundlePath3D.pathAppended("user_3d_bundle.json");
if (!jsonFilePath.exists()) {
QByteArray jsonContent = "{\n";
jsonContent += " \"id\": \"User3D\",\n";
jsonContent += " \"items\": {\n";
jsonContent += " }\n";
jsonContent += " \"items\": []\n";
jsonContent += "}";
Utils::expected_str<qint64> res = jsonFilePath.writeFileContents(jsonContent);
if (!res.has_value()) {
@@ -393,24 +441,21 @@ void ContentLibraryUserModel::load3DBundle()
m_bundleObj3D["id"] = m_bundleId3D;
// parse 3d items
const QJsonObject itemsObj = m_bundleObj3D.value("items").toObject();
const QStringList itemNames = itemsObj.keys();
QString typePrefix = compUtils.user3DBundleType();
for (const QString &itemName : itemNames) {
const QJsonObject itemObj = itemsObj.value(itemName).toObject();
const QJsonArray itemsArr = m_bundleObj3D.value("items").toArray();
for (const QJsonValueConstRef &itemRef : itemsArr) {
const QJsonObject itemObj = itemRef.toObject();
QString name = itemObj.value("name").toString();
QString qml = itemObj.value("qml").toString();
TypeName type = QLatin1String("%1.%2").arg(typePrefix, qml.chopped(4)).toLatin1();
QUrl icon = m_bundlePath3D.pathAppended(itemObj.value("icon").toString()).toUrl();
QStringList files;
const QJsonArray assetsArr = itemObj.value("files").toArray();
for (const QJsonValueConstRef &asset : assetsArr)
files.append(asset.toString());
QUrl icon = m_bundlePath3D.pathAppended(itemObj.value("icon").toString()).toUrl();
QString qml = itemObj.value("qml").toString();
TypeName type = QLatin1String("%1.%2").arg(typePrefix, qml.chopped(4)).toLatin1();
auto bundleItem = new ContentLibraryItem(nullptr, itemName, qml, type, icon, files);
m_user3DItems.append(bundleItem);
m_user3DItems.append(new ContentLibraryItem(nullptr, name, qml, type, icon, files));
}
m_bundle3DSharedFiles.clear();

View File

@@ -43,7 +43,8 @@ public:
void setSearchText(const QString &searchText);
void updateImportedState(const QStringList &importedItems);
QPair<QString, QString> getUniqueLibMaterialNameAndQml(const QString &matName) const;
QPair<QString, QString> getUniqueLibMaterialNameAndQml(const QString &defaultName = {}) const;
QString getUniqueLib3DQmlName(const QString &defaultName = {}) const;
void setQuick3DImportVersion(int major, int minor);
@@ -59,12 +60,15 @@ public:
void updateIsEmpty3D();
void addMaterial(const QString &name, const QString &qml, const QUrl &icon, const QStringList &files);
void add3DItem(const QString &name, const QString &qml, const QUrl &icon, const QStringList &files);
void refresh3DSection();
void addTextures(const QStringList &paths);
void add3DInstance(ContentLibraryItem *bundleItem);
void setBundleObj(const QJsonObject &newBundleObj);
QJsonObject &bundleJsonObjectRef();
QJsonObject &bundleJsonMaterialObjectRef();
QJsonObject &bundleJson3DObjectRef();
void loadBundles();

View File

@@ -44,8 +44,10 @@
namespace QmlDesigner {
ContentLibraryView::ContentLibraryView(ExternalDependenciesInterface &externalDependencies)
ContentLibraryView::ContentLibraryView(AsynchronousImageCache &imageCache,
ExternalDependenciesInterface &externalDependencies)
: AbstractView(externalDependencies)
, m_imageCache(imageCache)
, m_createTexture(this)
{}
@@ -364,6 +366,8 @@ void ContentLibraryView::customNotification(const AbstractView *view,
addLibMaterial(nodeList.first(), data.first().value<QPixmap>());
} else if (identifier == "add_assets_to_content_lib") {
addLibAssets(data.first().toStringList());
} else if (identifier == "add_3d_to_content_lib") {
addLib3DItem(nodeList.first());
}
}
@@ -513,10 +517,10 @@ void ContentLibraryView::addLibMaterial(const ModelNode &mat, const QPixmap &ico
qWarning() << __FUNCTION__ << "icon save failed";
// generate and save material Qml file
const QStringList depAssets = writeLibMaterialQml(mat, qml);
const QStringList depAssets = writeLibItemQml(mat, qml);
// add the material to the bundle json
QJsonObject &jsonRef = m_widget->userModel()->bundleJsonObjectRef();
QJsonObject &jsonRef = m_widget->userModel()->bundleJsonMaterialObjectRef();
QJsonObject matsObj = jsonRef.value("materials").toObject();
QJsonObject matObj;
matObj.insert("qml", qml);
@@ -554,14 +558,16 @@ void ContentLibraryView::addLibMaterial(const ModelNode &mat, const QPixmap &ico
m_widget->userModel()->addMaterial(name, qml, QUrl::fromLocalFile(fullIconPath), depAssets);
}
QStringList ContentLibraryView::writeLibMaterialQml(const ModelNode &mat, const QString &qml)
QStringList ContentLibraryView::writeLibItemQml(const ModelNode &node, const QString &qml)
{
QStringList depListIds;
auto [qmlString, assets] = modelNodeToQmlString(mat, depListIds);
auto [qmlString, assets] = modelNodeToQmlString(node, depListIds);
qmlString.prepend("import QtQuick\nimport QtQuick3D\n\n");
auto qmlPath = Utils::FilePath::fromString(Paths::bundlesPathSetting() + "/User/materials/" + qml);
QString itemType = QLatin1String(node.metaInfo().isQtQuick3DMaterial() ? "material" : "3d");
auto qmlPath = Utils::FilePath::fromString(QLatin1String("%1/User/%2/%3")
.arg(Paths::bundlesPathSetting(), itemType, qml));
auto result = qmlPath.writeFileContents(qmlString.toUtf8());
if (!result)
qWarning() << __FUNCTION__ << result.error();
@@ -584,15 +590,23 @@ QPair<QString, QSet<QString>> ContentLibraryView::modelNodeToQmlString(const Mod
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 (strcmp(pValue.typeName(), "QString") == 0 || strcmp(pValue.typeName(), "QColor") == 0) {
val = QLatin1String("\"%1\"").arg(pValue.toString());
} else if (strcmp(pValue.typeName(), "QUrl") == 0) {
val = QLatin1String("\"%1\"").arg(pValue.toString());
QString pValueStr = pValue.toString();
val = QLatin1String("\"%1\"").arg(pValueStr);
if (!pValueStr.startsWith("#"))
assets.insert(pValue.toString());
} else if (strcmp(pValue.typeName(), "QmlDesigner::Enumeration") == 0) {
val = pValue.value<QmlDesigner::Enumeration>().toString();
@@ -649,6 +663,82 @@ void ContentLibraryView::addLibAssets(const QStringList &paths)
m_widget->userModel()->addTextures(pathsInBundle);
}
void ContentLibraryView::addLib3DItem(const ModelNode &node)
{
auto bundlePath = Utils::FilePath::fromString(Paths::bundlesPathSetting() + "/User/3d/");
QString name = node.variantProperty("objectName").value().toString();
QString qml = m_widget->userModel()->getUniqueLib3DQmlName(node.id());
QString iconPath = QLatin1String("icons/%1.png").arg(node.id()); // TODO: make sure path is unique
// generate and save item Qml file
const QStringList depAssets = writeLibItemQml(node, qml);
// generate and save icon
QString qmlPath = QLatin1String("%1/User/3d/%2").arg(Paths::bundlesPathSetting(), qml);
QString fullIconPath = bundlePath.pathAppended(iconPath).toString();
genAndSaveIcon(qmlPath, fullIconPath);
// add the item to the bundle json
QJsonObject &jsonRef = m_widget->userModel()->bundleJson3DObjectRef();
QJsonArray itemsArr = jsonRef.value("items").toArray();
QJsonObject itemObj;
itemObj.insert("name", name);
itemObj.insert("qml", qml);
itemObj.insert("icon", iconPath);
QJsonArray filesArr;
for (const QString &assetPath : depAssets)
filesArr.append(assetPath);
itemObj.insert("files", filesArr);
itemsArr.append(itemObj);
jsonRef["items"] = itemsArr;
auto result = bundlePath.pathAppended("user_3d_bundle.json")
.writeFileContents(QJsonDocument(jsonRef).toJson());
if (!result)
qWarning() << __FUNCTION__ << result.error();
// copy item's assets to bundle folder
for (const QString &assetPath : depAssets) {
Utils::FilePath assetPathSource = DocumentManager::currentResourcePath().pathAppended(assetPath);
Utils::FilePath assetPathTarget = bundlePath.pathAppended(assetPath);
assetPathTarget.parentDir().ensureWritableDir();
auto result = assetPathSource.copyFile(assetPathTarget);
if (!result)
qWarning() << __FUNCTION__ << result.error();
}
m_widget->userModel()->add3DItem(name, qml, QUrl::fromLocalFile(fullIconPath), depAssets);
}
/**
* @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::genAndSaveIcon(const QString &qmlPath, const QString &iconPath)
{
m_imageCache.requestSmallImage(
Utils::PathString{qmlPath},
[&, qmlPath, iconPath](const QImage &image) {
bool iconSaved = image.save(iconPath);
if (iconSaved)
m_widget->userModel()->refresh3DSection();
else
qWarning() << "ContentLibraryView::genAndSaveIcon(): icon save failed";
},
[](ImageCache::AbortReason abortReason) {
if (abortReason == ImageCache::AbortReason::Abort)
qWarning() << "ContentLibraryView::genAndSaveIcon(): icon generation aborted, reason: Abort";
else if (abortReason == ImageCache::AbortReason::Failed)
qWarning() << "ContentLibraryView::genAndSaveIcon(): icon generation aborted, reason: Failed";
else if (abortReason == ImageCache::AbortReason::NoEntry)
qWarning() << "ContentLibraryView::genAndSaveIcon(): icon generation aborted, reason: NoEntry";
});
}
ModelNode ContentLibraryView::getBundleMaterialDefaultInstance(const TypeName &type)
{
ModelNode matLib = Utils3D::materialLibraryNode(this);

View File

@@ -3,6 +3,7 @@
#pragma once
#include <asynchronousimagecache.h>
#include <abstractview.h>
#include <createtexture.h>
#include <nodemetainfo.h>
@@ -25,7 +26,8 @@ class ContentLibraryView : public AbstractView
Q_OBJECT
public:
ContentLibraryView(ExternalDependenciesInterface &externalDependencies);
ContentLibraryView(AsynchronousImageCache &imageCache,
ExternalDependenciesInterface &externalDependencies);
~ContentLibraryView() override;
bool hasWidget() const override;
@@ -55,7 +57,9 @@ private:
void updateBundlesQuick3DVersion();
void addLibMaterial(const ModelNode &mat, const QPixmap &icon);
void addLibAssets(const QStringList &paths);
QStringList writeLibMaterialQml(const ModelNode &mat, const QString &qml);
void addLib3DItem(const ModelNode &node);
void genAndSaveIcon(const QString &qmlPath, const QString &iconPath);
QStringList writeLibItemQml(const ModelNode &node, const QString &qml);
QPair<QString, QSet<QString>> modelNodeToQmlString(const ModelNode &node, QStringList &depListIds,
int depth = 0);
@@ -79,6 +83,7 @@ private:
ContentLibraryMaterial *m_draggedBundleMaterial = nullptr;
ContentLibraryTexture *m_draggedBundleTexture = nullptr;
ContentLibraryItem *m_draggedBundleItem = nullptr;
AsynchronousImageCache &m_imageCache;
bool m_bundleMaterialAddToSelected = false;
bool m_hasQuick3DImport = false;
qint32 m_sceneId = -1;

View File

@@ -359,6 +359,14 @@ void Edit3DWidget::createContextMenu()
resetAction->setToolTip(tr("Reset all shading options for all viewports."));
m_contextMenu->addSeparator();
m_addToContentLibAction = m_contextMenu->addAction(
contextIcon(DesignerIcons::CreateIcon), // TODO: placeholder icon
tr("Add to Content Library"), [&] {
view()->emitCustomNotification("add_3d_to_content_lib", {m_contextMenuTarget});
});
m_contextMenu->addSeparator();
}
bool Edit3DWidget::isPasteAvailable() const
@@ -612,6 +620,7 @@ void Edit3DWidget::showContextMenu(const QPoint &pos, const ModelNode &modelNode
m_contextMenuPos3d = pos3d;
const bool isModel = modelNode.metaInfo().isQtQuick3DModel();
const bool isNode = modelNode.metaInfo().isQtQuick3DNode();
const bool allowAlign = view()->edit3DAction(View3DActionType::AlignCamerasToView)->action()->isEnabled();
const bool isSingleComponent = view()->hasSingleSelectedModelNode() && modelNode.isComponent();
const bool anyNodeSelected = view()->hasSelectedModelNodes();
@@ -633,6 +642,7 @@ void Edit3DWidget::showContextMenu(const QPoint &pos, const ModelNode &modelNode
m_toggleGroupAction->setEnabled(true);
m_bakeLightsAction->setVisible(view()->bakeLightsAction()->action()->isVisible());
m_bakeLightsAction->setEnabled(view()->bakeLightsAction()->action()->isEnabled());
m_addToContentLibAction->setEnabled(isNode);
if (m_view) {
int idx = m_view->activeSplit();

View File

@@ -100,6 +100,7 @@ private:
QPointer<QAction> m_selectParentAction;
QPointer<QAction> m_toggleGroupAction;
QPointer<QAction> m_wireFrameAction;
QPointer<QAction> m_addToContentLibAction;
QHash<int, QPointer<QAction>> m_matOverrideActions;
QPointer<QMenu> m_createSubMenu;
ModelNode m_contextMenuTarget;