From bc5628afca0c0716642cd69679d6b51acfa60316 Mon Sep 17 00:00:00 2001 From: Mahmoud Badri Date: Thu, 21 Mar 2024 12:47:09 +0200 Subject: QmlDesigner: Add content library user materials bundle Fixes: QDS-12389 Change-Id: Icec1b06c57e0eaa4ff444e3143d3cba0803c8dd1 Reviewed-by: Miikka Heikkinen --- .../contentLibraryQmlSource/ContentLibrary.qml | 46 ++- .../ContentLibraryMaterial.qml | 13 +- .../ContentLibraryMaterialContextMenu.qml | 13 +- .../ContentLibraryMaterialsView.qml | 25 +- .../ContentLibraryTexturesView.qml | 3 - .../ContentLibraryUserView.qml | 151 ++++++++++ .../UnimportBundleMaterialDialog.qml | 23 +- src/plugins/qmldesigner/CMakeLists.txt | 1 + .../contentlibrary/contentlibrarymaterial.h | 3 +- .../contentlibrary/contentlibraryusermodel.cpp | 308 +++++++++++++++++++++ .../contentlibrary/contentlibraryusermodel.h | 118 ++++++++ .../contentlibrary/contentlibraryview.cpp | 81 ++++++ .../components/contentlibrary/contentlibraryview.h | 2 + .../contentlibrary/contentlibrarywidget.cpp | 17 +- .../contentlibrary/contentlibrarywidget.h | 4 + 15 files changed, 761 insertions(+), 47 deletions(-) create mode 100644 share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryUserView.qml create mode 100644 src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.cpp create mode 100644 src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.h diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibrary.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibrary.qml index c6db8425ff2..2c98b58adc7 100644 --- a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibrary.qml +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibrary.qml @@ -23,6 +23,7 @@ Item { texturesView.closeContextMenu() environmentsView.closeContextMenu() effectsView.closeContextMenu() + userView.closeContextMenu() HelperWidgets.Controller.closeContextMenu() } @@ -113,10 +114,18 @@ Item { id: tabBar width: parent.width height: StudioTheme.Values.toolbarHeight - tabsModel: [{name: qsTr("Materials"), icon: StudioTheme.Constants.material_medium}, - {name: qsTr("Textures"), icon: StudioTheme.Constants.textures_medium}, - {name: qsTr("Environments"), icon: StudioTheme.Constants.languageList_medium}, - {name: qsTr("Effects"), icon: StudioTheme.Constants.effects}] + + Component.onCompleted: { + var tabs = [ + { name: qsTr("Materials"), icon: StudioTheme.Constants.material_medium }, + { name: qsTr("Textures"), icon: StudioTheme.Constants.textures_medium }, + { name: qsTr("Environments"), icon: StudioTheme.Constants.languageList_medium }, + { name: qsTr("Effects"), icon: StudioTheme.Constants.effects } + ]; + if (ContentLibraryBackend.rootView.userBundleEnabled()) + tabs.push({ name: qsTr("User Assets"), icon: StudioTheme.Constants.effects }); + tabBar.tabsModel = tabs; + } } } } @@ -148,7 +157,8 @@ Item { onUnimport: (bundleMat) => { confirmUnimportDialog.targetBundleItem = bundleMat - confirmUnimportDialog.targetBundleType = "material" + confirmUnimportDialog.targetBundleLabel = "material" + confirmUnimportDialog.targetBundleModel = ContentLibraryBackend.materialsModel confirmUnimportDialog.open() } @@ -208,7 +218,31 @@ Item { onUnimport: (bundleItem) => { confirmUnimportDialog.targetBundleItem = bundleItem - confirmUnimportDialog.targetBundleType = "effect" + confirmUnimportDialog.targetBundleLabel = "effect" + confirmUnimportDialog.targetBundleModel = ContentLibraryBackend.effectsModel + confirmUnimportDialog.open() + } + + onCountChanged: root.responsiveResize(stackLayout.width, stackLayout.height) + } + + ContentLibraryUserView { + id: userView + + adsFocus: root.adsFocus + width: root.width + + cellWidth: root.thumbnailSize + cellHeight: root.thumbnailSize + 20 + numColumns: root.numColumns + hideHorizontalScrollBar: true + + searchBox: searchBox + + onUnimport: (bundleItem) => { + confirmUnimportDialog.targetBundleItem = bundleItem + confirmUnimportDialog.targetBundleLabel = "material" + confirmUnimportDialog.targetBundleModel = ContentLibraryBackend.userModel confirmUnimportDialog.open() } diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterial.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterial.qml index 93b226d6caf..0e9fc4903eb 100644 --- a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterial.qml +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterial.qml @@ -12,12 +12,15 @@ import WebFetcher Item { id: root - signal showContextMenu() - // Download states: "" (ie default, not downloaded), "unavailable", "downloading", "downloaded", // "failed" property string downloadState: modelData.isDownloaded() ? "downloaded" : "" + property bool importerRunning: false + + signal showContextMenu() + signal addToProject() + visible: modelData.bundleMaterialVisible MouseArea { @@ -29,7 +32,7 @@ Item { acceptedButtons: Qt.LeftButton | Qt.RightButton onPressed: (mouse) => { - if (mouse.button === Qt.LeftButton && !materialsModel.importerRunning) { + if (mouse.button === Qt.LeftButton && !root.importerRunning) { if (root.downloadState === "downloaded") ContentLibraryBackend.rootView.startDragMaterial(modelData, mapToGlobal(mouse.x, mouse.y)) } else if (mouse.button === Qt.RightButton && root.downloadState === "downloaded") { @@ -96,12 +99,12 @@ Item { pressColor: Qt.hsla(c.hslHue, c.hslSaturation, c.hslLightness, .4) anchors.right: img.right anchors.bottom: img.bottom - enabled: !ContentLibraryBackend.materialsModel.importerRunning + enabled: !root.importerRunning visible: root.downloadState === "downloaded" && (containsMouse || mouseArea.containsMouse) onClicked: { - ContentLibraryBackend.materialsModel.addToProject(modelData) + root.addToProject() } } diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterialContextMenu.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterialContextMenu.qml index ca3a05bdd12..b67ec311ef0 100644 --- a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterialContextMenu.qml +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterialContextMenu.qml @@ -15,8 +15,9 @@ StudioControls.Menu { readonly property bool targetAvailable: targetMaterial && !importerRunning - signal unimport(var bundleMat); - signal addToProject(var bundleMat) + signal unimport(); + signal addToProject() + signal applyToSelected(bool add) function popupMenu(targetMaterial = null) { @@ -29,13 +30,13 @@ StudioControls.Menu { StudioControls.MenuItem { text: qsTr("Apply to selected (replace)") enabled: root.targetAvailable && root.hasModelSelection - onTriggered: materialsModel.applyToSelected(root.targetMaterial, false) + onTriggered: root.applyToSelected(false) } StudioControls.MenuItem { text: qsTr("Apply to selected (add)") enabled: root.targetAvailable && root.hasModelSelection - onTriggered: materialsModel.applyToSelected(root.targetMaterial, true) + onTriggered: root.applyToSelected(true) } StudioControls.MenuSeparator {} @@ -45,7 +46,7 @@ StudioControls.Menu { text: qsTr("Add an instance to project") onTriggered: { - root.addToProject(root.targetMaterial) + root.addToProject() } } @@ -53,6 +54,6 @@ StudioControls.Menu { enabled: root.targetAvailable && root.targetMaterial.bundleMaterialImported text: qsTr("Remove from project") - onTriggered: root.unimport(root.targetMaterial) + onTriggered: root.unimport() } } diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterialsView.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterialsView.qml index c21baf4c580..9a0e33b8e5e 100644 --- a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterialsView.qml +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterialsView.qml @@ -27,8 +27,6 @@ HelperWidgets.ScrollView { root.count = c } - property var currMaterialItem: null - property var rootItem: null property var materialsModel: ContentLibraryBackend.materialsModel required property var searchBox @@ -51,17 +49,19 @@ HelperWidgets.ScrollView { ContentLibraryMaterialContextMenu { id: ctxMenu - hasModelSelection: materialsModel.hasModelSelection - importerRunning: materialsModel.importerRunning + hasModelSelection: root.materialsModel.hasModelSelection + importerRunning: root.materialsModel.importerRunning - onUnimport: (bundleMat) => root.unimport(bundleMat) - onAddToProject: (bundleMat) => materialsModel.addToProject(bundleMat) + onApplyToSelected: (add) => root.materialsModel.applyToSelected(ctxMenu.targetMaterial, add) + + onUnimport: root.unimport(ctxMenu.targetMaterial) + onAddToProject: root.materialsModel.addToProject(ctxMenu.targetMaterial) } Repeater { id: categoryRepeater - model: materialsModel + model: root.materialsModel delegate: HelperWidgets.Section { id: section @@ -73,7 +73,7 @@ HelperWidgets.ScrollView { bottomPadding: StudioTheme.Values.sectionPadding caption: bundleCategoryName - visible: bundleCategoryVisible && !materialsModel.isEmpty + visible: bundleCategoryVisible && !root.materialsModel.isEmpty expanded: bundleCategoryExpanded expandOnClick: false category: "ContentLib_Mat" @@ -103,7 +103,10 @@ HelperWidgets.ScrollView { width: root.cellWidth height: root.cellHeight + importerRunning: root.materialsModel.importerRunning + onShowContextMenu: ctxMenu.popupMenu(modelData) + onAddToProject: root.materialsModel.addToProject(modelData) } onCountChanged: root.assignMaxCount() @@ -115,13 +118,13 @@ HelperWidgets.ScrollView { Text { id: infoText text: { - if (!materialsModel.matBundleExists) + if (!root.materialsModel.matBundleExists) qsTr("No materials available. Make sure you have internet connection.") else if (!ContentLibraryBackend.rootView.isQt6Project) qsTr("Content Library materials are 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 (!materialsModel.hasRequiredQuick3DImport) + else if (!root.materialsModel.hasRequiredQuick3DImport) qsTr("To use Content Library, version 6.3 or later of the QtQuick3D module is required.") else if (!ContentLibraryBackend.rootView.hasMaterialLibrary) qsTr("Content Library is disabled inside a non-visual component.") @@ -134,7 +137,7 @@ HelperWidgets.ScrollView { font.pixelSize: StudioTheme.Values.baseFontSize topPadding: 10 leftPadding: 10 - visible: materialsModel.isEmpty + visible: root.materialsModel.isEmpty } } } diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryTexturesView.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryTexturesView.qml index 1fac9f2234e..617b724e664 100644 --- a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryTexturesView.qml +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryTexturesView.qml @@ -27,9 +27,6 @@ HelperWidgets.ScrollView { root.count = c } - property var currMaterialItem: null - property var rootItem: null - required property var searchBox required property var model required property string sectionCategory diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryUserView.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryUserView.qml new file mode 100644 index 00000000000..d3d1dbad92d --- /dev/null +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryUserView.qml @@ -0,0 +1,151 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls +import StudioTheme as StudioTheme +import ContentLibraryBackend + +HelperWidgets.ScrollView { + id: root + + clip: true + interactive: !ctxMenu.opened && !ContentLibraryBackend.rootView.isDragging + && !HelperWidgets.Controller.contextMenuOpened + + property real cellWidth: 100 + property real cellHeight: 120 + property int numColumns: 4 + + property int count: 0 + function assignMaxCount() { + let c = 0 + for (let i = 0; i < categoryRepeater.count; ++i) + c = Math.max(c, categoryRepeater.itemAt(i)?.count ?? 0) + + root.count = c + } + + required property var searchBox + + signal unimport(var bundleItem); + + function closeContextMenu() { + ctxMenu.close() + } + + function expandVisibleSections() { + for (let i = 0; i < categoryRepeater.count; ++i) { + let cat = categoryRepeater.itemAt(i) + if (cat.visible && !cat.expanded) + cat.expandSection() + } + } + + Column { + ContentLibraryMaterialContextMenu { + id: ctxMenu + + hasModelSelection: ContentLibraryBackend.userModel.hasModelSelection + importerRunning: ContentLibraryBackend.userModel.importerRunning + + onApplyToSelected: (add) => ContentLibraryBackend.userModel.applyToSelected(ctxMenu.targetMaterial, add) + + onUnimport: root.unimport(ctxMenu.targetMaterial) + onAddToProject: ContentLibraryBackend.userModel.addToProject(ctxMenu.targetMaterial) + } + + Repeater { + id: categoryRepeater + + model: ContentLibraryBackend.userModel + + delegate: HelperWidgets.Section { + id: section + + width: root.width + leftPadding: StudioTheme.Values.sectionPadding + rightPadding: StudioTheme.Values.sectionPadding + topPadding: StudioTheme.Values.sectionPadding + bottomPadding: StudioTheme.Values.sectionPadding + + caption: categoryName + visible: categoryVisible + expanded: categoryExpanded + expandOnClick: false + category: "ContentLib_User" + + onToggleExpand: categoryExpanded = !categoryExpanded + onExpand: categoryExpanded = true + onCollapse: categoryExpanded = false + + function expandSection() { + categoryExpanded = true + } + + property alias count: repeater.count + + onCountChanged: root.assignMaxCount() + + property int numVisibleItem: 1 // initially, the tab is invisible so this will be 0 + + Grid { + width: section.width - section.leftPadding - section.rightPadding + spacing: StudioTheme.Values.sectionGridSpacing + columns: root.numColumns + + Repeater { + id: repeater + model: categoryItems + + delegate: ContentLibraryMaterial { + width: root.cellWidth + height: root.cellHeight + + importerRunning: ContentLibraryBackend.userModel.importerRunning + + onShowContextMenu: ctxMenu.popupMenu(modelData) + onAddToProject: ContentLibraryBackend.userModel.addToProject(modelData) + + onVisibleChanged: { + section.numVisibleItem += visible ? 1 : -1 + } + } + + onCountChanged: root.assignMaxCount() + } + } + + Text { + text: qsTr("No match found."); + color: StudioTheme.Values.themeTextColor + font.pixelSize: StudioTheme.Values.baseFontSize + leftPadding: 10 + visible: !searchBox.isEmpty() && section.numVisibleItem === 0 + } + } + } + + Text { + id: infoText + text: { + if (!ContentLibraryBackend.effectsModel.bundleExists) + qsTr("User bundle couldn't be found.") + else 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 + "" + } + color: StudioTheme.Values.themeTextColor + font.pixelSize: StudioTheme.Values.baseFontSize + topPadding: 10 + leftPadding: 10 + visible: ContentLibraryBackend.effectsModel.isEmpty + } + } +} diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/UnimportBundleMaterialDialog.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/UnimportBundleMaterialDialog.qml index 48be045d8bd..4385e3bf82e 100644 --- a/share/qtcreator/qmldesigner/contentLibraryQmlSource/UnimportBundleMaterialDialog.qml +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/UnimportBundleMaterialDialog.qml @@ -12,24 +12,27 @@ import ContentLibraryBackend StudioControls.Dialog { id: root - title: qsTr("Bundle material might be in use") + property var targetBundleItem + property var targetBundleLabel // "effect" or "material" + property var targetBundleModel + + title: qsTr("Bundle %1 might be in use").arg(root.targetBundleLabel) anchors.centerIn: parent closePolicy: Popup.CloseOnEscape implicitWidth: 300 modal: true - property var targetBundleType // "effect" or "material" - property var targetBundleItem + onOpened: warningText.forceActiveFocus() contentItem: Column { spacing: 20 width: parent.width Text { - id: folderNotEmpty + id: warningText - text: qsTr("If the %1 you are removing is in use, it might cause the project to malfunction.\n\nAre you sure you want to remove the %1?") - .arg(root.targetBundleType) + text: qsTr("If the %1 you are removing is in use, it might cause the project to malfunction.\n\nAre you sure you want to remove it?") + .arg(root.targetBundleLabel) color: StudioTheme.Values.themeTextColor wrapMode: Text.WordWrap anchors.right: parent.right @@ -49,11 +52,7 @@ StudioControls.Dialog { text: qsTr("Remove") onClicked: { - if (root.targetBundleType === "material") - ContentLibraryBackend.materialsModel.removeFromProject(root.targetBundleItem) - else if (root.targetBundleType === "effect") - ContentLibraryBackend.effectsModel.removeFromProject(root.targetBundleItem) - + root.targetBundleModel.removeFromProject(root.targetBundleItem) root.accept() } } @@ -64,6 +63,4 @@ StudioControls.Dialog { } } } - - onOpened: folderNotEmpty.forceActiveFocus() } diff --git a/src/plugins/qmldesigner/CMakeLists.txt b/src/plugins/qmldesigner/CMakeLists.txt index 01f03cc2712..1c7bebffa32 100644 --- a/src/plugins/qmldesigner/CMakeLists.txt +++ b/src/plugins/qmldesigner/CMakeLists.txt @@ -826,6 +826,7 @@ extend_qtc_plugin(QmlDesigner contentlibraryeffect.cpp contentlibraryeffect.h contentlibraryeffectscategory.cpp contentlibraryeffectscategory.h contentlibraryeffectsmodel.cpp contentlibraryeffectsmodel.h + contentlibraryusermodel.cpp contentlibraryusermodel.h ) extend_qtc_plugin(QmlDesigner diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterial.h b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterial.h index f546ea98cd3..90d8468fa16 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterial.h +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterial.h @@ -3,9 +3,8 @@ #pragma once -#include "qmldesignercorelib_global.h" +#include "nodeinstanceglobal.h" -#include #include #include diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.cpp new file mode 100644 index 00000000000..56ba8cc6c73 --- /dev/null +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.cpp @@ -0,0 +1,308 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "contentlibraryusermodel.h" + +#include "contentlibrarybundleimporter.h" +#include "contentlibrarymaterial.h" +#include "contentlibrarymaterialscategory.h" +#include "contentlibrarywidget.h" + +#include +#include "qmldesignerconstants.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace QmlDesigner { + +ContentLibraryUserModel::ContentLibraryUserModel(ContentLibraryWidget *parent) + : QAbstractListModel(parent) + , m_widget(parent) +{ + m_userCategories = {tr("Materials")/*, tr("Textures"), tr("3D"), tr("Effects"), tr("2D components")*/}; // TODO + + loadUserBundle(); +} + +int ContentLibraryUserModel::rowCount(const QModelIndex &) const +{ + return m_userCategories.size(); +} + +QVariant ContentLibraryUserModel::data(const QModelIndex &index, int role) const +{ + QTC_ASSERT(index.isValid() && index.row() < m_userCategories.size(), return {}); + QTC_ASSERT(roleNames().contains(role), return {}); + + if (role == NameRole) + return m_userCategories.at(index.row()); + + if (role == ItemsRole) { + if (index.row() == 0) + return QVariant::fromValue(m_userMaterials); + if (index.row() == 1) + return QVariant::fromValue(m_userTextures); + if (index.row() == 2) + return QVariant::fromValue(m_user3DItems); + if (index.row() == 3) + return QVariant::fromValue(m_userEffects); + } + + if (role == VisibleRole) + return true; // TODO + + if (role == ExpandedRole) + return true; // TODO + + return {}; +} + +bool ContentLibraryUserModel::isValidIndex(int idx) const +{ + return idx > -1 && idx < rowCount(); +} + +void ContentLibraryUserModel::updateIsEmpty() +{ + bool anyMatVisible = Utils::anyOf(m_userMaterials, [&](ContentLibraryMaterial *mat) { + return mat->visible(); + }); + + bool newEmpty = !anyMatVisible || !m_widget->hasMaterialLibrary() || !hasRequiredQuick3DImport(); + + if (newEmpty != m_isEmpty) { + m_isEmpty = newEmpty; + emit isEmptyChanged(); + } +} + +QHash ContentLibraryUserModel::roleNames() const +{ + static const QHash roles { + {NameRole, "categoryName"}, + {VisibleRole, "categoryVisible"}, + {ExpandedRole, "categoryExpanded"}, + {ItemsRole, "categoryItems"} + }; + return roles; +} + +void ContentLibraryUserModel::createImporter(const QString &bundlePath, const QString &bundleId, + const QStringList &sharedFiles) +{ + m_importer = new Internal::ContentLibraryBundleImporter(bundlePath, bundleId, sharedFiles); +#ifdef QDS_USE_PROJECTSTORAGE + connect(m_importer, + &Internal::ContentLibraryBundleImporter::importFinished, + this, + [&](const QmlDesigner::TypeName &typeName) { + m_importerRunning = false; + emit importerRunningChanged(); + if (typeName.size()) + emit bundleMaterialImported(typeName); + }); +#else + connect(m_importer, + &Internal::ContentLibraryBundleImporter::importFinished, + this, + [&](const QmlDesigner::NodeMetaInfo &metaInfo) { + m_importerRunning = false; + emit importerRunningChanged(); + if (metaInfo.isValid()) + emit bundleMaterialImported(metaInfo); + }); +#endif + + connect(m_importer, &Internal::ContentLibraryBundleImporter::unimportFinished, this, + [&](const QmlDesigner::NodeMetaInfo &metaInfo) { + Q_UNUSED(metaInfo) + m_importerRunning = false; + emit importerRunningChanged(); + emit bundleMaterialUnimported(metaInfo); + }); + + resetModel(); + updateIsEmpty(); +} + +void ContentLibraryUserModel::loadUserBundle() +{ + if (m_matBundleExists) + return; + + QDir bundleDir{Paths::bundlesPathSetting() + "/User/materials"}; + + if (m_bundleObj.isEmpty()) { + QFile matsJsonFile(bundleDir.filePath("user_materials_bundle.json")); + + if (!matsJsonFile.open(QIODevice::ReadOnly)) { + qWarning("Couldn't open user_materials_bundle.json"); + return; + } + + QJsonDocument matBundleJsonDoc = QJsonDocument::fromJson(matsJsonFile.readAll()); + if (matBundleJsonDoc.isNull()) { + qWarning("Invalid user_materials_bundle.json file"); + return; + } else { + m_bundleObj = matBundleJsonDoc.object(); + } + } + + QString bundleId = m_bundleObj.value("id").toString(); + + // parse materials + const QJsonObject matsObj = m_bundleObj.value("materials").toObject(); + const QStringList materialNames = matsObj.keys(); + for (const QString &matName : materialNames) { + const QJsonObject matObj = matsObj.value(matName).toObject(); + + QStringList files; + const QJsonArray assetsArr = matObj.value("files").toArray(); + for (const auto /*QJson{Const,}ValueRef*/ &asset : assetsArr) + files.append(asset.toString()); + + QUrl icon = QUrl::fromLocalFile(bundleDir.filePath(matObj.value("icon").toString())); + QString qml = matObj.value("qml").toString(); + + TypeName type = QLatin1String("%1.%2.%3").arg( + QLatin1String(Constants::COMPONENT_BUNDLES_FOLDER).mid(1), + bundleId, + qml.chopped(4)).toLatin1(); // chopped(4): remove .qml + + auto userMat = new ContentLibraryMaterial(this, matName, qml, type, icon, files, + bundleDir.path(), ""); + + m_userMaterials.append(userMat); + } + + QStringList sharedFiles; + const QJsonArray sharedFilesArr = m_bundleObj.value("sharedFiles").toArray(); + for (const auto /*QJson{Const,}ValueRef*/ &file : sharedFilesArr) + sharedFiles.append(file.toString()); + + createImporter(bundleDir.path(), bundleId, sharedFiles); + + m_matBundleExists = true; + emit matBundleExistsChanged(); +} + +bool ContentLibraryUserModel::hasRequiredQuick3DImport() const +{ + return m_widget->hasQuick3DImport() && m_quick3dMajorVersion == 6 && m_quick3dMinorVersion >= 3; +} + +bool ContentLibraryUserModel::matBundleExists() const +{ + return m_matBundleExists; +} + +Internal::ContentLibraryBundleImporter *ContentLibraryUserModel::bundleImporter() const +{ + return m_importer; +} + +void ContentLibraryUserModel::setSearchText(const QString &searchText) +{ + QString lowerSearchText = searchText.toLower(); + + if (m_searchText == lowerSearchText) + return; + + m_searchText = lowerSearchText; + + for (ContentLibraryMaterial *mat : std::as_const(m_userMaterials)) + mat->filter(m_searchText); + + updateIsEmpty(); +} + +void ContentLibraryUserModel::updateImportedState(const QStringList &importedMats) +{ + bool changed = false; + + for (ContentLibraryMaterial *mat : std::as_const(m_userMaterials)) + changed |= mat->setImported(importedMats.contains(mat->qml().chopped(4))); + + if (changed) + resetModel(); +} + +void ContentLibraryUserModel::setQuick3DImportVersion(int major, int minor) +{ + bool oldRequiredImport = hasRequiredQuick3DImport(); + + m_quick3dMajorVersion = major; + m_quick3dMinorVersion = minor; + + bool newRequiredImport = hasRequiredQuick3DImport(); + + if (oldRequiredImport == newRequiredImport) + return; + + emit hasRequiredQuick3DImportChanged(); + + updateIsEmpty(); +} + +void ContentLibraryUserModel::resetModel() +{ + beginResetModel(); + endResetModel(); +} + +void ContentLibraryUserModel::applyToSelected(ContentLibraryMaterial *mat, bool add) +{ + emit applyToSelectedTriggered(mat, add); +} + +void ContentLibraryUserModel::addToProject(ContentLibraryMaterial *mat) +{ + QString err = m_importer->importComponent(mat->qml(), mat->files()); + + if (err.isEmpty()) { + m_importerRunning = true; + emit importerRunningChanged(); + } else { + qWarning() << __FUNCTION__ << err; + } +} + +void ContentLibraryUserModel::removeFromProject(ContentLibraryMaterial *mat) +{ + emit bundleMaterialAboutToUnimport(mat->type()); + + QString err = m_importer->unimportComponent(mat->qml()); + + if (err.isEmpty()) { + m_importerRunning = true; + emit importerRunningChanged(); + } else { + qWarning() << __FUNCTION__ << err; + } +} + +bool ContentLibraryUserModel::hasModelSelection() const +{ + return m_hasModelSelection; +} + +void ContentLibraryUserModel::setHasModelSelection(bool b) +{ + if (b == m_hasModelSelection) + return; + + m_hasModelSelection = b; + emit hasModelSelectionChanged(); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.h b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.h new file mode 100644 index 00000000000..72c535e4014 --- /dev/null +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.h @@ -0,0 +1,118 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "nodemetainfo.h" + +#include +#include + +namespace QmlDesigner { + +class ContentLibraryEffect; +class ContentLibraryMaterial; +class ContentLibraryTexture; +class ContentLibraryWidget; + +namespace Internal { +class ContentLibraryBundleImporter; +} + +class ContentLibraryUserModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(bool matBundleExists READ matBundleExists NOTIFY matBundleExistsChanged) + Q_PROPERTY(bool isEmpty MEMBER m_isEmpty NOTIFY isEmptyChanged) + Q_PROPERTY(bool hasRequiredQuick3DImport READ hasRequiredQuick3DImport NOTIFY hasRequiredQuick3DImportChanged) + Q_PROPERTY(bool hasModelSelection READ hasModelSelection NOTIFY hasModelSelectionChanged) + Q_PROPERTY(bool importerRunning MEMBER m_importerRunning NOTIFY importerRunningChanged) + Q_PROPERTY(QList userMaterials MEMBER m_userMaterials NOTIFY userMaterialsChanged) + Q_PROPERTY(QList userTextures MEMBER m_userTextures NOTIFY userTexturesChanged) + Q_PROPERTY(QList user3DItems MEMBER m_user3DItems NOTIFY user3DItemsChanged) + Q_PROPERTY(QList userEffects MEMBER m_userEffects NOTIFY userEffectsChanged) + +public: + ContentLibraryUserModel(ContentLibraryWidget *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + void setSearchText(const QString &searchText); + void updateImportedState(const QStringList &importedMats); + + void setQuick3DImportVersion(int major, int minor); + + bool hasRequiredQuick3DImport() const; + + bool matBundleExists() const; + + bool hasModelSelection() const; + void setHasModelSelection(bool b); + + void resetModel(); + void updateIsEmpty(); + + Internal::ContentLibraryBundleImporter *bundleImporter() const; + + Q_INVOKABLE void applyToSelected(QmlDesigner::ContentLibraryMaterial *mat, bool add = false); + Q_INVOKABLE void addToProject(QmlDesigner::ContentLibraryMaterial *mat); + Q_INVOKABLE void removeFromProject(QmlDesigner::ContentLibraryMaterial *mat); + +signals: + void isEmptyChanged(); + void hasRequiredQuick3DImportChanged(); + void hasModelSelectionChanged(); + void userMaterialsChanged(); + void userTexturesChanged(); + void user3DItemsChanged(); + void userEffectsChanged(); + + void applyToSelectedTriggered(QmlDesigner::ContentLibraryMaterial *mat, bool add = false); + +#ifdef QDS_USE_PROJECTSTORAGE + void bundleMaterialImported(const QmlDesigner::TypeName &typeName); +#else + void bundleMaterialImported(const QmlDesigner::NodeMetaInfo &metaInfo); +#endif + void bundleMaterialAboutToUnimport(const QmlDesigner::TypeName &type); + void bundleMaterialUnimported(const QmlDesigner::NodeMetaInfo &metaInfo); + void importerRunningChanged(); + void matBundleExistsChanged(); + +private: + void loadUserBundle(); + bool isValidIndex(int idx) const; + void createImporter(const QString &bundlePath, const QString &bundleId, + const QStringList &sharedFiles); + + ContentLibraryWidget *m_widget = nullptr; + QString m_searchText; + + QList m_userMaterials; + QList m_userTextures; + QList m_userEffects; + QList m_user3DItems; + QStringList m_userCategories; + + QJsonObject m_bundleObj; + Internal::ContentLibraryBundleImporter *m_importer = nullptr; + + bool m_isEmpty = true; + bool m_matBundleExists = false; + bool m_hasModelSelection = false; + bool m_importerRunning = false; + + int m_quick3dMajorVersion = -1; + int m_quick3dMinorVersion = -1; + + QString m_importerBundlePath; + QString m_importerBundleId; + QStringList m_importerSharedFiles; + + enum Roles { NameRole = Qt::UserRole + 1, VisibleRole, ExpandedRole, ItemsRole }; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp index 61ae078ea8d..37ad648c3a4 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp @@ -10,6 +10,7 @@ #include "contentlibrarymaterialsmodel.h" #include "contentlibrarytexture.h" #include "contentlibrarytexturesmodel.h" +#include "contentlibraryusermodel.h" #include "contentlibrarywidget.h" #include "externaldependenciesinterface.h" #include "nodelistproperty.h" @@ -204,6 +205,8 @@ WidgetInfo ContentLibraryView::widgetInfo() connect(effectsModel, &ContentLibraryEffectsModel::bundleItemUnimported, this, &ContentLibraryView::updateBundleEffectsImportedState); + + connectUserBundle(); } return createWidgetInfo(m_widget.data(), @@ -213,6 +216,64 @@ WidgetInfo ContentLibraryView::widgetInfo() tr("Content Library")); } +void ContentLibraryView::connectUserBundle() +{ + ContentLibraryUserModel *userModel = m_widget->userModel().data(); + + connect(userModel, + &ContentLibraryUserModel::applyToSelectedTriggered, + this, + [&](ContentLibraryMaterial *bundleMat, bool add) { + if (m_selectedModels.isEmpty()) + return; + + m_bundleMaterialTargets = m_selectedModels; + m_bundleMaterialAddToSelected = add; + + ModelNode defaultMat = getBundleMaterialDefaultInstance(bundleMat->type()); + if (defaultMat.isValid()) + applyBundleMaterialToDropTarget(defaultMat); + else + m_widget->userModel()->addToProject(bundleMat); + }); + +#ifdef QDS_USE_PROJECTSTORAGE + connect(userModel, + &ContentLibraryUserModel::bundleMaterialImported, + this, + [&](const QmlDesigner::TypeName &typeName) { + applyBundleMaterialToDropTarget({}, typeName); + updateBundleUserMaterialsImportedState(); + }); +#else + connect(userModel, + &ContentLibraryUserModel::bundleMaterialImported, + this, + [&](const QmlDesigner::NodeMetaInfo &metaInfo) { + applyBundleMaterialToDropTarget({}, metaInfo); + updateBundleUserMaterialsImportedState(); + }); +#endif + + connect(userModel, &ContentLibraryUserModel::bundleMaterialAboutToUnimport, this, + [&] (const QmlDesigner::TypeName &type) { + // delete instances of the bundle material that is about to be unimported + executeInTransaction("ContentLibraryView::connectUserModel", [&] { + ModelNode matLib = Utils3D::materialLibraryNode(this); + if (!matLib.isValid()) + return; + + Utils::reverseForeach(matLib.directSubModelNodes(), [&](const ModelNode &mat) { + if (mat.isValid() && mat.type() == type) + QmlObjectNode(mat).destroy(); + }); + }); + }); + + connect(userModel, &ContentLibraryUserModel::bundleMaterialUnimported, this, + &ContentLibraryView::updateBundleUserMaterialsImportedState); +} + void ContentLibraryView::modelAttached(Model *model) { AbstractView::modelAttached(model); @@ -276,6 +337,7 @@ void ContentLibraryView::selectedNodesChanged(const QList &selectedNo }); m_widget->materialsModel()->setHasModelSelection(!m_selectedModels.isEmpty()); + m_widget->userModel()->setHasModelSelection(!m_selectedModels.isEmpty()); } void ContentLibraryView::customNotification(const AbstractView *view, @@ -548,6 +610,25 @@ void ContentLibraryView::updateBundleMaterialsImportedState() m_widget->materialsModel()->updateImportedState(importedBundleMats); } +void ContentLibraryView::updateBundleUserMaterialsImportedState() +{ + using namespace Utils; + + if (!m_widget->userModel()->bundleImporter()) + return; + + QStringList importedBundleMats; + + FilePath bundlePath = m_widget->userModel()->bundleImporter()->resolveBundleImportPath(); + + if (bundlePath.exists()) { + importedBundleMats = transform(bundlePath.dirEntries({{"*.qml"}, QDir::Files}), + [](const FilePath &f) { return f.fileName().chopped(4); }); + } + + m_widget->userModel()->updateImportedState(importedBundleMats); +} + void ContentLibraryView::updateBundleEffectsImportedState() { using namespace Utils; diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h index 3b57b7a4abb..48a8fd98f49 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h @@ -46,8 +46,10 @@ public: const QVariant &data) override; private: + void connectUserBundle(); void active3DSceneChanged(qint32 sceneId); void updateBundleMaterialsImportedState(); + void updateBundleUserMaterialsImportedState(); void updateBundleEffectsImportedState(); void updateBundlesQuick3DVersion(); #ifdef QDS_USE_PROJECTSTORAGE diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.cpp index c885a76ba71..8a2e81cfb21 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.cpp +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.cpp @@ -10,6 +10,7 @@ #include "contentlibrarytexture.h" #include "contentlibrarytexturesmodel.h" #include "contentlibraryiconprovider.h" +#include "contentlibraryusermodel.h" #include "utils/filedownloader.h" #include "utils/fileextractor.h" @@ -126,6 +127,7 @@ ContentLibraryWidget::ContentLibraryWidget() , m_texturesModel(new ContentLibraryTexturesModel("Textures", this)) , m_environmentsModel(new ContentLibraryTexturesModel("Environments", this)) , m_effectsModel(new ContentLibraryEffectsModel(this)) + , m_userModel(new ContentLibraryUserModel(this)) { qmlRegisterType("WebFetcher", 1, 0, "FileDownloader"); qmlRegisterType("WebFetcher", 1, 0, "FileExtractor"); @@ -177,7 +179,8 @@ ContentLibraryWidget::ContentLibraryWidget() {"materialsModel", QVariant::fromValue(m_materialsModel.data())}, {"texturesModel", QVariant::fromValue(m_texturesModel.data())}, {"environmentsModel", QVariant::fromValue(m_environmentsModel.data())}, - {"effectsModel", QVariant::fromValue(m_effectsModel.data())}}); + {"effectsModel", QVariant::fromValue(m_effectsModel.data())}, + {"userModel", QVariant::fromValue(m_userModel.data())}}); reloadQmlSource(); } @@ -601,6 +604,12 @@ void ContentLibraryWidget::markTextureUpdated(const QString &textureKey) m_environmentsModel->markTextureHasNoUpdates(subcategory, textureKey); } +bool ContentLibraryWidget::userBundleEnabled() const +{ + // TODO: this method is to be removed after user bundle implementation is complete + return Core::ICore::settings()->value("QML/Designer/UseExperimentalFeatures45", false).toBool(); +} + QSize ContentLibraryWidget::sizeHint() const { return {420, 420}; @@ -715,6 +724,7 @@ void ContentLibraryWidget::updateSearch() m_effectsModel->setSearchText(m_filterText); m_texturesModel->setSearchText(m_filterText); m_environmentsModel->setSearchText(m_filterText); + m_userModel->setSearchText(m_filterText); m_quickWidget->update(); } @@ -821,4 +831,9 @@ QPointer ContentLibraryWidget::effectsModel() const return m_effectsModel; } +QPointer ContentLibraryWidget::userModel() const +{ + return m_userModel; +} + } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.h b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.h index ab71a3dc799..729443817ea 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.h +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.h @@ -24,6 +24,7 @@ class ContentLibraryMaterial; class ContentLibraryMaterialsModel; class ContentLibraryTexture; class ContentLibraryTexturesModel; +class ContentLibraryUserModel; class ContentLibraryWidget : public QFrame { @@ -65,6 +66,7 @@ public: QPointer texturesModel() const; QPointer environmentsModel() const; QPointer effectsModel() const; + QPointer userModel() const; Q_INVOKABLE void startDragEffect(QmlDesigner::ContentLibraryEffect *eff, const QPointF &mousePos); Q_INVOKABLE void startDragMaterial(QmlDesigner::ContentLibraryMaterial *mat, const QPointF &mousePos); @@ -74,6 +76,7 @@ public: Q_INVOKABLE void addLightProbe(QmlDesigner::ContentLibraryTexture *tex); Q_INVOKABLE void updateSceneEnvState(); Q_INVOKABLE void markTextureUpdated(const QString &textureKey); + Q_INVOKABLE bool userBundleEnabled() const; QSize sizeHint() const override; @@ -112,6 +115,7 @@ private: QPointer m_texturesModel; QPointer m_environmentsModel; QPointer m_effectsModel; + QPointer m_userModel; QShortcut *m_qmlSourceUpdateShortcut = nullptr; -- cgit v1.2.3