diff --git a/src/plugins/qmldesigner/components/componentcore/viewmanager.cpp b/src/plugins/qmldesigner/components/componentcore/viewmanager.cpp index b011d9fbbf3..025ef5cb73a 100644 --- a/src/plugins/qmldesigner/components/componentcore/viewmanager.cpp +++ b/src/plugins/qmldesigner/components/componentcore/viewmanager.cpp @@ -65,7 +65,7 @@ public: externalDependencies, true) , collectionView{externalDependencies} - , contentLibraryView{externalDependencies} + , contentLibraryView{imageCache, externalDependencies} , componentView{externalDependencies} #ifndef QTC_USE_QML_DESIGNER_LITE , edit3DView{externalDependencies} diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.cpp index b8f528d6557..165f3350aa2 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.cpp +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.cpp @@ -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 ContentLibraryUserModel::getUniqueLibMaterialNameAndQml(const QString &matName) const +QPair ContentLibraryUserModel::getUniqueLibMaterialNameAndQml(const QString &defaultName) const { QTC_ASSERT(!m_bundleObjMaterial.isEmpty(), return {}); @@ -209,10 +227,9 @@ QPair 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 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 ContentLibraryUserModel::roleNames() const { static const QHash roles { @@ -242,11 +285,16 @@ QHash 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 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(); diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.h b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.h index c9cf4ad0772..d3ed51b1c95 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.h +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.h @@ -43,7 +43,8 @@ public: void setSearchText(const QString &searchText); void updateImportedState(const QStringList &importedItems); - QPair getUniqueLibMaterialNameAndQml(const QString &matName) const; + QPair 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(); diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp index a72475f6d75..b7e5b6775fa 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp @@ -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()); } 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,16 +590,24 @@ QPair> ContentLibraryView::modelNodeToQmlString(const Mod qml += indent + "id: " + (depth == 0 ? "root" : node.id()) + " \n\n"; + const QList excludedProps = {"x", "y", "z", "eulerRotation.x", "eulerRotation.y", + "eulerRotation.z", "scale.x", "scale.y", "scale.z", + "pivot.x", "pivot.y", "pivot.z"}; const QList 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()); - assets.insert(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().toString(); } else { @@ -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); diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h index 232f7dd194c..2891960f337 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -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> 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; diff --git a/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp b/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp index 6aba55f6a86..b5e6e245e5e 100644 --- a/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp +++ b/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp @@ -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(); diff --git a/src/plugins/qmldesigner/components/edit3d/edit3dwidget.h b/src/plugins/qmldesigner/components/edit3d/edit3dwidget.h index 211b044d41d..97c0469668f 100644 --- a/src/plugins/qmldesigner/components/edit3d/edit3dwidget.h +++ b/src/plugins/qmldesigner/components/edit3d/edit3dwidget.h @@ -100,6 +100,7 @@ private: QPointer m_selectParentAction; QPointer m_toggleGroupAction; QPointer m_wireFrameAction; + QPointer m_addToContentLibAction; QHash> m_matOverrideActions; QPointer m_createSubMenu; ModelNode m_contextMenuTarget;