diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibrary.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibrary.qml index d3eb29563d2..94de88fc138 100644 --- a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibrary.qml +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibrary.qml @@ -235,7 +235,6 @@ Item { cellWidth: root.thumbnailSize cellHeight: root.thumbnailSize + 20 numColumns: root.numColumns - hideHorizontalScrollBar: true searchBox: searchBox diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryItemContextMenu.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryItemContextMenu.qml index a2e72335e9f..1e4e177997b 100644 --- a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryItemContextMenu.qml +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryItemContextMenu.qml @@ -10,7 +10,8 @@ StudioControls.Menu { id: root property var targetItem: null - property bool enableRemove: false // true: adds an option to remove targetItem + property bool showRemoveAction: false // true: adds an option to remove targetItem + property bool showImportAction: false // true: adds an option to import a bundle from file readonly property bool targetAvailable: targetItem && !ContentLibraryBackend.rootView.importerRunning @@ -18,12 +19,13 @@ StudioControls.Menu { signal addToProject() signal applyToSelected(bool add) signal removeFromContentLib() + signal importBundle() function popupMenu(item = null) { root.targetItem = item - let isMaterial = root.targetItem.bundleId === "UserMaterials" + let isMaterial = item && root.targetItem.bundleId === "UserMaterials" applyToSelectedReplace.visible = isMaterial applyToSelectedAdd.visible = isMaterial @@ -64,8 +66,15 @@ StudioControls.Menu { StudioControls.MenuItem { text: qsTr("Remove from Content Library") - visible: root.enableRemove && root.targetAvailable + visible: root.showRemoveAction && root.targetAvailable height: visible ? implicitHeight : 0 onTriggered: root.removeFromContentLib() } + + StudioControls.MenuItem { + text: qsTr("Import bundle") + visible: root.showImportAction + height: visible ? implicitHeight : 0 + onTriggered: root.importBundle() + } } diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryTextureContextMenu.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryTextureContextMenu.qml index b1f690f10c9..3dba67243fe 100644 --- a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryTextureContextMenu.qml +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryTextureContextMenu.qml @@ -12,7 +12,7 @@ StudioControls.Menu { property var targetTexture: null property bool hasSceneEnv: false - property bool enableRemove: false // true: adds an option to remove targetTexture + property bool showRemoveAction: false // true: adds an option to remove targetTexture property bool canUse3D: targetTexture && ContentLibraryBackend.rootView.hasQuick3DImport && ContentLibraryBackend.rootView.hasMaterialLibrary @@ -45,7 +45,7 @@ StudioControls.Menu { StudioControls.MenuItem { text: qsTr("Remove from Content Library") - visible: root.targetTexture && root.enableRemove + visible: root.targetTexture && root.showRemoveAction height: visible ? implicitHeight : 0 onTriggered: ContentLibraryBackend.userModel.removeTexture(root.targetTexture) } diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryUserView.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryUserView.qml index f0aa94bba87..b47c01e2466 100644 --- a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryUserView.qml +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryUserView.qml @@ -8,12 +8,10 @@ import StudioControls as StudioControls import StudioTheme as StudioTheme import ContentLibraryBackend -HelperWidgets.ScrollView { +Item { id: root - clip: true - interactive: !ctxMenuItem.opened && !ctxMenuTexture.opened - && !ContentLibraryBackend.rootView.isDragging && !HelperWidgets.Controller.contextMenuOpened + property alias adsFocus: scrollView.adsFocus property real cellWidth: 100 property real cellHeight: 120 @@ -46,128 +44,152 @@ HelperWidgets.ScrollView { } } - Column { - ContentLibraryItemContextMenu { - id: ctxMenuItem + ContentLibraryItemContextMenu { + id: ctxMenuItem - enableRemove: true + showRemoveAction: true + showImportAction: true - onApplyToSelected: (add) => ContentLibraryBackend.userModel.applyToSelected(ctxMenuItem.targetItem, add) + onApplyToSelected: (add) => ContentLibraryBackend.userModel.applyToSelected(ctxMenuItem.targetItem, add) - onUnimport: root.unimport(ctxMenuItem.targetItem) - onAddToProject: ContentLibraryBackend.userModel.addToProject(ctxMenuItem.targetItem) - onRemoveFromContentLib: root.removeFromContentLib(ctxMenuItem.targetItem) + onUnimport: root.unimport(ctxMenuItem.targetItem) + onAddToProject: ContentLibraryBackend.userModel.addToProject(ctxMenuItem.targetItem) + onRemoveFromContentLib: root.removeFromContentLib(ctxMenuItem.targetItem) + onImportBundle: ContentLibraryBackend.rootView.importBundle(); + } + + ContentLibraryTextureContextMenu { + id: ctxMenuTexture + + showRemoveAction: true + hasSceneEnv: ContentLibraryBackend.texturesModel.hasSceneEnv + } + + MouseArea { + id: rootMouseArea + + anchors.fill: parent + acceptedButtons: Qt.RightButton + enabled: infoText.text === "" + + onClicked: (mouse) => { + ctxMenuItem.popupMenu() } + } - ContentLibraryTextureContextMenu { - id: ctxMenuTexture + HelperWidgets.ScrollView { + id: scrollView + anchors.fill: parent - enableRemove: true - hasSceneEnv: ContentLibraryBackend.texturesModel.hasSceneEnv - } + clip: true + interactive: !ctxMenuItem.opened && !ctxMenuTexture.opened + && !ContentLibraryBackend.rootView.isDragging && !HelperWidgets.Controller.contextMenuOpened + hideHorizontalScrollBar: true - Repeater { - id: categoryRepeater + Column { + Repeater { + id: categoryRepeater - model: ContentLibraryBackend.userModel + model: ContentLibraryBackend.userModel - delegate: HelperWidgets.Section { - id: section + delegate: HelperWidgets.Section { + id: section - width: root.width - leftPadding: StudioTheme.Values.sectionPadding - rightPadding: StudioTheme.Values.sectionPadding - topPadding: StudioTheme.Values.sectionPadding - bottomPadding: StudioTheme.Values.sectionPadding + width: root.width + leftPadding: StudioTheme.Values.sectionPadding + rightPadding: StudioTheme.Values.sectionPadding + topPadding: StudioTheme.Values.sectionPadding + bottomPadding: StudioTheme.Values.sectionPadding - caption: categoryTitle - visible: !categoryEmpty && infoText.text === "" - category: "ContentLib_User" + caption: categoryTitle + visible: !categoryEmpty && infoText.text === "" + category: "ContentLib_User" - function expandSection() { - section.expanded = true - } + function expandSection() { + section.expanded = true + } - property alias count: repeater.count + property alias count: repeater.count - onCountChanged: root.assignMaxCount() + onCountChanged: root.assignMaxCount() - Grid { - width: section.width - section.leftPadding - section.rightPadding - spacing: StudioTheme.Values.sectionGridSpacing - columns: root.numColumns + Grid { + width: section.width - section.leftPadding - section.rightPadding + spacing: StudioTheme.Values.sectionGridSpacing + columns: root.numColumns - Repeater { - id: repeater - model: categoryItems + Repeater { + id: repeater + model: categoryItems - delegate: DelegateChooser { - role: "bundleId" + delegate: DelegateChooser { + role: "bundleId" - DelegateChoice { - roleValue: "UserMaterials" - ContentLibraryItem { - width: root.cellWidth - height: root.cellHeight + DelegateChoice { + roleValue: "UserMaterials" + ContentLibraryItem { + width: root.cellWidth + height: root.cellHeight - onShowContextMenu: ctxMenuItem.popupMenu(modelData) - onAddToProject: ContentLibraryBackend.userModel.addToProject(modelData) + onShowContextMenu: ctxMenuItem.popupMenu(modelData) + onAddToProject: ContentLibraryBackend.userModel.addToProject(modelData) + } + } + DelegateChoice { + roleValue: "UserTextures" + delegate: ContentLibraryTexture { + width: root.cellWidth + height: root.cellWidth // for textures use a square size since there is no name row + + onShowContextMenu: ctxMenuTexture.popupMenu(modelData) + } + } + DelegateChoice { + roleValue: "User3D" + delegate: ContentLibraryItem { + width: root.cellWidth + height: root.cellHeight + + onShowContextMenu: ctxMenuItem.popupMenu(modelData) + onAddToProject: ContentLibraryBackend.userModel.addToProject(modelData) + } } } - DelegateChoice { - roleValue: "UserTextures" - delegate: ContentLibraryTexture { - width: root.cellWidth - height: root.cellWidth // for textures use a square size since there is no name row - onShowContextMenu: ctxMenuTexture.popupMenu(modelData) - } - } - DelegateChoice { - roleValue: "User3D" - delegate: ContentLibraryItem { - width: root.cellWidth - height: root.cellHeight - - onShowContextMenu: ctxMenuItem.popupMenu(modelData) - onAddToProject: ContentLibraryBackend.userModel.addToProject(modelData) - } - } + onCountChanged: root.assignMaxCount() } + } - onCountChanged: root.assignMaxCount() + Text { + text: qsTr("No match found."); + color: StudioTheme.Values.themeTextColor + font.pixelSize: StudioTheme.Values.baseFontSize + leftPadding: 10 + visible: infoText.text === "" && !searchBox.isEmpty() && categoryNoMatch } } + } - Text { - text: qsTr("No match found."); - color: StudioTheme.Values.themeTextColor - font.pixelSize: StudioTheme.Values.baseFontSize - leftPadding: 10 - visible: infoText.text === "" && !searchBox.isEmpty() && categoryNoMatch + Text { + id: infoText + text: { + if (!ContentLibraryBackend.rootView.isQt6Project) + qsTr("Content Library is not supported in Qt5 projects.") + else if (!ContentLibraryBackend.rootView.hasQuick3DImport) + qsTr("To use Content Library, first add the QtQuick3D module in the Components view.") + else if (!ContentLibraryBackend.rootView.hasMaterialLibrary) + qsTr("Content Library is disabled inside a non-visual component.") + else if (ContentLibraryBackend.userModel.isEmpty) + qsTr("There are no user assets in the Content Library.") + else + "" } + color: StudioTheme.Values.themeTextColor + font.pixelSize: StudioTheme.Values.baseFontSize + topPadding: 10 + leftPadding: 10 + visible: infoText.text !== "" } } - - Text { - id: infoText - text: { - if (!ContentLibraryBackend.rootView.isQt6Project) - qsTr("Content Library is not supported in Qt5 projects.") - else if (!ContentLibraryBackend.rootView.hasQuick3DImport) - qsTr("To use Content Library, first add the QtQuick3D module in the Components view.") - else if (!ContentLibraryBackend.rootView.hasMaterialLibrary) - qsTr("Content Library is disabled inside a non-visual component.") - else if (ContentLibraryBackend.userModel.isEmpty) - qsTr("There are no user assets in the Content Library.") - else - "" - } - color: StudioTheme.Values.themeTextColor - font.pixelSize: StudioTheme.Values.baseFontSize - topPadding: 10 - leftPadding: 10 - visible: infoText.text !== "" - } } } diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp index 422acc19508..b4e7a802be3 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp @@ -100,6 +100,9 @@ WidgetInfo ContentLibraryView::widgetInfo() m_widget->environmentsModel()->setHasSceneEnv(sceneEnvExists); }); + connect(m_widget, &ContentLibraryWidget::importBundle, this, + &ContentLibraryView::importBundleToContentLib); + connect(m_widget->materialsModel(), &ContentLibraryMaterialsModel::applyToSelectedTriggered, this, @@ -393,8 +396,8 @@ void ContentLibraryView::customNotification(const AbstractView *view, exportLibItem(nodeList.first()); } else if (identifier == "export_material_as_bundle") { exportLibItem(nodeList.first(), data.first().value()); - } else if (identifier == "import_bundle") { - importBundle(); + } else if (identifier == "import_bundle_to_project") { + importBundleToProject(); } } @@ -1023,7 +1026,7 @@ void ContentLibraryView::exportLibItem(const ModelNode &node, const QPixmap &ico addIconAndCloseZip(iconPixmap); } -void ContentLibraryView::importBundle() +void ContentLibraryView::importBundleToContentLib() { QString importPath = getImportPath(); if (importPath.isEmpty()) @@ -1049,6 +1052,7 @@ void ContentLibraryView::importBundle() bool isMat = isMaterialBundle(bundleId); QString bundleFolderName = isMat ? QLatin1String("materials") : QLatin1String("3d"); + auto bundlePath = Utils::FilePath::fromString(QLatin1String("%1/User/%3/") .arg(Paths::bundlesPathSetting(), bundleFolderName)); @@ -1096,6 +1100,7 @@ void ContentLibraryView::importBundle() m_widget->userModel()->addItem(bundleId, name, qml, iconUrl, files); } + m_widget->userModel()->refreshSection(bundleId); zipReader.close(); @@ -1107,6 +1112,85 @@ void ContentLibraryView::importBundle() QTC_ASSERT_EXPECTED(result,); } +void ContentLibraryView::importBundleToProject() +{ + QString importPath = getImportPath(); + if (importPath.isEmpty()) + return; + + auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils(); + + ZipReader zipReader(importPath); + + QByteArray bundleJsonContent = zipReader.fileData(Constants::BUNDLE_JSON_FILENAME); + QTC_ASSERT(!bundleJsonContent.isEmpty(), return); + + const QJsonObject importedJsonObj = QJsonDocument::fromJson(bundleJsonContent).object(); + const QJsonArray importedItemsArr = importedJsonObj.value("items").toArray(); + QTC_ASSERT(!importedItemsArr.isEmpty(), return); + + QString bundleVersion = importedJsonObj.value("version").toString(); + bool bundleVersionOk = !bundleVersion.isEmpty() && bundleVersion == BUNDLE_VERSION; + if (!bundleVersionOk) { + QMessageBox::warning(m_widget, tr("Unsupported bundle file"), + tr("The chosen bundle was created with an incompatible version of Qt Design Studio")); + return; + } + QString bundleId = importedJsonObj.value("id").toString(); + + QTemporaryDir tempDir; + QTC_ASSERT(tempDir.isValid(), return); + auto bundlePath = Utils::FilePath::fromString(tempDir.path()); + + const QStringList existingQmls = Utils::transform(compUtils.userBundlePath(bundleId) + .dirEntries(QDir::Files), [](const Utils::FilePath &path) { + return path.fileName(); + }); + + for (const QJsonValueConstRef &itemRef : importedItemsArr) { + QJsonObject itemObj = itemRef.toObject(); + QString qml = itemObj.value("qml").toString(); + + // confirm overwrite if an item with same name exists + if (existingQmls.contains(qml)) { + QMessageBox::StandardButton reply = QMessageBox::question(m_widget, tr("Component Exists"), + tr("A component with the same name '%1' already " + "exists in the project, are you sure " + "you want to overwrite it?") + .arg(qml), QMessageBox::Yes | QMessageBox::No); + if (reply == QMessageBox::No) + continue; + + // 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(); + + // copy files + QStringList allFiles = files; + allFiles << qml << icon; + for (const QString &file : std::as_const(allFiles)) { + Utils::FilePath filePath = bundlePath.pathAppended(file); + filePath.parentDir().ensureWritableDir(); + QTC_ASSERT_EXPECTED(filePath.writeFileContents(zipReader.fileData(file)),); + } + + QString typePrefix = compUtils.userBundleType(bundleId); + TypeName type = QLatin1String("%1.%2").arg(typePrefix, qml.chopped(4)).toLatin1(); + + QString err = m_widget->importer()->importComponent(bundlePath.toFSPathString(), type, qml, files); + + if (err.isEmpty()) + m_widget->setImporterRunning(true); + else + qWarning() << __FUNCTION__ << err; + } + + zipReader.close(); +} + /** * @brief Generates an icon image from a qml component * @param qmlPath path to the qml component file to be rendered diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h index 724e94189b9..edd7769f5ee 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h @@ -66,7 +66,8 @@ private: void exportLib3DComponent(const ModelNode &node); void addLibItem(const ModelNode &node, const QPixmap &iconPixmap = {}); void exportLibItem(const ModelNode &node, const QPixmap &iconPixmap = {}); - void importBundle(); + void importBundleToContentLib(); + void importBundleToProject(); void getImageFromCache(const QString &qmlPath, std::function successCallback); QString getExportPath(const ModelNode &node) const; diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.h b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.h index 8e96d9d2f3b..6ead35f72a1 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.h +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.h @@ -108,6 +108,7 @@ signals: void isQt6ProjectChanged(); void importerRunningChanged(); void hasModelSelectionChanged(); + void importBundle(); protected: bool eventFilter(QObject *obj, QEvent *event) override; diff --git a/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp b/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp index f6f514431a9..7a98b21cbd4 100644 --- a/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp +++ b/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp @@ -373,7 +373,7 @@ void Edit3DWidget::createContextMenu() m_importBundleAction = m_contextMenu->addAction( contextIcon(DesignerIcons::CreateIcon), // TODO: placeholder icon tr("Import Components"), [&] { - view()->emitCustomNotification("import_bundle"); // To ContentLibrary + view()->emitCustomNotification("import_bundle_to_project"); // To ContentLibrary }); m_exportBundleAction = m_contextMenu->addAction( diff --git a/src/plugins/qmldesigner/components/materialbrowser/materialbrowserwidget.cpp b/src/plugins/qmldesigner/components/materialbrowser/materialbrowserwidget.cpp index 86fb13a7a33..87b24306a8a 100644 --- a/src/plugins/qmldesigner/components/materialbrowser/materialbrowserwidget.cpp +++ b/src/plugins/qmldesigner/components/materialbrowser/materialbrowserwidget.cpp @@ -374,7 +374,7 @@ void MaterialBrowserWidget::addMaterialToContentLibrary() void MaterialBrowserWidget::importMaterial() { ModelNode mat = m_materialBrowserModel->selectedMaterial(); - m_materialBrowserView->emitCustomNotification("import_bundle"); // to ContentLibrary + m_materialBrowserView->emitCustomNotification("import_bundle_to_project"); // to ContentLibrary } void MaterialBrowserWidget::exportMaterial() { diff --git a/src/plugins/qmldesigner/designercore/generatedcomponentutils.cpp b/src/plugins/qmldesigner/designercore/generatedcomponentutils.cpp index 9d67d1cbf82..75f4cbf4aa2 100644 --- a/src/plugins/qmldesigner/designercore/generatedcomponentutils.cpp +++ b/src/plugins/qmldesigner/designercore/generatedcomponentutils.cpp @@ -131,6 +131,25 @@ Utils::FilePath GeneratedComponentUtils::effectBundlePath() const return basePath.resolvePath(QLatin1String(Constants::COMPONENT_BUNDLES_EFFECT_BUNDLE_TYPE)); } +Utils::FilePath GeneratedComponentUtils::userBundlePath(const QString &bundleId) const +{ + Utils::FilePath basePath = componentBundlesBasePath(); + if (basePath.isEmpty()) + return {}; + + if (bundleId == userMaterialsBundleId()) + return basePath.pathAppended(Constants::COMPONENT_BUNDLES_USER_MATERIAL_BUNDLE_TYPE); + + if (bundleId == userEffectsBundleId()) + return basePath.pathAppended(Constants::COMPONENT_BUNDLES_USER_EFFECT_BUNDLE_TYPE); + + if (bundleId == user3DBundleId()) + return basePath.pathAppended(Constants::COMPONENT_BUNDLES_USER_3D_BUNDLE_TYPE); + + qWarning() << __FUNCTION__ << "no bundleType for bundleId:" << bundleId; + return {}; +} + Utils::FilePath GeneratedComponentUtils::projectModulePath(bool generateIfNotExists) const { using Utils::FilePath; diff --git a/src/plugins/qmldesigner/designercore/generatedcomponentutils.h b/src/plugins/qmldesigner/designercore/generatedcomponentutils.h index df15d252cc3..f35dee554a7 100644 --- a/src/plugins/qmldesigner/designercore/generatedcomponentutils.h +++ b/src/plugins/qmldesigner/designercore/generatedcomponentutils.h @@ -23,6 +23,7 @@ public: Utils::FilePath import3dBasePath() const; Utils::FilePath materialBundlePath() const; Utils::FilePath effectBundlePath() const; + Utils::FilePath userBundlePath(const QString &bundleId) const; Utils::FilePath projectModulePath(bool generateIfNotExists = false) const; bool isImport3dPath(const QString &path) const;