QmlDesigner: Add content library user materials bundle

Fixes: QDS-12389
Change-Id: Icec1b06c57e0eaa4ff444e3143d3cba0803c8dd1
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
This commit is contained in:
Mahmoud Badri
2024-03-21 12:47:09 +02:00
parent da21fa4c33
commit bc5628afca
15 changed files with 761 additions and 47 deletions

View File

@@ -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()
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -3,9 +3,8 @@
#pragma once
#include "qmldesignercorelib_global.h"
#include "nodeinstanceglobal.h"
#include <QDataStream>
#include <QObject>
#include <QUrl>

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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;