Files
qt-creator/src/plugins/qmldesigner/components/assetslibrary/assetslibrarywidget.cpp
Miikka Heikkinen ede7969ea3 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>
2025-02-13 09:22:06 +00:00

848 lines
32 KiB
C++

// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "assetslibrarywidget.h"
#include "assetslibraryiconprovider.h"
#include "assetslibrarymodel.h"
#include "assetslibraryview.h"
#include <qmldesignertr.h>
#include <asynchronousimagecache.h>
#include <createtexture.h>
#include <designeractionmanager.h>
#include <designermcumanager.h>
#include <designerpaths.h>
#include <designmodewidget.h>
#include <hdrimage.h>
#include <import.h>
#include <modelnodeoperations.h>
#include <nodemetainfo.h>
#include <qmldesignerconstants.h>
#include <qmldesignerplugin.h>
#include <studioquickwidget.h>
#include <theme.h>
#include <uniquename.h>
#include <utils3d.h>
#include <coreplugin/fileutils.h>
#include <coreplugin/icore.h>
#include <coreplugin/messagebox.h>
#include <utils/algorithm.h>
#include <qmldesignerutils/asset.h>
#include <utils/environment.h>
#include <utils/fileutils.h>
#include <utils/qtcassert.h>
#include <QFileDialog>
#include <QFileInfo>
#include <QImageReader>
#include <QMessageBox>
#include <QMimeData>
#include <QMouseEvent>
#include <QPointF>
#include <QQmlContext>
#include <QQuickItem>
#include <QShortcut>
#include <QToolButton>
#include <QVBoxLayout>
#include <memory>
using namespace Core;
namespace QmlDesigner {
static QString propertyEditorResourcesPath()
{
#ifdef SHARE_QML_PATH
if (Utils::qtcEnvironmentVariableIsSet("LOAD_QML_FROM_SOURCE"))
return QLatin1String(SHARE_QML_PATH) + "/propertyEditorQmlSources";
#endif
return Core::ICore::resourcePath("qmldesigner/propertyEditorQmlSources").toString();
}
bool AssetsLibraryWidget::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::FocusOut) {
if (obj == m_assetsWidget->quickWidget())
QMetaObject::invokeMethod(m_assetsWidget->rootObject(), "handleViewFocusOut");
} else if (event->type() == QMouseEvent::MouseMove) {
if (!m_assetsToDrag.isEmpty() && m_assetsView->model()) {
QMouseEvent *me = static_cast<QMouseEvent *>(event);
if ((me->globalPosition().toPoint() - m_dragStartPoint).manhattanLength() > 10) {
auto mimeData = std::make_unique<QMimeData>();
mimeData->setData(Constants::MIME_TYPE_ASSETS, m_assetsToDrag.join(',').toUtf8());
QList<QUrl> urlsToDrag = Utils::transform(m_assetsToDrag, &QUrl::fromLocalFile);
QString draggedAsset = m_assetsToDrag[0];
m_assetsToDrag.clear();
mimeData->setUrls(urlsToDrag);
m_assetsView->model()->startDrag(std::move(mimeData),
m_assetsIconProvider->requestPixmap(draggedAsset,
nullptr,
{128, 128}),
this);
}
}
} else if (event->type() == QMouseEvent::MouseButtonRelease) {
m_assetsToDrag.clear();
setIsDragging(false);
}
return QObject::eventFilter(obj, event);
}
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)}
, m_assetsView{view}
, m_assetsWidget{Utils::makeUniqueObjectPtr<StudioQuickWidget>(this)}
{
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);
// We want font images to have custom size, so don't scale them in the tooltip
m_fontPreviewTooltipBackend->setScaleImage(false);
// Note: Though the text specified here appears in UI, it shouldn't be translated, as it's
// a commonly used sentence to preview the font glyphs in latin fonts.
// For fonts that do not have latin glyphs, the font family name will have to suffice for preview.
m_fontPreviewTooltipBackend->setAuxiliaryData(
ImageCache::FontCollectorSizeAuxiliaryData{QSize{300, 150},
Theme::getColor(Theme::DStextColor).name(),
QStringLiteral("The quick brown fox jumps\n"
"over the lazy dog\n"
"1234567890")});
// create assets widget
m_assetsWidget->quickWidget()->setObjectName(Constants::OBJECT_NAME_ASSET_LIBRARY);
m_assetsWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
Theme::setupTheme(m_assetsWidget->engine());
m_assetsWidget->engine()->addImportPath(propertyEditorResourcesPath() + "/imports");
m_assetsWidget->setClearColor(Theme::getColor(Theme::Color::QmlDesigner_BackgroundColorDarkAlternate));
m_assetsWidget->engine()->addImageProvider("qmldesigner_assets", m_assetsIconProvider);
connect(m_assetsModel, &AssetsLibraryModel::fileChanged,
QmlDesignerPlugin::instance(), &QmlDesignerPlugin::assetChanged);
connect(m_assetsModel, &AssetsLibraryModel::generatedAssetsDeleted,
this, &AssetsLibraryWidget::handleDeletedGeneratedAssets);
auto layout = new QVBoxLayout(this);
layout->setContentsMargins({});
layout->setSpacing(0);
layout->addWidget(m_assetsWidget.get());
updateSearch();
setStyleSheet(Theme::replaceCssColors(
QString::fromUtf8(Utils::FileReader::fetchQrc(":/qmldesigner/stylesheet.css"))));
m_qmlSourceUpdateShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_F6), this);
connect(m_qmlSourceUpdateShortcut, &QShortcut::activated, this, &AssetsLibraryWidget::reloadQmlSource);
connect(this,
&AssetsLibraryWidget::extFilesDrop,
this,
&AssetsLibraryWidget::handleExtFilesDrop,
Qt::QueuedConnection);
QmlDesignerPlugin::trackWidgetFocusTime(this, Constants::EVENT_ASSETSLIBRARY_TIME);
auto map = m_assetsWidget->registerPropertyMap("AssetsLibraryBackend");
map->setProperties({{"assetsModel", QVariant::fromValue(m_assetsModel)},
{"rootView", QVariant::fromValue(this)},
{"tooltipBackend", QVariant::fromValue(m_fontPreviewTooltipBackend.get())}});
// init the first load of the QML UI elements
reloadQmlSource();
setFocusProxy(m_assetsWidget->quickWidget());
IContext::attach(this,
Context(Constants::qmlAssetsLibraryContextId, Constants::qtQuickToolsMenuContextId),
[this](const IContext::HelpCallback &callback) { contextHelp(callback); });
}
AssetsLibraryWidget::~AssetsLibraryWidget() = default;
void AssetsLibraryWidget::contextHelp(const Core::IContext::HelpCallback &callback) const
{
if (m_assetsView)
QmlDesignerPlugin::contextHelp(callback, m_assetsView->contextHelpId());
else
callback({});
}
void AssetsLibraryWidget::deleteSelectedAssets()
{
emit deleteSelectedAssetsRequested();
}
QString AssetsLibraryWidget::getUniqueEffectPath(const QString &parentFolder, const QString &effectName)
{
QString effectsDir = ModelNodeOperations::getEffectsDefaultDirectory(parentFolder);
QString effectPath = QLatin1String("%1/%2.qep").arg(effectsDir, effectName);
return UniqueName::generatePath(effectPath);
}
bool AssetsLibraryWidget::createNewEffect(const QString &effectPath, bool openInEffectComposer)
{
bool created = QFile(effectPath).open(QIODevice::WriteOnly);
if (created && openInEffectComposer) {
openEffectComposer(effectPath);
emit directoryCreated(QFileInfo(effectPath).absolutePath());
}
return created;
}
bool AssetsLibraryWidget::isEffectsCreationAllowed() const
{
if (!Core::ICore::isQtDesignStudio() || DesignerMcuManager::instance().isMCUProject())
return false;
#ifdef LICENSECHECKER
return checkLicense() == FoundLicense::enterprise;
#else
return true;
#endif
}
void AssetsLibraryWidget::showInGraphicalShell(const QString &path)
{
Core::FileUtils::showInGraphicalShell(Core::ICore::dialogParent(), Utils::FilePath::fromString(path));
}
QString AssetsLibraryWidget::showInGraphicalShellMsg() const
{
return Core::FileUtils::msgGraphicalShellAction();
}
int AssetsLibraryWidget::qtVersion() const
{
return QT_VERSION;
}
void AssetsLibraryWidget::addTextures(const QStringList &filePaths)
{
m_assetsView->executeInTransaction(__FUNCTION__, [&] {
CreateTexture(m_assetsView)
.execute(filePaths,
AddTextureMode::Texture,
Utils3D::active3DSceneId(m_assetsView->model()));
});
}
void AssetsLibraryWidget::addLightProbe(const QString &filePath)
{
m_assetsView->executeInTransaction(__FUNCTION__, [&] {
CreateTexture(m_assetsView)
.execute(filePath,
AddTextureMode::LightProbe,
Utils3D::active3DSceneId(m_assetsView->model()));
});
}
void AssetsLibraryWidget::updateContextMenuActionsEnableState()
{
setHasMaterialLibrary(Utils3D::materialLibraryNode(m_assetsView).isValid()
&& m_assetsView->model()->hasImport("QtQuick3D"));
ModelNode activeSceneEnv = Utils3D::resolveSceneEnv(m_assetsView,
Utils3D::active3DSceneId(
m_assetsView->model()));
setHasSceneEnv(activeSceneEnv.isValid());
setCanCreateEffects(isEffectsCreationAllowed());
}
void AssetsLibraryWidget::setHasMaterialLibrary(bool enable)
{
if (m_hasMaterialLibrary == enable)
return;
m_hasMaterialLibrary = enable;
emit hasMaterialLibraryChanged();
}
bool AssetsLibraryWidget::hasMaterialLibrary() const
{
return m_hasMaterialLibrary;
}
void AssetsLibraryWidget::setHasSceneEnv(bool b)
{
if (b == m_hasSceneEnv)
return;
m_hasSceneEnv = b;
emit hasSceneEnvChanged();
}
void AssetsLibraryWidget::handleDeletedGeneratedAssets(const QHash<QString, Utils::FilePath> &assetData)
{
// 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;
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();
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);
}
}
}
if (!removedImports.isEmpty()) {
m_assetsView->model()->changeImports({}, removedImports);
clearStacks = true;
}
});
// The size check here is to weed out cases where project path somehow resolves
// to just slash or drive + slash. (Shortest legal currentProjectDirPath() would be "/a/")
if (m_assetsModel->currentProjectDirPath().size() < 4)
return;
// Delete the asset modules
for (const Utils::FilePath &dir : assetData) {
if (dir.exists() && dir.toFSPathString().startsWith(m_assetsModel->currentProjectDirPath())) {
QString error;
dir.removeRecursively(&error);
if (!error.isEmpty()) {
QMessageBox::warning(Core::ICore::dialogParent(),
Tr::tr("Failed to Delete Asset Resources"),
Tr::tr("Could not delete \"%1\".").arg(dir.toFSPathString()));
}
}
}
// Reset undo stack as removing effect components cannot be undone, and thus the stack will
// contain only unworkable states.
if (clearStacks)
document->clearUndoRedoStacks();
m_assetsView->emitCustomNotification("effectcomposer_effects_deleted", {}, {effectNames});
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)
{
m_assetsIconProvider->invalidateThumbnail(id);
}
QSize AssetsLibraryWidget::imageSize(const QString &id)
{
return m_assetsIconProvider->imageSize(id);
}
QString AssetsLibraryWidget::assetFileSize(const QString &id)
{
qint64 fileSize = m_assetsIconProvider->fileSize(id);
return QLocale::system().formattedDataSize(fileSize, 2, QLocale::DataSizeTraditionalFormat);
}
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)
{
QMetaObject::invokeMethod(this, "handleAssetsDrop", Qt::QueuedConnection, Q_ARG(QList<QUrl>, urls), Q_ARG(QString, targetDir));
}
void AssetsLibraryWidget::handleAssetsDrop(const QList<QUrl> &urls, const QString &targetDir)
{
if (urls.isEmpty() || targetDir.isEmpty())
return;
Utils::FilePath destDir = Utils::FilePath::fromUserInput(targetDir);
QString resourceFolder = DocumentManager::currentResourcePath().toString();
if (destDir.isFile())
destDir = destDir.parentDir();
QMessageBox msgBox;
msgBox.setInformativeText("What would you like to do with the existing asset?");
msgBox.addButton("Keep Both", QMessageBox::AcceptRole);
msgBox.addButton("Replace", QMessageBox::ResetRole);
msgBox.addButton("Cancel", QMessageBox::RejectRole);
for (const QUrl &url : urls) {
Utils::FilePath src = Utils::FilePath::fromUrl(url);
Utils::FilePath dest = destDir.pathAppended(src.fileName());
if (destDir == src.parentDir() || !src.startsWith(resourceFolder))
continue;
if (dest.exists()) {
msgBox.setText("An asset named " + dest.fileName() + " already exists.");
msgBox.exec();
int userAction = msgBox.buttonRole(msgBox.clickedButton());
if (userAction == QMessageBox::AcceptRole) { // "Keep Both"
dest = Utils::FilePath::fromString(UniqueName::generatePath(dest.toString()));
} else if (userAction == QMessageBox::ResetRole && dest.exists()) { // "Replace"
if (!dest.removeFile()) {
qWarning() << __FUNCTION__ << "Failed to remove existing file" << dest;
continue;
}
} else if (userAction == QMessageBox::RejectRole) { // "Cancel"
continue;
}
}
bool isDir = src.isDir();
if (src.renameFile(dest)) {
if (isDir)
m_assetsModel->updateExpandPath(src, dest);
} else if (isDir) {
Core::AsynchronousMessageBox::warning(
Tr::tr("Folder move failure"),
Tr::tr("Failed to move folder \"%1\". The folder might contain subfolders or one "
"of its files is in use.")
.arg(src.fileName()));
}
}
if (m_assetsView->model())
m_assetsView->model()->endDrag();
}
QList<QToolButton *> AssetsLibraryWidget::createToolBarWidgets()
{
return {};
}
void AssetsLibraryWidget::handleSearchFilterChanged(const QString &filterText)
{
if (filterText == m_filterText || (m_assetsModel->isEmpty()
&& filterText.contains(m_filterText, Qt::CaseInsensitive)))
return;
m_filterText = filterText;
updateSearch();
}
void AssetsLibraryWidget::handleAddAsset()
{
addResources({});
}
void AssetsLibraryWidget::emitExtFilesDrop(const QList<QUrl> &simpleFilePaths,
const QList<QUrl> &complexFilePaths,
const QString &targetDirPath)
{
// workaround for but QDS-8010: we need to postpone the call to handleExtFilesDrop, otherwise
// the TreeViewDelegate might be recreated (therefore, destroyed) while we're still in a handler
// of a QML DropArea which is a child of the delegate being destroyed - this would cause a crash.
emit extFilesDrop(simpleFilePaths, complexFilePaths, targetDirPath);
}
void AssetsLibraryWidget::handleExtFilesDrop(const QList<QUrl> &simpleFilePaths,
const QList<QUrl> &complexFilePaths,
const QString &targetDirPath)
{
QStringList simpleFilePathStrings = Utils::transform<QStringList>(simpleFilePaths,
&QUrl::toLocalFile);
QStringList complexFilePathStrings = Utils::transform<QStringList>(complexFilePaths,
&QUrl::toLocalFile);
if (!simpleFilePathStrings.isEmpty()) {
if (targetDirPath.isEmpty()) {
addResources(simpleFilePathStrings, false);
} else {
AddFilesResult result = ModelNodeOperations::addFilesToProject(simpleFilePathStrings,
targetDirPath,
false);
if (result.status() == AddFilesResult::Failed) {
QWidget *w = Core::AsynchronousMessageBox::warning(
Tr::tr("Failed to Add Files"),
Tr::tr("Could not add %1 to project.").arg(simpleFilePathStrings.join(' ')));
// Avoid multiple modal dialogs open at the same time
auto mb = qobject_cast<QMessageBox *>(w);
if (mb && !complexFilePathStrings.empty())
mb->exec();
}
}
}
if (!complexFilePathStrings.empty())
addResources(complexFilePathStrings, false);
m_assetsView->model()->endDrag();
}
QSet<QString> AssetsLibraryWidget::supportedAssetSuffixes(bool complex)
{
const QList<AddResourceHandler> handlers = QmlDesignerPlugin::instance()->viewManager()
.designerActionManager().addResourceHandler();
QSet<QString> suffixes;
for (const AddResourceHandler &handler : handlers) {
if (Asset::isSupported(handler.filter) != complex)
suffixes.insert(handler.filter);
}
return suffixes;
}
void AssetsLibraryWidget::openEffectComposer(const QString &filePath)
{
ModelNodeOperations::openEffectComposer(filePath);
}
QString AssetsLibraryWidget::qmlSourcesPath()
{
#ifdef SHARE_QML_PATH
if (Utils::qtcEnvironmentVariableIsSet("LOAD_QML_FROM_SOURCE"))
return QLatin1String(SHARE_QML_PATH) + "/assetsLibraryQmlSources";
#endif
return Core::ICore::resourcePath("qmldesigner/assetsLibraryQmlSources").toString();
}
void AssetsLibraryWidget::clearSearchFilter()
{
QMetaObject::invokeMethod(m_assetsWidget->rootObject(), "clearSearchFilter");
}
void AssetsLibraryWidget::reloadQmlSource()
{
const QString assetsQmlPath = qmlSourcesPath() + "/Assets.qml";
QTC_ASSERT(QFileInfo::exists(assetsQmlPath), return);
m_assetsWidget->setSource(QUrl::fromLocalFile(assetsQmlPath));
}
void AssetsLibraryWidget::updateSearch()
{
m_assetsModel->setSearchText(m_filterText);
}
void AssetsLibraryWidget::setIsDragging(bool val)
{
if (m_isDragging != val) {
m_isDragging = val;
emit isDraggingChanged();
}
}
void AssetsLibraryWidget::setResourcePath(const QString &resourcePath)
{
m_assetsModel->setRootPath(resourcePath);
m_assetsIconProvider->clearCache();
updateSearch();
}
void AssetsLibraryWidget::startDragAsset(const QStringList &assetPaths, const QPointF &mousePos)
{
// Actual drag is created after mouse has moved to avoid a QDrag bug that causes drag to stay
// active (and blocks mouse release) if mouse is released at the same spot of the drag start.
m_assetsToDrag = assetPaths;
m_dragStartPoint = mousePos.toPoint();
setIsDragging(true);
}
QPair<QString, QByteArray> AssetsLibraryWidget::getAssetTypeAndData(const QString &assetPath)
{
Asset asset(assetPath);
if (asset.hasSuffix()) {
if (asset.isImage()) {
// Data: Image format (suffix)
return {Constants::MIME_TYPE_ASSET_IMAGE, asset.suffix().toUtf8()};
} else if (asset.isFont()) {
// Data: Font family name
QRawFont font(assetPath, 10);
QString fontFamily = font.isValid() ? font.familyName() : "";
return {Constants::MIME_TYPE_ASSET_FONT, fontFamily.toUtf8()};
} else if (asset.isShader()) {
// Data: shader type, frament (f) or vertex (v)
return {Constants::MIME_TYPE_ASSET_SHADER,
asset.isFragmentShader() ? "f" : "v"};
} else if (asset.isAudio()) {
// No extra data for sounds
return {Constants::MIME_TYPE_ASSET_SOUND, {}};
} else if (asset.isVideo()) {
// No extra data for videos
return {Constants::MIME_TYPE_ASSET_VIDEO, {}};
} else if (asset.isTexture3D()) {
// Data: Image format (suffix)
return {Constants::MIME_TYPE_ASSET_TEXTURE3D, asset.suffix().toUtf8()};
} 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 {};
}
static QHash<QByteArray, QStringList> allImageFormats()
{
const QList<QByteArray> mimeTypes = QImageReader::supportedMimeTypes();
auto transformer = [](const QByteArray& format) -> QString { return QString("*.") + format; };
QHash<QByteArray, QStringList> imageFormats;
for (const auto &mimeType : mimeTypes)
imageFormats.insert(mimeType, Utils::transform(QImageReader::imageFormatsForMimeType(mimeType), transformer));
imageFormats.insert("image/vnd.radiance", {"*.hdr"});
imageFormats.insert("image/ktx", {"*.ktx"});
return imageFormats;
}
void AssetsLibraryWidget::addResources(const QStringList &files, bool showDialog)
{
clearSearchFilter();
DesignDocument *document = QmlDesignerPlugin::instance()->currentDesignDocument();
QTC_ASSERT(document, return);
const QList<AddResourceHandler> handlers = QmlDesignerPlugin::instance()->viewManager()
.designerActionManager().addResourceHandler();
QStringList fileNames = files;
if (fileNames.isEmpty()) { // if no files, show the "add assets" dialog
QMultiMap<QString, QString> map;
QHash<QString, int> priorities;
for (const AddResourceHandler &handler : handlers) {
map.insert(handler.category, handler.filter);
priorities.insert(handler.category, handler.piority);
}
QStringList sortedKeys = map.uniqueKeys();
Utils::sort(sortedKeys, [&priorities](const QString &first, const QString &second) {
return priorities.value(first) < priorities.value(second);
});
QStringList filters{Tr::tr("All Files (%1)").arg("*.*")};
QString filterTemplate = "%1 (%2)";
for (const QString &key : std::as_const(sortedKeys)) {
const QStringList values = map.values(key);
if (values.contains("*.png")) { // Avoid long filter for images by splitting
const QHash<QByteArray, QStringList> imageFormats = allImageFormats();
QHash<QByteArray, QStringList>::const_iterator i = imageFormats.constBegin();
while (i != imageFormats.constEnd()) {
filters.append(filterTemplate.arg(key + QString::fromLatin1(i.key()), i.value().join(' ')));
++i;
}
} else {
filters.append(filterTemplate.arg(key, values.join(' ')));
}
}
static QString lastDir;
const QString currentDir = lastDir.isEmpty() ? document->fileName().parentDir().toString() : lastDir;
fileNames = QFileDialog::getOpenFileNames(Core::ICore::dialogParent(),
Tr::tr("Add Assets"),
currentDir,
filters.join(";;"));
if (!fileNames.isEmpty())
lastDir = QFileInfo(fileNames.first()).absolutePath();
}
QHash<QString, QString> filterToCategory;
QHash<QString, AddResourceOperation> categoryToOperation;
for (const AddResourceHandler &handler : handlers) {
filterToCategory.insert(handler.filter, handler.category);
categoryToOperation.insert(handler.category, handler.operation);
}
QMultiMap<QString, QString> categoryFileNames; // filenames grouped by category
for (const QString &fileName : std::as_const(fileNames)) {
const QString suffix = "*." + QFileInfo(fileName).suffix().toLower();
const QString category = filterToCategory.value(suffix);
categoryFileNames.insert(category, fileName);
}
QStringList unsupportedFiles;
QStringList failedOpsFiles;
for (const QString &category : categoryFileNames.uniqueKeys()) {
QStringList fileNames = categoryFileNames.values(category);
AddResourceOperation operation = categoryToOperation.value(category);
QmlDesignerPlugin::emitUsageStatistics(Constants::EVENT_RESOURCE_IMPORTED + category);
if (operation) {
AddFilesResult result = operation(fileNames,
document->fileName().parentDir().toString(), showDialog);
if (result.status() == AddFilesResult::Failed) {
failedOpsFiles.append(fileNames);
} else {
if (!result.directory().isEmpty()) {
emit directoryCreated(result.directory());
} else if (result.haveDelayedResult()) {
QObject *delayedResult = result.delayedResult();
QObject::connect(delayedResult, &QObject::destroyed, this, [this, delayedResult]() {
QVariant propValue = delayedResult->property(AddFilesResult::directoryPropName);
QString directory = propValue.toString();
if (!directory.isEmpty())
emit directoryCreated(directory);
});
}
}
} else {
unsupportedFiles.append(fileNames);
}
}
if (!failedOpsFiles.isEmpty()) {
QWidget *w = Core::AsynchronousMessageBox::warning(Tr::tr("Failed to Add Files"),
Tr::tr("Could not add %1 to project.")
.arg(failedOpsFiles.join(' ')));
// Avoid multiple modal dialogs open at the same time
auto mb = qobject_cast<QMessageBox *>(w);
if (mb && !unsupportedFiles.isEmpty())
mb->exec();
}
if (!unsupportedFiles.isEmpty()) {
Core::AsynchronousMessageBox::warning(
Tr::tr("Failed to Add Files"),
Tr::tr("Could not add %1 to project. Unsupported file format.")
.arg(unsupportedFiles.join(' ')));
}
}
void AssetsLibraryWidget::addAssetsToContentLibrary(const QStringList &assetPaths)
{
QmlDesignerPlugin::instance()->mainWidget()->showDockWidget("ContentLibrary");
m_assetsView->emitCustomNotification("add_assets_to_content_lib", {}, {assetPaths});
}
void AssetsLibraryWidget::setCanCreateEffects(bool newVal)
{
if (m_canCreateEffects == newVal)
return;
m_canCreateEffects = newVal;
emit canCreateEffectsChanged();
}
bool AssetsLibraryWidget::canCreateEffects() const
{
return m_canCreateEffects;
}
} // namespace QmlDesigner