QmlDesigner: Make imported 3D scenes available via assets

A placeholder .q3d file is created under content for imported 3D
components found under Generated/QtQuick3D on asset view attach and
every time new import is done. .q3d file contains a project root
relative path to component's import folder.

.q3d files get generated preview as icon in assets view.

Imported 3D items are no longer shown in Components view.

Removing .q3d file will remove the corresponding module as well
as all model nodes created from that asset.

Removing last model node of asset will remove the import statement
on next document save.

Fixes: QDS-12193
Fixes: QDS-14565
Change-Id: If01546ca4c78334bac73b055ed156276f6f8f2a4
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
Reviewed-by: Marco Bubke <marco.bubke@qt.io>
This commit is contained in:
Miikka Heikkinen
2024-12-13 10:56:56 +02:00
parent 5bc716d956
commit ede7969ea3
42 changed files with 672 additions and 219 deletions

View File

@@ -239,6 +239,9 @@ T.TreeViewDelegate {
+ size.width + " x " + size.height
+ "\n" + fileSize
+ " " + fileExt
} else if (rootView.assetIsImported3d(model.filePath)) {
return filePath + "\n"
+ fileExt
} else {
return filePath + "\n"
+ fileSize

View File

@@ -187,50 +187,6 @@ void EffectComposerView::selectedNodesChanged(const QList<QmlDesigner::ModelNode
m_widget->effectComposerModel()->setHasValidTarget(hasValidTarget);
}
void EffectComposerView::nodeAboutToBeRemoved(const QmlDesigner::ModelNode &removedNode)
{
QList<QmlDesigner::ModelNode> nodes = removedNode.allSubModelNodesAndThisNode();
bool effectRemoved = false;
for (const QmlDesigner::ModelNode &node : nodes) {
QmlDesigner::QmlItemNode qmlNode(node);
if (qmlNode.isEffectItem()) {
effectRemoved = true;
break;
}
}
if (effectRemoved)
QTimer::singleShot(0, this, &EffectComposerView::removeUnusedEffectImports);
}
void EffectComposerView::removeUnusedEffectImports()
{
QTC_ASSERT(model(), return);
const QString effectPrefix = m_componentUtils.composedEffectsTypePrefix();
const QmlDesigner::Imports &imports = model()->imports();
QHash<QString, QmlDesigner::Import> effectImports;
for (const QmlDesigner::Import &import : imports) {
if (import.url().startsWith(effectPrefix)) {
QString type = import.url().split('.').last();
effectImports.insert(type, import);
}
}
const QList<QmlDesigner::ModelNode> allNodes = allModelNodes();
for (const QmlDesigner::ModelNode &node : allNodes) {
if (QmlDesigner::QmlItemNode(node).isEffectItem())
effectImports.remove(node.simplifiedTypeName());
}
if (!effectImports.isEmpty()) {
QmlDesigner::Imports removeImports;
for (const QmlDesigner::Import &import : effectImports)
removeImports.append(import);
model()->changeImports({}, removeImports);
}
}
void EffectComposerView::highlightSupportedProperties(bool highlight, const QString &suffix)
{
QQmlContext *ctxObj = m_widget->quickWidget()->rootContext();

View File

@@ -29,7 +29,6 @@ public:
void modelAboutToBeDetached(QmlDesigner::Model *model) override;
void selectedNodesChanged(const QList<QmlDesigner::ModelNode> &selectedNodeList,
const QList<QmlDesigner::ModelNode> &lastSelectedNodeList) override;
void nodeAboutToBeRemoved(const QmlDesigner::ModelNode &removedNode) override;
void dragStarted(QMimeData *mimeData) override;
void dragEnded() override;
@@ -41,7 +40,6 @@ public:
private:
void customNotification(const AbstractView *view, const QString &identifier,
const QList<QmlDesigner::ModelNode> &nodeList, const QList<QVariant> &data) override;
void removeUnusedEffectImports();
QPointer<EffectComposerWidget> m_widget;
QString m_currProjectPath;

View File

@@ -24,5 +24,7 @@
<file>images/asset_ktx_128.png</file>
<file>images/asset_folder.png</file>
<file>images/asset_folder@2x.png</file>
<file>images/asset_imported3d.png</file>
<file>images/asset_imported3d@2x.png</file>
</qresource>
</RCC>

View File

@@ -77,7 +77,7 @@ QPixmap AssetsLibraryIconProvider::generateFontIcons(const QString &filePath, co
"Abc"}).pixmap(reqSize);
}
QPair<QPixmap, qint64> AssetsLibraryIconProvider::fetchPixmap(const QString &id, const QSize &requestedSize) const
QPair<QPixmap, qint64> AssetsLibraryIconProvider::fetchPixmap(const QString &id, const QSize &requestedSize)
{
Asset asset(id);
@@ -100,6 +100,18 @@ QPair<QPixmap, qint64> AssetsLibraryIconProvider::fetchPixmap(const QString &id,
qint64 size = QFileInfo(id).size();
QString filePath = Utils::StyleHelper::dpiSpecificImageFile(":/AssetsLibrary/images/asset_ktx.png");
return {QPixmap{filePath}, size};
} else if (asset.isImported3D()) {
static QPixmap defaultPreview = QPixmap::fromImage(QImage(":/AssetsLibrary/images/asset_imported3d.png"));
QPixmap pixmap{requestedSize};
QString assetId = id.mid(id.lastIndexOf('/') + 1);
assetId.chop(asset.suffix().size() - 1); // Remove suffix
if (m_pixmaps.contains(assetId)) {
pixmap = m_pixmaps.value(assetId);
} else {
pixmap = defaultPreview;
emit asyncAssetPreviewRequested(assetId, id);
}
return {pixmap, 0};
} else {
QString type;
if (asset.isShader())
@@ -145,5 +157,18 @@ qint64 AssetsLibraryIconProvider::fileSize(const QString &id)
return m_thumbnails.contains(id) ? m_thumbnails[id].fileSize : 0;
}
QString AssetsLibraryIconProvider::setPixmap(const QString &id, const QPixmap &pixmap,
const QString &suffix)
{
m_pixmaps.insert(id, pixmap);
const QStringList thumbs = m_thumbnails.keys();
const QString checkName = id + "." + suffix;
for (const auto &thumb : thumbs) {
if (thumb.endsWith(checkName))
return thumb;
}
return {};
}
} // namespace QmlDesigner

View File

@@ -20,6 +20,7 @@ struct Thumbnail
class AssetsLibraryIconProvider : public QQuickImageProvider
{
Q_OBJECT
public:
AssetsLibraryIconProvider(SynchronousImageCache &fontImageCache);
@@ -28,10 +29,14 @@ public:
void invalidateThumbnail(const QString &id);
QSize imageSize(const QString &id);
qint64 fileSize(const QString &id);
QString setPixmap(const QString &id, const QPixmap &pixmap, const QString &suffix);
signals:
void asyncAssetPreviewRequested(const QString &assetId, const QString &assetFile);
private:
QPixmap generateFontIcons(const QString &filePath, const QSize &requestedSize) const;
QPair<QPixmap, qint64> fetchPixmap(const QString &id, const QSize &requestedSize) const;
QPair<QPixmap, qint64> fetchPixmap(const QString &id, const QSize &requestedSize);
Thumbnail createThumbnail(const QString &id, const QSize &requestedSize);
SynchronousImageCache &m_fontImageCache;
@@ -42,6 +47,7 @@ private:
{96, 96}, // list @2x
{48, 48}}; // list
QHash<QString, Thumbnail> m_thumbnails;
QHash<QString, QPixmap> m_pixmaps;
};
} // namespace QmlDesigner

View File

