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

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