diff options
author | Mahmoud Badri <mahmoud.badri@qt.io> | 2024-03-21 12:47:09 +0200 |
---|---|---|
committer | Mahmoud Badri <mahmoud.badri@qt.io> | 2024-04-08 12:13:16 +0000 |
commit | bc5628afca0c0716642cd69679d6b51acfa60316 (patch) | |
tree | ff0ceee4cea89c86f2ef60ad352b68da8e822a74 | |
parent | da21fa4c3396e867537909b68f4a053114d195c0 (diff) |
QmlDesigner: Add content library user materials bundle
Fixes: QDS-12389
Change-Id: Icec1b06c57e0eaa4ff444e3143d3cba0803c8dd1
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
15 files changed, 761 insertions, 47 deletions
diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibrary.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibrary.qml index c6db8425ff..2c98b58adc 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 93b226d6ca..0e9fc4903e 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 ca3a05bdd1..b67ec311ef 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 c21baf4c58..9a0e33b8e5 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("<b>Content Library</b> materials are not supported in Qt5 projects.") else if (!ContentLibraryBackend.rootView.hasQuick3DImport) qsTr("To use <b>Content Library</b>, first add the QtQuick3D module in the <b>Components</b> view.") - else if (!materialsModel.hasRequiredQuick3DImport) + else if (!root.materialsModel.hasRequiredQuick3DImport) qsTr("To use <b>Content Library</b>, version 6.3 or later of the QtQuick3D module is required.") else if (!ContentLibraryBackend.rootView.hasMaterialLibrary) qsTr("<b>Content Library</b> 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 1fac9f2234..617b724e66 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 0000000000..d3d1dbad92 --- /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("<b>Content Library</b> is not supported in Qt5 projects.") + else if (!ContentLibraryBackend.rootView.hasQuick3DImport) + qsTr("To use <b>Content Library</b>, first add the QtQuick3D module in the <b>Components</b> view.") + else if (!ContentLibraryBackend.rootView.hasMaterialLibrary) + qsTr("<b>Content Library</b> 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 48be045d8b..4385e3bf82 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 01f03cc271..1c7bebffa3 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 f546ea98cd..90d8468fa1 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 <QDataStream> #include <QObject> #include <QUrl> diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.cpp new file mode 100644 index 0000000000..56ba8cc6c7 --- /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 <designerpaths.h> +#include "qmldesignerconstants.h" + +#include <utils/algorithm.h> +#include <utils/hostosinfo.h> +#include <utils/qtcassert.h> + +#include <QCoreApplication> +#include <QJsonArray> +#include <QJsonDocument> +#include <QQmlEngine> +#include <QStandardPaths> +#include <QUrl> + +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<int, QByteArray> ContentLibraryUserModel::roleNames() const +{ + static const QHash<int, QByteArray> 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 0000000000..72c535e401 --- /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 <QAbstractListModel> +#include <QJsonObject> + +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<ContentLibraryMaterial *> userMaterials MEMBER m_userMaterials NOTIFY userMaterialsChanged) + Q_PROPERTY(QList<ContentLibraryTexture *> userTextures MEMBER m_userTextures NOTIFY userTexturesChanged) + Q_PROPERTY(QList<ContentLibraryEffect *> user3DItems MEMBER m_user3DItems NOTIFY user3DItemsChanged) + Q_PROPERTY(QList<ContentLibraryEffect *> 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<int, QByteArray> 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<ContentLibraryMaterial *> m_userMaterials; + QList<ContentLibraryTexture *> m_userTextures; + QList<ContentLibraryEffect *> m_userEffects; + QList<ContentLibraryEffect *> 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 61ae078ea8..37ad648c3a 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<ModelNode> &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 3b57b7a4ab..48a8fd98f4 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 c885a76ba7..8a2e81cfb2 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<QmlDesigner::FileDownloader>("WebFetcher", 1, 0, "FileDownloader"); qmlRegisterType<QmlDesigner::FileExtractor>("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<ContentLibraryEffectsModel> ContentLibraryWidget::effectsModel() const return m_effectsModel; } +QPointer<ContentLibraryUserModel> 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 ab71a3dc79..729443817e 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<ContentLibraryTexturesModel> texturesModel() const; QPointer<ContentLibraryTexturesModel> environmentsModel() const; QPointer<ContentLibraryEffectsModel> effectsModel() const; + QPointer<ContentLibraryUserModel> 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<ContentLibraryTexturesModel> m_texturesModel; QPointer<ContentLibraryTexturesModel> m_environmentsModel; QPointer<ContentLibraryEffectsModel> m_effectsModel; + QPointer<ContentLibraryUserModel> m_userModel; QShortcut *m_qmlSourceUpdateShortcut = nullptr; |