@@ -118,19 +118,33 @@ void AssetsLibraryModel::deleteFiles(const QStringList &filePaths, bool dontAskA
if (dontAskAgain)
QmlDesignerPlugin::settings().insert(DesignerSettingsKey::ASK_BEFORE_DELETING_ASSET, false);
QStringList deletedEffects;
QHash<QString, Utils::FilePath> deletedAssets;
const GeneratedComponentUtils &compUtils = QmlDesignerPlugin::instance()->documentManager()
.generatedComponentUtils();
const QString effectTypePrefix = compUtils.composedEffectsTypePrefix();
const Utils::FilePath effectBasePath = compUtils.composedEffectsBasePath();
for (const QString &filePath : filePaths) {
QFileInfo fi(filePath);
if (fi.exists()) {
if (QFile::remove(filePath)) {
if (Asset(filePath).isEffect()) {
// If an effect composer effect was removed, also remove effect module from project
QString effectName = fi.baseName();
if (!effectName.isEmpty())
deletedEffects.append(effectName);
Utils::FilePath fp = Utils::FilePath::fromString(filePath);
if (fp.exists()) {
// If a generated asset was removed, also remove its module from project
Asset asset(filePath);
QString fullType;
if (asset.isEffect()) {
QString effectName = fp.baseName();
fullType = QString("%1.%2.%2").arg(effectTypePrefix, effectName, effectName);
deletedAssets.insert(fullType, effectBasePath.resolvePath(effectName));
} else if (asset.isImported3D()) {
Utils::FilePath qmlFile = compUtils.getImported3dQml(filePath);
if (qmlFile.exists()) {
QString importName = compUtils.getImported3dImportName(qmlFile);
fullType = QString("%1.%2").arg(importName, qmlFile.baseName());
deletedAssets.insert(fullType, qmlFile.absolutePath());
}
} else {
}
if (!fp.removeFile()) {
deletedAssets.remove(fullType);
QMessageBox::warning(Core::ICore::dialogParent(),
Tr::tr("Failed to Delete File"),
Tr::tr("Could not delete \"%1\".").arg(filePath));
@@ -138,8 +152,8 @@ void AssetsLibraryModel::deleteFiles(const QStringList &filePaths, bool dontAskA
}
}
if (!deletedEffects.isEmpty())
emit effectsDeleted(deletedEffects);
if (!deletedAssets.isEmpty())
emit generatedAssetsDeleted(deletedAssets);
}
bool AssetsLibraryModel::renameFolder(const QString &folderPath, const QString &newName)

View File

@@ -72,7 +72,7 @@ signals:
void rootPathChanged();
void isEmptyChanged();
void fileChanged(const QString &path);
void effectsDeleted(const QStringList &effectNames);
void generatedAssetsDeleted(const QHash<QString, Utils::FilePath> &assetData);
private:
void setIsEmpty(bool value);

View File

@@ -15,6 +15,7 @@
#include <imagecachecollectors/imagecachecollector.h>
#include <imagecachecollectors/imagecacheconnectionmanager.h>
#include <imagecachecollectors/imagecachefontcollector.h>
#include <modelnodeoperations.h>
#include <nodelistproperty.h>
#include <projectexplorer/kit.h>
#include <projectexplorer/project.h>
@@ -41,11 +42,18 @@ public:
TimeStampProvider timeStampProvider;
AsynchronousImageCache asynchronousFontImageCache{storage, fontGenerator, timeStampProvider};
SynchronousImageCache synchronousFontImageCache{storage, timeStampProvider, fontCollector};
AsynchronousImageCache *mainImageCache = {};
};
AssetsLibraryView::AssetsLibraryView(ExternalDependenciesInterface &externalDependencies)
AssetsLibraryView::AssetsLibraryView(AsynchronousImageCache &imageCache,
ExternalDependenciesInterface &externalDependencies)
: AbstractView{externalDependencies}
{}
{
imageCacheData()->mainImageCache = &imageCache;
m_3dImportsSyncTimer.callOnTimeout(this, &AssetsLibraryView::sync3dImports);
m_3dImportsSyncTimer.setInterval(1000);
m_3dImportsSyncTimer.setSingleShot(true);
}
AssetsLibraryView::~AssetsLibraryView()
{}
@@ -59,6 +67,7 @@ WidgetInfo AssetsLibraryView::widgetInfo()
{
if (!m_widget) {
m_widget = Utils::makeUniqueObjectPtr<AssetsLibraryWidget>(
*imageCacheData()->mainImageCache,
imageCacheData()->asynchronousFontImageCache,
imageCacheData()->synchronousFontImageCache,
this);
@@ -74,6 +83,8 @@ void AssetsLibraryView::customNotification(const AbstractView * /*view*/,
{
if (identifier == "delete_selected_assets")
m_widget->deleteSelectedAssets();
else if (identifier == "asset_import_finished")
m_3dImportsSyncTimer.start();
}
void AssetsLibraryView::modelAttached(Model *model)
@@ -82,17 +93,20 @@ void AssetsLibraryView::modelAttached(Model *model)
m_widget->clearSearchFilter();
setResourcePath(DocumentManager::currentResourcePath().toFileInfo().absoluteFilePath());
setResourcePath(DocumentManager::currentResourcePath().toFSPathString());
m_3dImportsSyncTimer.start();
}
void AssetsLibraryView::modelAboutToBeDetached(Model *model)
{
AbstractView::modelAboutToBeDetached(model);
m_3dImportsSyncTimer.stop();
}
void AssetsLibraryView::setResourcePath(const QString &resourcePath)
{
if (resourcePath == m_lastResourcePath)
return;
@@ -100,6 +114,7 @@ void AssetsLibraryView::setResourcePath(const QString &resourcePath)
if (!m_widget) {
m_widget = Utils::makeUniqueObjectPtr<AssetsLibraryWidget>(
*imageCacheData()->mainImageCache,
imageCacheData()->asynchronousFontImageCache,
imageCacheData()->synchronousFontImageCache,
this);
@@ -108,6 +123,68 @@ void AssetsLibraryView::setResourcePath(const QString &resourcePath)
m_widget->setResourcePath(resourcePath);
}
QHash<QString, Utils::FilePath> AssetsLibraryView::collectFiles(const Utils::FilePath &dirPath,
const QString &suffix)
{
if (dirPath.isEmpty())
return {};
QHash<QString, Utils::FilePath> files;
Utils::FilePaths entryList = dirPath.dirEntries(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
for (const Utils::FilePath &entry : entryList) {
if (entry.isDir()) {
files.insert(collectFiles(entry.absoluteFilePath(), suffix));
} else if (entry.suffix() == suffix) {
QString baseName = entry.baseName();
files.insert(baseName, entry);
}
}
return files;
}
void AssetsLibraryView::sync3dImports()
{
if (!model())
return;
// TODO: Once project storage supports notifications for new and removed types,
// sync3dImports() should be called in that case as well.
// Also, custom notification "asset_import_finished" should not be necessary in that case.
// Sync generated 3d imports to .q3d files in project content
const GeneratedComponentUtils &compUtils
= QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
auto projPath = Utils::FilePath::fromString(externalDependencies().currentProjectDirPath());
const QList<Utils::FilePath> qmlFiles = compUtils.imported3dComponents();
Utils::FilePath resPath = DocumentManager::currentResourcePath();
QHash<QString, Utils::FilePath> files = collectFiles(resPath, "q3d");
const QString pathTemplate("/%1.q3d");
for (const Utils::FilePath &qmlFile : qmlFiles) {
const QString fileStr = qmlFile.baseName();
if (files.contains(fileStr)) {
files.remove(fileStr);
} else {
Utils::FilePath targetPath = ModelNodeOperations::getImported3dDefaultDirectory();
if (!targetPath.isAbsolutePath() || !targetPath.exists())
targetPath = resPath;
Utils::FilePath newFile = targetPath.pathAppended(pathTemplate.arg(fileStr));
QByteArray data;
data.append(qmlFile.relativePathFrom(projPath).toFSPathString().toLatin1());
newFile.writeFileContents(data);
}
}
// Remove .q3d files that do not match any imported 3d
for (const Utils::FilePath &f : std::as_const(files))
f.removeFile();
}
AssetsLibraryView::ImageCacheData *AssetsLibraryView::imageCacheData()
{
std::call_once(imageCacheFlag,

View File

@@ -5,9 +5,11 @@
#include "abstractview.h"
#include <utils/filepath.h>
#include <utils/uniqueobjectptr.h>
#include <QPointer>
#include <QTimer>
#include <mutex>
@@ -21,7 +23,8 @@ class AssetsLibraryView : public AbstractView
Q_OBJECT
public:
AssetsLibraryView(ExternalDependenciesInterface &externalDependencies);
AssetsLibraryView(AsynchronousImageCache &imageCache,
ExternalDependenciesInterface &externalDependencies);
~AssetsLibraryView() override;
bool hasWidget() const override;
@@ -30,7 +33,6 @@ public:
// AbstractView
void modelAttached(Model *model) override;
void modelAboutToBeDetached(Model *model) override;
void setResourcePath(const QString &resourcePath);
private:
@@ -39,11 +41,15 @@ private:
void customNotification(const AbstractView *view, const QString &identifier,
const QList<ModelNode> &nodeList, const QList<QVariant> &data) override;
QHash<QString, Utils::FilePath> collectFiles(const Utils::FilePath &dirPath,
const QString &suffix);
void sync3dImports();
std::once_flag imageCacheFlag;
std::unique_ptr<ImageCacheData> m_imageCacheData;
Utils::UniqueObjectPtr<AssetsLibraryWidget> m_widget;
QString m_lastResourcePath;
QTimer m_3dImportsSyncTimer;
};
}

View File

@@ -8,6 +8,7 @@
#include "assetslibraryview.h"
#include <qmldesignertr.h>
#include <asynchronousimagecache.h>
#include <createtexture.h>
#include <designeractionmanager.h>
#include <designermcumanager.h>
@@ -97,10 +98,12 @@ bool AssetsLibraryWidget::eventFilter(QObject *obj, QEvent *event)
return QObject::eventFilter(obj, event);
}
AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFontImageCache,
AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &mainImageCache,
AsynchronousImageCache &asynchronousFontImageCache,
SynchronousImageCache &synchronousFontImageCache,
AssetsLibraryView *view)
: m_itemIconSize{24, 24}
, m_mainImageCache{mainImageCache}
, m_fontImageCache{synchronousFontImageCache}
, m_assetsIconProvider{new AssetsLibraryIconProvider(synchronousFontImageCache)}
, m_assetsModel{new AssetsLibraryModel(this)}
@@ -110,6 +113,44 @@ AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFon
setWindowTitle(Tr::tr("Assets Library", "Title of assets library widget"));
setMinimumWidth(250);
connect(m_assetsIconProvider, &AssetsLibraryIconProvider::asyncAssetPreviewRequested,
this, [this](const QString &assetId, const QString &assetFile) {
Asset asset{assetFile};
if (!asset.isImported3D())
return;
Utils::FilePath fullPath = QmlDesignerPlugin::instance()->documentManager()
.generatedComponentUtils().getImported3dQml(assetFile);
if (!fullPath.exists())
return;
m_mainImageCache.requestImage(
Utils::PathString{fullPath.toFSPathString()},
[this, assetId](const QImage &image) {
QMetaObject::invokeMethod(this, [this, assetId, image] {
updateAssetPreview(assetId, QPixmap::fromImage(image), "q3d");
}, Qt::QueuedConnection);
},
[assetFile](ImageCache::AbortReason abortReason) {
if (abortReason == ImageCache::AbortReason::Abort) {
qWarning() << QLatin1String(
"AssetsLibraryIconProvider::asyncAssetPreviewRequested(): preview generation "
"failed for path %1, reason: Abort").arg(assetFile);
} else if (abortReason == ImageCache::AbortReason::Failed) {
qWarning() << QLatin1String(
"AssetsLibraryIconProvider::asyncAssetPreviewRequested(): preview generation "
"failed for path %1, reason: Failed").arg(assetFile);
} else if (abortReason == ImageCache::AbortReason::NoEntry) {
qWarning() << QLatin1String(
"AssetsLibraryIconProvider::asyncAssetPreviewRequested(): preview generation "
"failed for path %1, reason: NoEntry").arg(assetFile);
}
},
"libIcon",
ImageCache::LibraryIconAuxiliaryData{true});
});
m_assetsWidget->quickWidget()->installEventFilter(this);
m_fontPreviewTooltipBackend = std::make_unique<PreviewTooltipBackend>(asynchronousFontImageCache);
@@ -135,8 +176,8 @@ AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFon
connect(m_assetsModel, &AssetsLibraryModel::fileChanged,
QmlDesignerPlugin::instance(), &QmlDesignerPlugin::assetChanged);
connect(m_assetsModel, &AssetsLibraryModel::effectsDeleted,
this, &AssetsLibraryWidget::handleDeleteEffects);
connect(m_assetsModel, &AssetsLibraryModel::generatedAssetsDeleted,
this, &AssetsLibraryWidget::handleDeletedGeneratedAssets);
auto layout = new QVBoxLayout(this);
layout->setContentsMargins({});
@@ -292,43 +333,63 @@ void AssetsLibraryWidget::setHasSceneEnv(bool b)
emit hasSceneEnvChanged();
}
void AssetsLibraryWidget::handleDeleteEffects([[maybe_unused]] const QStringList &effectNames)
void AssetsLibraryWidget::handleDeletedGeneratedAssets(const QHash<QString, Utils::FilePath> &assetData)
{
#ifdef QDS_USE_PROJECTSTORAGE
// That code has to rewritten with modules. Seem try to find all effects nodes.
#else
// assetData key: full type name including import, value: import dir
// This method removes all nodes of the deleted type (assetData.keys())
// and removes the import statement for that type
DesignDocument *document = QmlDesignerPlugin::instance()->currentDesignDocument();
if (!document)
return;
bool clearStacks = false;
// Remove usages of deleted effects from the current document
const Imports imports = m_assetsView->model()->imports();
const GeneratedComponentUtils &compUtils = QmlDesignerPlugin::instance()->documentManager()
.generatedComponentUtils();
QString effectPrefix = compUtils.composedEffectsTypePrefix();
QStringList effectNames;
// Remove usages of deleted assets from the current document
m_assetsView->executeInTransaction(__FUNCTION__, [&]() {
QList<ModelNode> allNodes = m_assetsView->allModelNodes();
const QString typeTemplate = "%1.%2.%2";
const QString importUrlTemplate = "%1.%2";
const Imports imports = m_assetsView->model()->imports();
Imports removedImports;
const QString typePrefix = QmlDesignerPlugin::instance()->documentManager()
.generatedComponentUtils().composedEffectsTypePrefix();
for (const QString &effectName : effectNames) {
if (effectName.isEmpty())
continue;
const TypeName type = typeTemplate.arg(typePrefix, effectName).toUtf8();
for (ModelNode &node : allNodes) {
if (node.metaInfo().typeName() == type) {
clearStacks = true;
node.destroy();
QList<Import> removedImports;
const QStringList assetTypes = assetData.keys();
for (const QString &assetType : assetTypes) {
QString removedImportUrl;
int idx = assetType.lastIndexOf('.');
if (idx >= 0) {
if (assetType.startsWith(effectPrefix))
effectNames.append(assetType.sliced(idx + 1));
removedImportUrl = assetType.first(idx);
#ifdef QDS_USE_PROJECTSTORAGE
auto module = m_assetsView->model()->module(removedImportUrl.toUtf8(),
Storage::ModuleKind::QmlLibrary);
auto metaInfo = m_assetsView->model()->metaInfo(module, assetType.sliced(idx + 1).toUtf8());
for (ModelNode &node : allNodes) {
if (node.metaInfo() == metaInfo) {
#else
TypeName type = assetType.toUtf8();
for (ModelNode &node : allNodes) {
if (node.metaInfo().typeName() == type) {
#endif
clearStacks = true;
node.destroy();
}
}
if (!removedImportUrl.isEmpty()) {
Import removedImport = Utils::findOrDefault(imports,
[&removedImportUrl](const Import &import) {
return import.url() == removedImportUrl;
});
if (!removedImport.isEmpty())
removedImports.append(removedImport);
}
}
const QString importPath = importUrlTemplate.arg(typePrefix, effectName);
Import removedImport = Utils::findOrDefault(imports, [&importPath](const Import &import) {
return import.url() == importPath;
});
if (!removedImport.isEmpty())
removedImports.append(removedImport);
}
if (!removedImports.isEmpty()) {
@@ -338,22 +399,20 @@ void AssetsLibraryWidget::handleDeleteEffects([[maybe_unused]] const QStringList
});
// The size check here is to weed out cases where project path somehow resolves
// to just slash. Shortest legal currentProjectDirPath() would be "/a/".
if (m_assetsModel->currentProjectDirPath().size() < 3)
// to just slash or drive + slash. (Shortest legal currentProjectDirPath() would be "/a/")
if (m_assetsModel->currentProjectDirPath().size() < 4)
return;
Utils::FilePath effectsDir = ModelNodeOperations::getEffectsImportDirectory();
// Delete the effect modules
for (const QString &effectName : effectNames) {
Utils::FilePath eDir = effectsDir.pathAppended(effectName);
if (eDir.exists() && eDir.toString().startsWith(m_assetsModel->currentProjectDirPath())) {
// Delete the asset modules
for (const Utils::FilePath &dir : assetData) {
if (dir.exists() && dir.toFSPathString().startsWith(m_assetsModel->currentProjectDirPath())) {
QString error;
eDir.removeRecursively(&error);
dir.removeRecursively(&error);
if (!error.isEmpty()) {
QMessageBox::warning(Core::ICore::dialogParent(),
Tr::tr("Failed to Delete Effect Resources"),
Tr::tr("Could not delete \"%1\".").arg(eDir.toString()));
Tr::tr("Failed to Delete Asset Resources"),
Tr::tr("Could not delete \"%1\".").arg(dir.toFSPathString()));
}
}
}
@@ -364,7 +423,16 @@ void AssetsLibraryWidget::handleDeleteEffects([[maybe_unused]] const QStringList
document->clearUndoRedoStacks();
m_assetsView->emitCustomNotification("effectcomposer_effects_deleted", {}, {effectNames});
#endif
m_assetsView->emitCustomNotification("assets_deleted");
}
void AssetsLibraryWidget::updateAssetPreview(const QString &id, const QPixmap &pixmap,
const QString &suffix)
{
const QString thumb = m_assetsIconProvider->setPixmap(id, pixmap, suffix);
if (!thumb.isEmpty())
emit m_assetsModel->fileChanged(thumb);
}
void AssetsLibraryWidget::invalidateThumbnail(const QString &id)
@@ -388,6 +456,11 @@ bool AssetsLibraryWidget::assetIsImageOrTexture(const QString &id)
return Asset(id).isValidTextureSource();
}
bool AssetsLibraryWidget::assetIsImported3d(const QString &id)
{
return Asset(id).isImported3D();
}
// needed to deal with "Object 0xXXXX destroyed while one of its QML signal handlers is in progress..." error which would lead to a crash
void AssetsLibraryWidget::invokeAssetsDrop(const QList<QUrl> &urls, const QString &targetDir)
{
@@ -615,6 +688,9 @@ QPair<QString, QByteArray> AssetsLibraryWidget::getAssetTypeAndData(const QStrin
} else if (asset.isEffect()) {
// Data: Effect Composer format (suffix)
return {Constants::MIME_TYPE_ASSET_EFFECT, asset.suffix().toUtf8()};
} else if (asset.isImported3D()) {
// Data: Imported 3D component (suffix)
return {Constants::MIME_TYPE_ASSET_IMPORTED3D, asset.suffix().toUtf8()};
}
}
return {};

View File

@@ -51,7 +51,8 @@ class AssetsLibraryWidget : public QFrame
Q_PROPERTY(bool isDragging MEMBER m_isDragging NOTIFY isDraggingChanged)
public:
AssetsLibraryWidget(AsynchronousImageCache &asynchronousFontImageCache,
AssetsLibraryWidget(AsynchronousImageCache &mainImageCache,
AsynchronousImageCache &asynchronousFontImageCache,
SynchronousImageCache &synchronousFontImageCache, AssetsLibraryView *view);
~AssetsLibraryWidget();
@@ -88,6 +89,7 @@ public:
Q_INVOKABLE QSize imageSize(const QString &id);
Q_INVOKABLE QString assetFileSize(const QString &id);
Q_INVOKABLE bool assetIsImageOrTexture(const QString &id);
Q_INVOKABLE bool assetIsImported3d(const QString &id);
Q_INVOKABLE void addTextures(const QStringList &filePaths);
Q_INVOKABLE void addLightProbe(const QString &filePaths);
Q_INVOKABLE void updateContextMenuActionsEnableState();
@@ -130,10 +132,12 @@ private:
void setHasSceneEnv(bool b);
void setCanCreateEffects(bool newVal);
void handleDeleteEffects(const QStringList &effectNames);
void handleDeletedGeneratedAssets(const QHash<QString, Utils::FilePath> &assetData);
void updateAssetPreview(const QString &id, const QPixmap &pixmap, const QString &suffix);
QSize m_itemIconSize;
AsynchronousImageCache &m_mainImageCache;
SynchronousImageCache &m_fontImageCache;
AssetsLibraryIconProvider *m_assetsIconProvider = nullptr;

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

View File

@@ -1879,6 +1879,13 @@ Utils::FilePath getImagesDefaultDirectory()
QmlDesignerPlugin::instance()->documentManager().currentProjectDirPath().toString()));
}
FilePath getImported3dDefaultDirectory()
{
return Utils::FilePath::fromString(getAssetDefaultDirectory(
"3d",
QmlDesignerPlugin::instance()->documentManager().currentProjectDirPath().toString()));
}
void jumpToCode(const ModelNode &modelNode)
{
QmlDesignerPlugin::instance()->viewManager().jumpToCodeInTextEditor(modelNode);
@@ -2009,6 +2016,36 @@ ModelNode handleItemLibraryEffectDrop(const QString &effectPath, const ModelNode
return newModelNode;
}
ModelNode handleImported3dAssetDrop(const QString &assetPath, const ModelNode &targetNode,
const QVector3D &position)
{
AbstractView *view = targetNode.view();
QTC_ASSERT(view, return {});
QTC_ASSERT(targetNode.isValid(), return {});
ModelNode newModelNode;
const GeneratedComponentUtils &compUtils = QmlDesignerPlugin::instance()->documentManager()
.generatedComponentUtils();
Utils::FilePath qmlFile = compUtils.getImported3dQml(assetPath);
if (qmlFile.exists()) {
TypeName qmlType = qmlFile.baseName().toUtf8();
QString importName = compUtils.getImported3dImportName(qmlFile);
if (!importName.isEmpty() && !qmlType.isEmpty())
newModelNode = QmlVisualNode::createQml3DNode(view, qmlType, targetNode, importName, position);
} else {
QMessageBox msgBox;
msgBox.setText(Tr::tr("Asset %1 is not complete.").arg(qmlFile.baseName()));
msgBox.setInformativeText(Tr::tr("Please reimport the asset."));
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.setIcon(QMessageBox::Information);
msgBox.exec();
}
return newModelNode;
}
void handleTextureDrop(const QMimeData *mimeData, const ModelNode &targetModelNode)
{
AbstractView *view = targetModelNode.view();

View File

@@ -8,6 +8,8 @@
#include <utils/filepath.h>
#include <QVector3D>
namespace QmlDesigner {
class AddFilesResult
@@ -138,10 +140,14 @@ bool validateEffect(const QString &effectPath);
bool isEffectComposerActivated();
QMLDESIGNERCOMPONENTS_EXPORT Utils::FilePath getImagesDefaultDirectory();
Utils::FilePath getImported3dDefaultDirectory();
//Item Library and Assets related drop operations
QMLDESIGNERCOMPONENTS_EXPORT ModelNode handleItemLibraryEffectDrop(const QString &effectPath,
const ModelNode &targetNode);
ModelNode handleImported3dAssetDrop(const QString &assetPath,
const ModelNode &targetNode,
const QVector3D &position = {});
void handleTextureDrop(const QMimeData *mimeData, const ModelNode &targetModelNode);
void handleMaterialDrop(const QMimeData *mimeData, const ModelNode &targetNode);
ModelNode handleItemLibraryImageDrop(const QString &imagePath,

View File

@@ -68,7 +68,7 @@ public:
#endif
, formEditorView{externalDependencies}
, textEditorView{externalDependencies}
, assetsLibraryView{externalDependencies}
, assetsLibraryView{imageCache, externalDependencies}
, itemLibraryView(imageCache, externalDependencies)
, navigatorView{externalDependencies}
, propertyEditorView(imageCache, externalDependencies)

View File

@@ -46,5 +46,7 @@
<file>images/align_view_on@2x.png</file>
<file>images/color_palette.png</file>
<file>images/color_palette@2x.png</file>
<file>images/item-3D_model-icon.png</file>
<file>images/item-3D_model-icon@2x.png</file>
</qresource>
</RCC>

View File

@@ -35,6 +35,8 @@
#include <coreplugin/icore.h>
#include <coreplugin/messagebox.h>
#include <qmldesignerutils/asset.h>
#include <projectexplorer/target.h>
#include <projectexplorer/kit.h>
@@ -305,7 +307,11 @@ void Edit3DView::modelAttached(Model *model)
if (QtSupport::QtVersion *qtVer = QtSupport::QtKitAspect::qtVersion(target->kit()))
m_isBakingLightsSupported = qtVer->qtVersion() >= QVersionNumber(6, 5, 0);
}
#ifndef QDS_USE_PROJECTSTORAGE
#ifdef QDS_USE_PROJECTSTORAGE
// TODO: Handle actual entries changed signal/notification once it is available.
// Until then, we simply get what entries are available at model attach time.
onEntriesChanged();
#else
connect(model->metaInfo().itemLibraryInfo(),
&ItemLibraryInfo::entriesChanged,
this,
@@ -333,15 +339,14 @@ void Edit3DView::handleEntriesChanged()
enum ItemLibraryEntryKeys : int { // used to maintain order
EK_cameras,
EK_lights,
EK_primitives,
EK_importedModels
EK_primitives
};
QMap<ItemLibraryEntryKeys, ItemLibraryDetails> entriesMap{
{EK_cameras, {tr("Cameras"), contextIcon(DesignerIcons::CameraIcon)}},
{EK_lights, {tr("Lights"), contextIcon(DesignerIcons::LightIcon)}},
{EK_primitives, {tr("Primitives"), contextIcon(DesignerIcons::PrimitivesIcon)}},
{EK_importedModels, {tr("Imported Models"), contextIcon(DesignerIcons::ImportedModelsIcon)}}};
{EK_primitives, {tr("Primitives"), contextIcon(DesignerIcons::PrimitivesIcon)}}
};
#ifdef QDS_USE_PROJECTSTORAGE
auto append = [&](const NodeMetaInfo &metaInfo, ItemLibraryEntryKeys key) {
@@ -356,16 +361,6 @@ void Edit3DView::handleEntriesChanged()
append(model()->qtQuick3DPointLightMetaInfo(), EK_lights);
append(model()->qtQuick3DOrthographicCameraMetaInfo(), EK_cameras);
append(model()->qtQuick3DPerspectiveCameraMetaInfo(), EK_cameras);
Utils::PathString import3dTypePrefix = QmlDesignerPlugin::instance()
->documentManager()
.generatedComponentUtils()
.import3dTypePrefix();
auto assetsModule = model()->module(import3dTypePrefix, Storage::ModuleKind::QmlLibrary);
for (const auto &metaInfo : model()->metaInfosForModule(assetsModule))
append(metaInfo, EK_importedModels);
#else
const QList<ItemLibraryEntry> itemLibEntries = model()->metaInfo().itemLibraryInfo()->entries();
for (const ItemLibraryEntry &entry : itemLibEntries) {
@@ -379,13 +374,6 @@ void Edit3DView::handleEntriesChanged()
} else if (entry.typeName() == "QtQuick3D.OrthographicCamera"
|| entry.typeName() == "QtQuick3D.PerspectiveCamera") {
entryKey = EK_cameras;
} else if (entry.typeName().startsWith(QmlDesignerPlugin::instance()
->documentManager()
.generatedComponentUtils()
.import3dTypePrefix()
.toUtf8())
&& NodeHints::fromItemLibraryEntry(entry, model()).canBeDroppedInView3D()) {
entryKey = EK_importedModels;
} else {
continue;
}
@@ -464,6 +452,8 @@ void Edit3DView::customNotification([[maybe_unused]] const AbstractView *view,
self->m_nodeAtPosReqType = NodeAtPosReqType::MainScenePick;
self->m_pickView3dNode = self->modelNodeForInternalId(qint32(data[1].toInt()));
});
} else if (identifier == "asset_import_finished" || identifier == "assets_deleted") {
handleEntriesChanged();
}
}
@@ -511,9 +501,22 @@ void Edit3DView::nodeAtPosReady(const ModelNode &modelNode, const QVector3D &pos
emitCustomNotification("apply_texture_to_model3D", {modelNode, m_droppedModelNode});
} else if (m_nodeAtPosReqType == NodeAtPosReqType::AssetDrop) {
bool isModel = modelNode.metaInfo().isQtQuick3DModel();
if (!m_droppedFile.isEmpty() && isModel) {
if (!m_droppedTexture.isEmpty() && isModel) {
QmlDesignerPlugin::instance()->mainWidget()->showDockWidget("MaterialBrowser");
emitCustomNotification("apply_asset_to_model3D", {modelNode}, {m_droppedFile}); // To MaterialBrowserView
emitCustomNotification("apply_asset_to_model3D", {modelNode}, {m_droppedTexture}); // To MaterialBrowserView
} else if (!m_dropped3dImports.isEmpty()) {
ModelNode sceneNode = Utils3D::active3DSceneNode(this);
if (!sceneNode.isValid())
sceneNode = rootModelNode();
ModelNode createdNode;
executeInTransaction(__FUNCTION__, [&] {
for (const QString &asset : std::as_const(m_dropped3dImports)) {
createdNode = ModelNodeOperations::handleImported3dAssetDrop(
asset, sceneNode, pos3d);
}
});
if (createdNode.isValid())
setSelectedModelNode(createdNode);
}
} else if (m_nodeAtPosReqType == NodeAtPosReqType::MainScenePick) {
if (modelNode.isValid())
@@ -524,7 +527,8 @@ void Edit3DView::nodeAtPosReady(const ModelNode &modelNode, const QVector3D &pos
}
m_droppedModelNode = {};
m_droppedFile.clear();
m_dropped3dImports.clear();
m_droppedTexture.clear();
m_nodeAtPosReqType = NodeAtPosReqType::None;
}
@@ -1462,10 +1466,22 @@ void Edit3DView::dropComponent(const ItemLibraryEntry &entry, const QPointF &pos
nodeAtPosReady({}, {}); // No need to actually resolve position for non-node items
}
void Edit3DView::dropAsset(const QString &file, const QPointF &pos)
void QmlDesigner::Edit3DView::dropAssets(const QList<QUrl> &urls, const QPointF &pos)
{
m_nodeAtPosReqType = NodeAtPosReqType::AssetDrop;
m_droppedFile = file;
m_dropped3dImports.clear();
for (const QUrl &url : urls) {
Asset asset(url.toLocalFile());
// For textures we only support single drops
if (m_dropped3dImports.isEmpty() && asset.isTexture3D()) {
m_droppedTexture = asset.fileName();
break;
} else if (asset.isImported3D()) {
m_dropped3dImports.append(asset.id());
}
}
emitView3DAction(View3DActionType::GetNodeAtPos, pos);
}

View File

@@ -101,7 +101,7 @@ public:
void dropBundleItem(const QPointF &pos);
void dropTexture(const ModelNode &textureNode, const QPointF &pos);
void dropComponent(const ItemLibraryEntry &entry, const QPointF &pos);
void dropAsset(const QString &file, const QPointF &pos);
void dropAssets(const QList<QUrl> &urls, const QPointF &pos);
bool isBakingLightsSupported() const;
@@ -203,7 +203,8 @@ private:
ModelCache<QImage> m_canvasCache;
ModelNode m_droppedModelNode;
ItemLibraryEntry m_droppedEntry;
QString m_droppedFile;
QStringList m_dropped3dImports;
QString m_droppedTexture;
NodeAtPosReqType m_nodeAtPosReqType;
QPoint m_contextMenuPosMouse;
QVector3D m_contextMenuPos3D;

View File

@@ -17,10 +17,8 @@
#include <designmodewidget.h>
#include <externaldependenciesinterface.h>
#include <generatedcomponentutils.h>
#include <import.h>
#include <materialutils.h>
#include <metainfo.h>
#include <modelnodeoperations.h>
#include <nodeabstractproperty.h>
#include <nodehints.h>
#include <nodeinstanceview.h>
@@ -455,6 +453,7 @@ void Edit3DWidget::updateCreateSubMenu(const QList<ItemLibraryDetails> &entriesL
m_createSubMenu->deleteLater();
}
m_nameToImport.clear();
m_nameToEntry.clear();
m_createSubMenu = new QmlEditorMenu(tr("Create"), m_contextMenu);
@@ -493,9 +492,43 @@ void Edit3DWidget::updateCreateSubMenu(const QList<ItemLibraryDetails> &entriesL
QAction *action = catMenu->addAction(getEntryIcon(entry), entry.name());
connect(action, &QAction::triggered, this, [this, action] { onCreateAction(action); });
action->setData(entry.name());
Import import = Import::createLibraryImport(entry.requiredImport(),
QString::number(entry.majorVersion())
+ QLatin1Char('.')
+ QString::number(entry.minorVersion()));
m_nameToImport.insert(entry.name(), import);
m_nameToEntry.insert(entry.name(), entry);
}
}
// Create menu for imported 3d models, which don't have ItemLibraryEntries
const GeneratedComponentUtils &compUtils
= QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
QList<Utils::FilePath> qmlFiles = compUtils.imported3dComponents();
QMenu *catMenu = nullptr;
if (!qmlFiles.isEmpty()) {
catMenu = new QmlEditorMenu(tr("Imported Models"), m_createSubMenu);
catMenu->setIcon(contextIcon(DesignerIcons::ImportedModelsIcon));
m_createSubMenu->addMenu(catMenu);
std::ranges::sort(qmlFiles, {}, &Utils::FilePath::baseName);
const QIcon icon = QIcon(":/edit3d/images/item-3D_model-icon.png");
for (const Utils::FilePath &qmlFile : std::as_const(qmlFiles)) {
QString qmlName = qmlFile.baseName();
QAction *action = catMenu->addAction(icon, qmlName);
connect(action, &QAction::triggered, this, [this, action] { onCreateAction(action); });
action->setData(qmlName);
QString importName = compUtils.getImported3dImportName(qmlFile);
if (!importName.isEmpty()) {
Import import = Import::createLibraryImport(importName);
m_nameToImport.insert(qmlName, import);
}
}
}
}
// Action triggered from the "create" sub-menu
@@ -505,17 +538,29 @@ void Edit3DWidget::onCreateAction(QAction *action)
return;
m_view->executeInTransaction(__FUNCTION__, [&] {
ItemLibraryEntry entry = m_nameToEntry.value(action->data().toString());
Import import = Import::createLibraryImport(entry.requiredImport(),
QString::number(entry.majorVersion())
+ QLatin1Char('.')
+ QString::number(entry.minorVersion()));
const QString actionName = action->data().toString();
Import import = m_nameToImport.value(actionName);
if (!m_view->model()->hasImport(import, true, true))
m_view->model()->changeImports({import}, {});
int activeScene = Utils3D::active3DSceneId(m_view->model());
auto modelNode = QmlVisualNode::createQml3DNode(m_view, entry,
activeScene, m_contextMenuPos3d).modelNode();
ModelNode modelNode;
if (m_nameToEntry.contains(actionName)) {
int activeScene = Utils3D::active3DSceneId(m_view->model());
ItemLibraryEntry entry = m_nameToEntry.value(actionName);
modelNode = QmlVisualNode::createQml3DNode(m_view, entry, activeScene,
m_contextMenuPos3d).modelNode();
} else {
ModelNode sceneNode = Utils3D::active3DSceneNode(m_view);
if (!sceneNode.isValid())
sceneNode = m_view->rootModelNode();
modelNode = QmlVisualNode::QmlVisualNode::createQml3DNode(
m_view, actionName.toUtf8(), sceneNode, import.url(),
m_contextMenuPos3d).modelNode();
}
QTC_ASSERT(modelNode.isValid(), return);
m_view->setSelectedModelNode(modelNode);
@@ -717,9 +762,12 @@ void Edit3DWidget::dragEnterEvent(QDragEnterEvent *dragEnterEvent)
if (dragEnterEvent->mimeData()->hasFormat(Constants::MIME_TYPE_ASSETS)
|| dragEnterEvent->mimeData()->hasFormat(Constants::MIME_TYPE_BUNDLE_TEXTURE)) {
const auto urls = dragEnterEvent->mimeData()->urls();
if (!urls.isEmpty()) {
if (Asset(urls.first().toLocalFile()).isValidTextureSource())
for (const QUrl &url : urls) {
Asset asset(url.toLocalFile());
if (asset.isImported3D() || asset.isTexture3D()) {
dragEnterEvent->acceptProposedAction();
break;
}
}
} else if (actionManager.externalDragHasSupportedAssets(dragEnterEvent->mimeData())
|| dragEnterEvent->mimeData()->hasFormat(Constants::MIME_TYPE_MATERIAL)
@@ -787,7 +835,7 @@ void Edit3DWidget::dropEvent(QDropEvent *dropEvent)
// handle dropping image assets
if (dropEvent->mimeData()->hasFormat(Constants::MIME_TYPE_ASSETS)
|| dropEvent->mimeData()->hasFormat(Constants::MIME_TYPE_BUNDLE_TEXTURE)) {
m_view->dropAsset(dropEvent->mimeData()->urls().first().toLocalFile(), pos);
m_view->dropAssets(dropEvent->mimeData()->urls(), pos);
m_view->model()->endDrag();
return;
}

View File

@@ -3,6 +3,7 @@
#pragma once
#include <bundlehelper.h>
#include <import.h>
#include <itemlibraryentry.h>
#include <modelnode.h>
@@ -111,6 +112,7 @@ private:
QPointer<QMenu> m_createSubMenu;
ModelNode m_contextMenuTarget;
QVector3D m_contextMenuPos3d;
QHash<QString, Import> m_nameToImport;
QHash<QString, ItemLibraryEntry> m_nameToEntry;
ItemLibraryEntry m_draggedEntry;
QHash<QAction *, Core::Command *> m_actionToCommandHash;

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

View File

@@ -398,9 +398,6 @@ void Import3dImporter::postParseQuick3DAsset(ParseData &pd)
qmlInfo.append(".");
qmlInfo.append(pd.assetName);
qmlInfo.append('\n');
const QString reqImp = generateRequiredImportForAsset(pd.assetName);
if (!m_requiredImports.contains(reqImp))
m_requiredImports.append(reqImp);
while (qmlIt.hasNext()) {
qmlIt.next();
QFileInfo fi = QFileInfo(qmlIt.filePath());
@@ -454,8 +451,10 @@ void Import3dImporter::postParseQuick3DAsset(ParseData &pd)
}
// Add quick3D import unless it is already added
if (impVersionMajor > 0 && m_requiredImports.first() != "QtQuick3D")
if (impVersionMajor > 0 && (m_requiredImports.isEmpty()
|| m_requiredImports.first() != "QtQuick3D")) {
m_requiredImports.prepend("QtQuick3D");
}
}
}
}
@@ -577,13 +576,6 @@ QString Import3dImporter::generateAssetFolderName(const QString &assetName) cons
return assetName + "_QDS_" + QString::number(counter++);
}
QString Import3dImporter::generateRequiredImportForAsset(const QString &assetName) const
{
return QStringLiteral("%1.%2").arg(
QmlDesignerPlugin::instance()->documentManager()
.generatedComponentUtils().import3dTypePrefix(), assetName);
}
Import3dImporter::OverwriteResult Import3dImporter::confirmAssetOverwrite(const QString &assetName)
{
const QString title = tr("Overwrite Existing Asset?");
@@ -744,9 +736,6 @@ void Import3dImporter::finalizeQuick3DImport()
addError(tr("Failed to insert import statement into qml document."));
transaction.commit();
#else
// TODO: ModelUtils::addImportsWithCheck requires Model::possibleImports()
// to return correct list instead of empty list, so until that is
// fixed we need to just trust the missing modules are available
const Imports &imports = model->imports();
Imports importsToAdd;
for (const QString &importName : std::as_const(m_requiredImports)) {
@@ -775,6 +764,7 @@ void Import3dImporter::finalizeQuick3DImport()
}
timer->stop();
notifyFinished();
model->rewriterView()->emitCustomNotification("asset_import_finished");
}
} else {
timer->stop();
@@ -791,7 +781,6 @@ void Import3dImporter::removeAssetFromImport(const QString &assetName)
{
m_parseData.remove(assetName);
m_importFiles.remove(assetName);
m_requiredImports.removeOne(generateRequiredImportForAsset(assetName));
}
QString Import3dImporter::sourceSceneTargetFilePath(const ParseData &pd)

View File

@@ -685,6 +685,55 @@ RewriterView *DesignDocument::rewriterView() const
return m_rewriterView.get();
}
#ifndef QDS_USE_PROJECTSTORAGE
static void removeUnusedImports(RewriterView *rewriter)
{
// Remove any import statements for asset based nodes (composed effect or imported3d)
// if there is no nodes using them in the scene.
QTC_ASSERT(rewriter && rewriter->model(), return);
GeneratedComponentUtils compUtils{rewriter->externalDependencies()};
const QString effectPrefix = compUtils.composedEffectsTypePrefix();
const QString imported3dPrefix = compUtils.import3dTypePrefix();
const QList<Utils::FilePath> qmlFiles = compUtils.imported3dComponents();
QHash<QString, QString> m_imported3dTypeMap;
for (const Utils::FilePath &qmlFile : qmlFiles) {
QString importName = compUtils.getImported3dImportName(qmlFile);
QString type = qmlFile.baseName();
m_imported3dTypeMap.insert(importName, type);
}
const Imports &imports = rewriter->model()->imports();
QHash<QString, Import> assetImports;
for (const Import &import : imports) {
if (import.url().startsWith(effectPrefix)) {
QString type = import.url().split('.').last();
assetImports.insert(type, import);
} else if (import.url().startsWith(imported3dPrefix)) {
assetImports.insert(m_imported3dTypeMap[import.url()], import);
}
}
const QList<ModelNode> allNodes = rewriter->allModelNodes();
for (const ModelNode &node : allNodes) {
if (QmlItemNode(node).isEffectItem()
|| (node.isComponent() && node.metaInfo().isQtQuick3DNode())) {
assetImports.remove(node.simplifiedTypeName());
}
}
if (!assetImports.isEmpty()) {
Imports removeImports;
for (const Import &import : assetImports)
removeImports.append(import);
rewriter->model()->changeImports({}, removeImports);
}
rewriter->forceAmend();
}
#endif
void DesignDocument::setEditor(Core::IEditor *editor)
{
m_textEditor = editor;
@@ -694,6 +743,12 @@ void DesignDocument::setEditor(Core::IEditor *editor)
this, [this](Core::IDocument *document) {
if (m_textEditor && m_textEditor->document() == document) {
if (m_documentModel && m_documentModel->rewriterView()) {
#ifdef QDS_USE_PROJECTSTORAGE
// TODO: ProjectStorage should handle this via Model somehow (QDS-14519)
#else
removeUnusedImports(rewriterView());
#endif
m_documentModel->rewriterView()->writeAuxiliaryData();
}
}

View File

@@ -19,9 +19,6 @@ QString ItemLibraryImport::importName() const
if (m_sectionType == SectionType::User)
return userComponentsTitle();
if (m_sectionType == SectionType::Quick3DAssets)
return quick3DAssetsTitle();
if (m_sectionType == SectionType::Unimported)
return unimportedComponentsTitle();
@@ -39,9 +36,6 @@ QString ItemLibraryImport::importUrl() const
if (m_sectionType == SectionType::User)
return userComponentsTitle();
if (m_sectionType == SectionType::Quick3DAssets)
return quick3DAssetsTitle();
if (m_sectionType == SectionType::Unimported)
return unimportedComponentsTitle();
@@ -61,9 +55,6 @@ QString ItemLibraryImport::sortingName() const
if (m_sectionType == SectionType::User)
return "_"; // user components always come first
if (m_sectionType == SectionType::Quick3DAssets)
return "__"; // Quick3DAssets come second
if (m_sectionType == SectionType::Unimported)
return "zzzzzz"; // Unimported components come last
@@ -235,12 +226,6 @@ QString ItemLibraryImport::userComponentsTitle()
return tr("My Components");
}
// static
QString ItemLibraryImport::quick3DAssetsTitle()
{
return tr("My 3D Components");
}
// static
QString ItemLibraryImport::unimportedComponentsTitle()
{

View File

@@ -28,7 +28,6 @@ public:
enum class SectionType {
Default,
User,
Quick3DAssets,
Unimported
};
@@ -67,7 +66,6 @@ public:
bool importUnimported() const { return m_sectionType == SectionType::Unimported; }
static QString userComponentsTitle();
static QString quick3DAssetsTitle();
static QString unimportedComponentsTitle();
SectionType sectionType() const;

View File

@@ -319,12 +319,7 @@ void ItemLibraryModel::update(Model *model)
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
QStringList excludedImports {
projectName,
compUtils.materialsBundleType(),
compUtils.effectsBundleType(),
compUtils.userMaterialsBundleType(),
compUtils.user3DBundleType(),
compUtils.userEffectsBundleType()
projectName
};
// create import sections
@@ -332,23 +327,17 @@ void ItemLibraryModel::update(Model *model)
QHash<QString, ItemLibraryImport *> importHash;
for (const Import &import : model->imports()) {
if (excludedImports.contains(import.url())
|| import.url().startsWith(compUtils.composedEffectsTypePrefix())) {
|| import.url().startsWith(compUtils.generatedComponentTypePrefix())) {
continue;
}
bool addNew = true;
bool isQuick3DAsset = import.url().startsWith(compUtils.import3dTypePrefix());
QString importUrl = import.url();
if (isQuick3DAsset)
importUrl = ItemLibraryImport::quick3DAssetsTitle();
else if (import.isFileImport())
if (import.isFileImport())
importUrl = import.toString(true, true).remove("\"");
ItemLibraryImport *oldImport = importHash.value(importUrl);
if (oldImport && oldImport->sectionType() == ItemLibraryImport::SectionType::Quick3DAssets
&& isQuick3DAsset) {
addNew = false; // add only 1 Quick3DAssets import section
} else if (oldImport && oldImport->importEntry().url() == import.url()) {
if (oldImport && oldImport->importEntry().url() == import.url()) {
// Retain the higher version if multiples exist
if (oldImport->importEntry().toVersion() >= import.toVersion() || import.hasVersion())
addNew = false;
@@ -357,8 +346,7 @@ void ItemLibraryModel::update(Model *model)
}
if (addNew) {
auto sectionType = isQuick3DAsset ? ItemLibraryImport::SectionType::Quick3DAssets
: ItemLibraryImport::SectionType::Default;
auto sectionType = ItemLibraryImport::SectionType::Default;
ItemLibraryImport *itemLibImport = new ItemLibraryImport(import, this, sectionType);
itemLibImport->setImportUsed(usedImports.contains(import));
importHash.insert(importUrl, itemLibImport);
@@ -445,8 +433,6 @@ void ItemLibraryModel::update(Model *model)
importSection = importHash[entry.requiredImport()];
}
} else if (catName == ItemLibraryImport::quick3DAssetsTitle()) {
importSection = importHash[ItemLibraryImport::quick3DAssetsTitle()];
} else {
if (catName.contains("Qt Quick - ")) {
QString sortingName = catName;
@@ -549,8 +535,6 @@ ItemLibraryImport *ItemLibraryModel::importByUrl(const QString &importUrl) const
|| (importUrl.isEmpty() && itemLibraryImport->importUrl() == "QtQuick")
|| (importUrl == ItemLibraryImport::userComponentsTitle()
&& itemLibraryImport->sectionType() == ItemLibraryImport::SectionType::User)
|| (importUrl == ItemLibraryImport::quick3DAssetsTitle()
&& itemLibraryImport->sectionType() == ItemLibraryImport::SectionType::Quick3DAssets)
|| (importUrl == ItemLibraryImport::unimportedComponentsTitle()
&& itemLibraryImport->sectionType() == ItemLibraryImport::SectionType::Unimported)) {
return itemLibraryImport;

View File

@@ -1015,7 +1015,7 @@ void MaterialEditorView::modelNodePreviewPixmapChanged(const ModelNode &node,
const QPixmap &pixmap,
const QByteArray &requestId)
{
if (node != m_selectedMaterial || requestId != m_previewRequestId)
if (!m_qmlBackEnd || node != m_selectedMaterial || requestId != m_previewRequestId)
return;
m_qmlBackEnd->updateMaterialPreview(pixmap);

View File

@@ -239,6 +239,10 @@ void NameItemDelegate::paint(QPainter *painter,
model->qtQuickBorderImageMetaInfo());
} else if (dragType == Constants::MIME_TYPE_ASSET_EFFECT) {
validDrop = metaInfo.isBasedOn(node.model()->qtQuickItemMetaInfo());
} else if (dragType == Constants::MIME_TYPE_ASSET_IMPORTED3D) {
Model *model = node.model();
validDrop = metaInfo.isBasedOn(model->qtQuick3DNodeMetaInfo(),
model->qtQuick3DView3DMetaInfo());
} else {
const NodeMetaInfo dragInfo = node.model()->metaInfo(dragType);
ChooseFromPropertyListFilter *filter = new ChooseFromPropertyListFilter(dragInfo, metaInfo, true);

View File

@@ -669,6 +669,10 @@ bool NavigatorTreeModel::dropMimeData(const QMimeData *mimeData,
currNode = ModelNodeOperations::handleItemLibraryEffectDrop(
assetPath, modelNodeForIndex(rowModelIndex));
moveNodesAfter = false;
} else if (assetType == Constants::MIME_TYPE_ASSET_IMPORTED3D) {
currNode = ModelNodeOperations::handleImported3dAssetDrop(
assetPath, modelNodeForIndex(rowModelIndex));
moveNodesAfter = false;
}
if (currNode.isValid())

View File

@@ -306,6 +306,9 @@ void NavigatorView::dragStarted(QMimeData *mimeData)
if (assetType == Constants::MIME_TYPE_ASSET_EFFECT) {
m_widget->setDragType(Constants::MIME_TYPE_ASSET_EFFECT);
m_widget->update();
} else if (assetType == Constants::MIME_TYPE_ASSET_IMPORTED3D) {
m_widget->setDragType(Constants::MIME_TYPE_ASSET_IMPORTED3D);
m_widget->update();
} else if (assetType == Constants::MIME_TYPE_ASSET_TEXTURE3D) {
m_widget->setDragType(Constants::MIME_TYPE_ASSET_TEXTURE3D);
m_widget->update();

View File

@@ -337,4 +337,61 @@ QString GeneratedComponentUtils::user3DBundleType() const
return componentBundlesTypePrefix() + '.' + user3DBundleId();
}
QList<Utils::FilePath> GeneratedComponentUtils::imported3dComponents() const
{
auto import3dPath = Utils::FilePath::fromString(import3dTypePath());
auto projPath = Utils::FilePath::fromString(m_externalDependencies.currentProjectDirPath());
auto fullPath = projPath.resolvePath(import3dPath);
if (fullPath.isEmpty())
return {};
return collectFiles(fullPath, "qml");
}
QString GeneratedComponentUtils::getImported3dImportName(const Utils::FilePath &qmlFile) const
{
const QStringList sl = qmlFile.toFSPathString().split('/');
int i = sl.size() - 4;
if (i >= 0)
return QStringView(u"%1.%2.%3").arg(sl[i], sl[i + 1], sl[i + 2]);
return {};
}
Utils::FilePath GeneratedComponentUtils::getImported3dQml(const QString &assetPath) const
{
Utils::FilePath assetFilePath = Utils::FilePath::fromString(assetPath);
const Utils::expected_str<QByteArray> data = assetFilePath.fileContents();
if (!data)
return {};
Utils::FilePath assetQmlFilePath = Utils::FilePath::fromUtf8(data.value());
Utils::FilePath projectPath = Utils::FilePath::fromString(m_externalDependencies.currentProjectDirPath());
assetQmlFilePath = projectPath.resolvePath(assetQmlFilePath);
return assetQmlFilePath;
}
// Recursively find files of certain suffix in a dir
QList<Utils::FilePath> GeneratedComponentUtils::collectFiles(const Utils::FilePath &dirPath,
const QString &suffix) const
{
if (dirPath.isEmpty())
return {};
QList<Utils::FilePath> files;
const Utils::FilePaths entryList = dirPath.dirEntries(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
for (const Utils::FilePath &entry : entryList) {
if (entry.isDir())
files.append(collectFiles(entry.absoluteFilePath(), suffix));
else if (entry.suffix() == suffix)
files.append(entry);
}
return files;
}
} // namespace QmlDesigner

View File

@@ -50,8 +50,14 @@ public:
QString userEffectsBundleType() const;
QString user3DBundleType() const;
QList<Utils::FilePath> imported3dComponents() const;
QString getImported3dImportName(const Utils::FilePath &qmlFile) const;
Utils::FilePath getImported3dQml(const QString &assetPath) const;
private:
ExternalDependenciesInterface &m_externalDependencies;
QList<Utils::FilePath> collectFiles(const Utils::FilePath &dirPath, const QString &suffix) const;
};
} // namespace QmlDesigner

View File

@@ -182,6 +182,7 @@ public:
NodeMetaInfo qtQuick3DSpotLightMetaInfo() const;
NodeMetaInfo qtQuick3DTextureMetaInfo() const;
NodeMetaInfo qtQuick3DTextureInputMetaInfo() const;
NodeMetaInfo qtQuick3DView3DMetaInfo() const;
NodeMetaInfo qtQuickBorderImageMetaInfo() const;
NodeMetaInfo qtQuickControlsLabelMetaInfo() const;
NodeMetaInfo qtQuickControlsTextAreaMetaInfo() const;

View File

@@ -2613,6 +2613,16 @@ NodeMetaInfo Model::qtQuick3DTextureInputMetaInfo() const
}
}
NodeMetaInfo Model::qtQuick3DView3DMetaInfo() const
{
if constexpr (useProjectStorage()) {
using namespace Storage::Info;
return createNodeMetaInfo<QtQuick3D, View3D>();
} else {
return metaInfo("QtQuick3D.View3D");
}
}
NodeMetaInfo Model::qtQuickBorderImageMetaInfo() const
{
if constexpr (useProjectStorage()) {

View File

@@ -85,6 +85,12 @@ const QStringList &Asset::supportedEffectComposerSuffixes()
return retList;
}
const QStringList &Asset::supportedImported3dSuffixes()
{
static QStringList retList {"*.q3d"};
return retList;
}
const QSet<QString> &Asset::supportedSuffixes()
{
static QSet<QString> allSuffixes;
@@ -100,6 +106,7 @@ const QSet<QString> &Asset::supportedSuffixes()
insertSuffixes(supportedVideoSuffixes());
insertSuffixes(supportedTexture3DSuffixes());
insertSuffixes(supportedEffectComposerSuffixes());
insertSuffixes(supportedImported3dSuffixes());
}
return allSuffixes;
}
@@ -182,6 +189,11 @@ bool Asset::isEffect() const
return m_type == Asset::Type::Effect;
}
bool Asset::isImported3D() const
{
return m_type == Asset::Type::Imported3D;
}
const QString Asset::suffix() const
{
return m_suffix;
@@ -236,7 +248,8 @@ void Asset::resolveType()
m_type = Asset::Type::Texture3D;
else if (supportedEffectComposerSuffixes().contains(m_suffix))
m_type = Asset::Type::Effect;
}
else if (supportedImported3dSuffixes().contains(m_suffix))
m_type = Asset::Type::Imported3D;}
bool Asset::hasSuffix() const
{

View File

@@ -15,17 +15,20 @@ namespace QmlDesigner {
class QMLDESIGNERUTILS_EXPORT Asset
{
public:
enum Type { Unknown,
Image,
MissingImage,
FragmentShader,
VertexShader,
Font,
Audio,
Video,
Texture3D,
Effect,
Folder };
enum Type {
Unknown,
Image,
MissingImage,
FragmentShader,
VertexShader,
Font,
Audio,
Video,
Texture3D,
Effect,
Folder,
Imported3D
};
Asset(const QString &filePath);
@@ -38,6 +41,7 @@ public:
static const QStringList &supportedVideoSuffixes();
static const QStringList &supportedTexture3DSuffixes();
static const QStringList &supportedEffectComposerSuffixes();
static const QStringList &supportedImported3dSuffixes();
static const QSet<QString> &supportedSuffixes();
static bool isSupported(const QString &path);
@@ -59,6 +63,7 @@ public:
bool isHdrFile() const;
bool isKtxFile() const;
bool isEffect() const;
bool isImported3D() const;
bool isSupported() const;
bool isValidTextureSource();
bool isFolder() const;

View File

@@ -87,6 +87,7 @@ inline constexpr char MIME_TYPE_ASSET_TEXTURE3D[]
= "application/vnd.qtdesignstudio.asset.texture3d";
inline constexpr char MIME_TYPE_MODELNODE_LIST[] = "application/vnd.qtdesignstudio.modelnode.list";
inline constexpr char MIME_TYPE_ASSET_EFFECT[] = "application/vnd.qtdesignstudio.asset.effect";
inline constexpr char MIME_TYPE_ASSET_IMPORTED3D[] = "application/vnd.qtdesignstudio.asset.imported3d";
// Menus
inline constexpr char M_VIEW_WORKSPACES[] = "QmlDesigner.Menu.View.Workspaces";

View File

@@ -482,6 +482,58 @@ QmlVisualNode QmlVisualNode::createQml3DNode(AbstractView *view,
return createQmlObjectNode(view, itemLibraryEntry, position, sceneNodeProperty, createInTransaction).modelNode();
}
QmlVisualNode QmlVisualNode::createQml3DNode(AbstractView *view,
const TypeName &typeName,
const ModelNode &parentNode,
const QString &importName,
const QVector3D &position,
bool createInTransaction)
{
NodeAbstractProperty targetParentProperty = parentNode.defaultNodeListProperty();
QTC_ASSERT(targetParentProperty.isValid(), return {});
QTC_ASSERT(!typeName.isEmpty(), return {});
QmlVisualNode newQmlObjectNode;
auto createNodeFunc = [&]() {
if (!importName.isEmpty()) {
Import import = Import::createLibraryImport(importName);
view->model()->changeImports({import}, {});
}
QList<QPair<PropertyName, QVariant> > propertyPairList;
propertyPairList.append(Position(position).propertyPairList());
#ifdef QDS_USE_PROJECTSTORAGE
newQmlObjectNode = QmlVisualNode(view->createModelNode(typeName,
propertyPairList));
#else
NodeMetaInfo metaInfo = view->model()->metaInfo(typeName);
newQmlObjectNode = QmlVisualNode(view->createModelNode(typeName,
metaInfo.majorVersion(),
metaInfo.minorVersion(),
propertyPairList));
#endif
if (newQmlObjectNode.id().isEmpty()) {
newQmlObjectNode.modelNode().setIdWithoutRefactoring(
view->model()->generateNewId(QString::fromUtf8(typeName)));
}
if (targetParentProperty.isValid())
targetParentProperty.reparentHere(newQmlObjectNode);
};
if (createInTransaction)
view->executeInTransaction(__FUNCTION__, createNodeFunc);
else
createNodeFunc();
return newQmlObjectNode;
}
NodeListProperty QmlVisualNode::findSceneNodeProperty(AbstractView *view, qint32 sceneRootId)
{
QTC_ASSERT(view, return {});

View File

@@ -88,6 +88,13 @@ public:
qint32 sceneRootId = -1, const QVector3D &position = {},
bool createInTransaction = true);
static QmlVisualNode createQml3DNode(AbstractView *view,
const TypeName &typeName,
const ModelNode &parentNode,
const QString &importName = {},
const QVector3D &position = {},
bool createInTransaction = true);
static NodeListProperty findSceneNodeProperty(AbstractView *view, qint32 sceneRootId);
static bool isFlowTransition(const ModelNode &node);