diff options
Diffstat (limited to 'src/Authoring/Qt3DStudio/Palettes/Project/ProjectFileSystemModel.cpp')
-rw-r--r-- | src/Authoring/Qt3DStudio/Palettes/Project/ProjectFileSystemModel.cpp | 1372 |
1 files changed, 1372 insertions, 0 deletions
diff --git a/src/Authoring/Qt3DStudio/Palettes/Project/ProjectFileSystemModel.cpp b/src/Authoring/Qt3DStudio/Palettes/Project/ProjectFileSystemModel.cpp new file mode 100644 index 00000000..4f62cd59 --- /dev/null +++ b/src/Authoring/Qt3DStudio/Palettes/Project/ProjectFileSystemModel.cpp @@ -0,0 +1,1372 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt 3D Studio. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +#include "qtAuthoring-config.h" +#include <QtCore/qset.h> +#include <QtCore/qtimer.h> + +#include "PresentationFile.h" +#include "Qt3DSCommonPrecompile.h" +#include "ProjectFileSystemModel.h" +#include "StudioUtils.h" +#include "StudioApp.h" +#include "ClientDataModelBridge.h" +#include "Core.h" +#include "Doc.h" +#include "Qt3DSFileTools.h" +#include "ImportUtils.h" +#include "Dialogs.h" +#include "Qt3DSDMStudioSystem.h" +#include "Qt3DSImportTranslation.h" +#include "Qt3DSMessageBox.h" +#include "IDocumentEditor.h" +#include "IDragable.h" +#include "IObjectReferenceHelper.h" +#include "IDirectoryWatchingSystem.h" + +ProjectFileSystemModel::ProjectFileSystemModel(QObject *parent) : QAbstractListModel(parent) + , m_model(new QFileSystemModel(this)) +{ + connect(m_model, &QAbstractItemModel::rowsInserted, this, &ProjectFileSystemModel::modelRowsInserted); + connect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ProjectFileSystemModel::modelRowsRemoved); + connect(m_model, &QAbstractItemModel::layoutChanged, this, &ProjectFileSystemModel::modelLayoutChanged); + connect(&g_StudioApp.GetCore()->getProjectFile(), &ProjectFile::presentationIdChanged, + this, &ProjectFileSystemModel::handlePresentationIdChange); + connect(&g_StudioApp.GetCore()->getProjectFile(), &ProjectFile::assetNameChanged, + this, &ProjectFileSystemModel::asyncUpdateReferences); + + m_projectReferencesUpdateTimer.setSingleShot(true); + m_projectReferencesUpdateTimer.setInterval(0); + + connect(&m_projectReferencesUpdateTimer, &QTimer::timeout, + this, &ProjectFileSystemModel::updateProjectReferences); +} + +QHash<int, QByteArray> ProjectFileSystemModel::roleNames() const +{ + auto modelRoleNames = m_model->roleNames(); + modelRoleNames.insert(IsExpandableRole, "_isExpandable"); + modelRoleNames.insert(IsDraggableRole, "_isDraggable"); + modelRoleNames.insert(IsReferencedRole, "_isReferenced"); + modelRoleNames.insert(IsProjectReferencedRole, "_isProjectReferenced"); + modelRoleNames.insert(DepthRole, "_depth"); + modelRoleNames.insert(ExpandedRole, "_expanded"); + modelRoleNames.insert(FileIdRole, "_fileId"); + modelRoleNames.insert(ExtraIconRole, "_extraIcon"); + return modelRoleNames; +} + +int ProjectFileSystemModel::rowCount(const QModelIndex &) const +{ + return m_items.count(); +} + +QVariant ProjectFileSystemModel::data(const QModelIndex &index, int role) const +{ + const auto &item = m_items.at(index.row()); + + switch (role) { + case Qt::DecorationRole: { + QString path = item.index.data(QFileSystemModel::FilePathRole).toString(); + return StudioUtils::resourceImageUrl() + getIconName(path); + } + + case IsExpandableRole: { + if (item.index == m_rootIndex) { + return false; + } else { + return hasVisibleChildren(item.index); + } + } + + case IsDraggableRole: + return QFileInfo(item.index.data(QFileSystemModel::FilePathRole).toString()).isFile(); + + case IsReferencedRole: { + const QString path = item.index.data(QFileSystemModel::FilePathRole).toString(); + return m_references.contains(path); + } + + case IsProjectReferencedRole: { + const QString path = item.index.data(QFileSystemModel::FilePathRole).toString(); + return m_projectReferences.contains(path); + } + + case DepthRole: + return item.depth; + + case ExpandedRole: + return item.expanded; + + case FileIdRole: { + const QString path = item.index.data(QFileSystemModel::FilePathRole).toString(); + EStudioObjectType iconType = getIconType(path); + if (iconType == OBJTYPE_PRESENTATION || iconType == OBJTYPE_QML_STREAM) + return presentationId(path); + else + return {}; + } + + case ExtraIconRole: { + const QString path = item.index.data(QFileSystemModel::FilePathRole).toString(); + EStudioObjectType iconType = getIconType(path); + if (iconType == OBJTYPE_PRESENTATION || iconType == OBJTYPE_QML_STREAM) { + if (presentationId(path).isEmpty()) + return QStringLiteral("warning.png"); + else + return {}; + } else { + return {}; + } + } + + default: + return m_model->data(item.index, role); + } +} + +QMimeData *ProjectFileSystemModel::mimeData(const QModelIndexList &indexes) const +{ + const QString path = filePath(indexes.first().row()); // can only drag one item + return CDropSourceFactory::Create(QT3DS_FLAVOR_ASSET_UICFILE, path); +} + +QString ProjectFileSystemModel::filePath(int row) const +{ + if (row < 0 || row >= m_items.size()) + return QString(); + const auto &item = m_items.at(row); + return item.index.data(QFileSystemModel::FilePathRole).toString(); +} + +bool ProjectFileSystemModel::isRefreshable(int row) const +{ + const QString path = filePath(row); + // Import needs to be refreshable even if it is not referenced, as user may drag just individual + // meshes into the scene, and not the whole import. + return path.endsWith(QLatin1String(".import")); +} + +void ProjectFileSystemModel::updateReferences() +{ + m_references.clear(); + const auto doc = g_StudioApp.GetCore()->GetDoc(); + const auto bridge = doc->GetStudioSystem()->GetClientDataModelBridge(); + const auto sourcePathList = bridge->GetSourcePathList(); + const auto fontFileList = bridge->GetFontFileList(); + const auto effectTextureList = bridge->GetDynamicObjectTextureList(); + auto renderableList = bridge->getRenderableList(); + auto subpresentationRecord = g_StudioApp.m_subpresentations; + + const QDir projectDir(doc->GetCore()->getProjectFile().getProjectPath()); + const QString projectPath = QDir::cleanPath(projectDir.absolutePath()); + const QString projectPathSlash = projectPath + QLatin1Char('/'); + + // Add current presentation to renderables list + renderableList.insert(doc->getPresentationId()); + subpresentationRecord.push_back( + SubPresentationRecord({}, doc->getPresentationId(), + projectDir.relativeFilePath(doc->GetDocumentPath()))); + + auto addReferencesPresentation = [this, doc, &projectPath](const QString &str) { + addPathsToReferences(m_references, projectPath, doc->GetResolvedPathToDoc(str)); + }; + auto addReferencesRenderable = [this, &projectPath, &projectPathSlash, &subpresentationRecord] + (const QString &id) { + for (SubPresentationRecord r : qAsConst(subpresentationRecord)) { + if (r.m_id == id) + addPathsToReferences(m_references, projectPath, projectPathSlash + r.m_argsOrSrc); + } + }; + + std::for_each(sourcePathList.begin(), sourcePathList.end(), addReferencesPresentation); + std::for_each(fontFileList.begin(), fontFileList.end(), addReferencesPresentation); + std::for_each(effectTextureList.begin(), effectTextureList.end(), addReferencesPresentation); + std::for_each(renderableList.begin(), renderableList.end(), addReferencesRenderable); + + m_references.insert(projectPath); + + updateRoles({IsReferencedRole, Qt::DecorationRole}); +} + +/** + * Checks if file is already imported and if not, adds it to outImportedFiles + * + * @param importFile The new imported file to check + * @param outImportedFiles List of already imported files + * @return true if importFile was added + */ +bool ProjectFileSystemModel::addUniqueImportFile(const QString &importFile, + QStringList &outImportedFiles) const +{ + const QString cleanPath = QFileInfo(importFile).canonicalFilePath(); + if (outImportedFiles.contains(cleanPath)) { + return false; + } else { + outImportedFiles.append(cleanPath); + return true; + } +} + +/** + * Copy a file with option to override an existing file or skip the override. + * + * @param srcFile The source file to copy. + * @param targetFile The destination file path. + * @param outImportedFiles list of absolute source paths of the dependent assets that are imported + * in the same import context. + * @param outOverrideChoice The copy skip/override choice used in this import context. + */ +void ProjectFileSystemModel::overridableCopyFile(const QString &srcFile, const QString &targetFile, + QStringList &outImportedFiles, + int &outOverrideChoice) const +{ + QFileInfo srcFileInfo(srcFile); + if (srcFileInfo.exists() && addUniqueImportFile(srcFile, outImportedFiles)) { + QFileInfo targetFileInfo(targetFile); + if (srcFileInfo == targetFileInfo) + return; // Autoskip when source and target is the same + if (!targetFileInfo.dir().exists()) + targetFileInfo.dir().mkpath(QStringLiteral(".")); + + if (targetFileInfo.exists()) { // asset exists, show override / skip box + if (outOverrideChoice == QMessageBox::YesToAll) { + QFile::remove(targetFile); + } else if (outOverrideChoice == QMessageBox::NoToAll) { + // QFile::copy() does not override files + } else { + QString pathFromRoot = QDir(g_StudioApp.GetCore()->getProjectFile() + .getProjectPath()) + .relativeFilePath(targetFile); + outOverrideChoice = g_StudioApp.GetDialogs() + ->displayOverrideAssetBox(pathFromRoot); + if (outOverrideChoice & (QMessageBox::Yes | QMessageBox::YesToAll)) + QFile::remove(targetFile); + } + } + QFile::copy(srcFile, targetFile); + } +} + +void ProjectFileSystemModel::updateProjectReferences() +{ + m_projectReferences.clear(); + + const QDir projectDir(g_StudioApp.GetCore()->getProjectFile().getProjectPath()); + const QString projectPath = QDir::cleanPath(projectDir.absolutePath()); + + QHashIterator<QString, bool> updateIt(m_projectReferencesUpdateMap); + while (updateIt.hasNext()) { + updateIt.next(); + m_presentationReferences.remove(updateIt.key()); + if (updateIt.value()) { + QFileInfo fi(updateIt.key()); + QDir fileDir = fi.dir(); + const QString suffix = fi.suffix(); + QHash<QString, QString> importPathMap; + QSet<QString> newReferences; + + const auto addReferencesFromImportMap = [&]() { + QHashIterator<QString, QString> pathIter(importPathMap); + while (pathIter.hasNext()) { + pathIter.next(); + const QString path = pathIter.key(); + QString targetAssetPath; + if (path.startsWith(QLatin1String("./"))) // path from project root + targetAssetPath = projectDir.absoluteFilePath(path); + else // relative path + targetAssetPath = fileDir.absoluteFilePath(path); + newReferences.insert(QDir::cleanPath(targetAssetPath)); + } + }; + + if (CDialogs::presentationExtensions().contains(suffix) + || CDialogs::qmlStreamExtensions().contains(suffix)) { + // Presentation file added/modified, check that it is one of the subpresentations, + // or we don't care about it + const QString relPath = g_StudioApp.GetCore()->getProjectFile() + .getRelativeFilePathTo(updateIt.key()); + for (int i = 0, count = g_StudioApp.m_subpresentations.size(); i < count; ++i) { + SubPresentationRecord &rec = g_StudioApp.m_subpresentations[i]; + if (rec.m_argsOrSrc == relPath) { + if (rec.m_type == QLatin1String("presentation")) { + // Since this is not actual import, source and target uip is the same, + // and we are only interested in the absolute paths of the "imported" + // asset files + QString dummyStr; + QHash<QString, QString> dummyMap; + QSet<QString> dummyDataInputSet; + QSet<QString> dummyDataOutputSet; + PresentationFile::getSourcePaths(fi, fi, importPathMap, + dummyStr, dummyMap, dummyDataInputSet, + dummyDataOutputSet); + addReferencesFromImportMap(); + } else { // qml-stream + QQmlApplicationEngine qmlEngine; + bool isQmlStream = false; + QObject *qmlRoot = getQmlStreamRootNode(qmlEngine, updateIt.key(), + isQmlStream); + if (qmlRoot && isQmlStream) { + QSet<QString> assetPaths; + getQmlAssets(qmlRoot, assetPaths); + QDir qmlDir = fi.dir(); + for (auto &assetSrc : qAsConst(assetPaths)) { + QString targetAssetPath; + targetAssetPath = qmlDir.absoluteFilePath(assetSrc); + newReferences.insert(QDir::cleanPath(targetAssetPath)); + } + } + } + break; + } + } + } else if (CDialogs::materialExtensions().contains(suffix) + || CDialogs::effectExtensions().contains(suffix)) { + // Use dummy set, as we are only interested in values set in material files + QSet<QString> dummySet; + g_StudioApp.GetCore()->GetDoc()->GetDocumentReader() + .ParseSourcePathsOutOfEffectFile( + updateIt.key(), + g_StudioApp.GetCore()->getProjectFile().getProjectPath(), + false, // No need to recurse src mats; those get handled individually + importPathMap, dummySet); + addReferencesFromImportMap(); + } + if (!newReferences.isEmpty()) + m_presentationReferences.insert(updateIt.key(), newReferences); + } + } + + // Update reference cache + QHashIterator<QString, QSet<QString>> presIt(m_presentationReferences); + while (presIt.hasNext()) { + presIt.next(); + const auto &refs = presIt.value(); + for (auto &ref : refs) + addPathsToReferences(m_projectReferences, projectPath, ref); + } + + m_projectReferencesUpdateMap.clear(); + updateRoles({IsProjectReferencedRole}); +} + +void ProjectFileSystemModel::getQmlAssets(const QObject *qmlNode, + QSet<QString> &outAssetPaths) const +{ + QString assetSrc = qmlNode->property("source").toString(); // absolute file path + + if (!assetSrc.isEmpty()) { + // remove file:/// + if (assetSrc.startsWith(QLatin1String("file:///"))) + assetSrc = assetSrc.mid(8); + else if (assetSrc.startsWith(QLatin1String("file://"))) + assetSrc = assetSrc.mid(7); + +#if !defined(Q_OS_WIN) + // Only windows has drive letter in the path, other platforms need to start with / + assetSrc.prepend(QLatin1Char('/')); +#endif + outAssetPaths.insert(assetSrc); + } + + // recursively load child nodes + const QObjectList qmlNodeChildren = qmlNode->children(); + for (auto &node : qmlNodeChildren) + getQmlAssets(node, outAssetPaths); +} + +QObject *ProjectFileSystemModel::getQmlStreamRootNode(QQmlApplicationEngine &qmlEngine, + const QString &filePath, + bool &outIsQmlStream) const +{ + QObject *qmlRoot = nullptr; + outIsQmlStream = false; + + qmlEngine.load(filePath); + if (qmlEngine.rootObjects().size() > 0) { + qmlRoot = qmlEngine.rootObjects().at(0); + const char *rootClassName = qmlEngine.rootObjects().at(0) + ->metaObject()->superClass()->className(); + // The assumption here is that any qml that is not a behavior is a qml stream + if (strcmp(rootClassName, "Q3DStudio::Q3DSQmlBehavior") != 0) + outIsQmlStream = true; + } + + return qmlRoot; +} + +Q3DStudio::DocumentEditorFileType::Enum ProjectFileSystemModel::assetTypeForRow(int row) +{ + if (row <= 0 || row >= m_items.size()) + return Q3DStudio::DocumentEditorFileType::Unknown; + + const QString rootPath = m_items[0].index.data(QFileSystemModel::FilePathRole).toString(); + QString path = m_items[row].index.data(QFileSystemModel::FilePathRole).toString(); + QFileInfo fi(path); + if (!fi.isDir()) + path = fi.absolutePath(); + path = path.mid(rootPath.length() + 1); + const int slash = path.indexOf(QLatin1String("/")); + if (slash >= 0) + path = path.left(slash); + if (path == QLatin1String("effects")) + return Q3DStudio::DocumentEditorFileType::Effect; + else if (path == QLatin1String("fonts")) + return Q3DStudio::DocumentEditorFileType::Font; + else if (path == QLatin1String("maps")) + return Q3DStudio::DocumentEditorFileType::Image; + else if (path == QLatin1String("materials")) + return Q3DStudio::DocumentEditorFileType::Material; + else if (path == QLatin1String("models")) + return Q3DStudio::DocumentEditorFileType::DAE; + else if (path == QLatin1String("scripts")) + return Q3DStudio::DocumentEditorFileType::Behavior; + else if (path == QLatin1String("presentations")) + return Q3DStudio::DocumentEditorFileType::Presentation; + else if (path == QLatin1String("qml")) + return Q3DStudio::DocumentEditorFileType::QmlStream; + + return Q3DStudio::DocumentEditorFileType::Unknown; +} + +void ProjectFileSystemModel::setRootPath(const QString &path) +{ + m_projectReferences.clear(); + m_presentationReferences.clear(); + m_projectReferencesUpdateMap.clear(); + m_projectReferencesUpdateTimer.stop(); + + // Delete the old model. If the new project is in a totally different directory tree, not + // doing this will result in unexplicable crashes when trying to parse something that should + // not be parsed. + disconnect(m_model, &QAbstractItemModel::rowsInserted, + this, &ProjectFileSystemModel::modelRowsInserted); + disconnect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, + this, &ProjectFileSystemModel::modelRowsRemoved); + disconnect(m_model, &QAbstractItemModel::layoutChanged, + this, &ProjectFileSystemModel::modelLayoutChanged); + delete m_model; + m_model = new QFileSystemModel(this); + connect(m_model, &QAbstractItemModel::rowsInserted, + this, &ProjectFileSystemModel::modelRowsInserted); + connect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, + this, &ProjectFileSystemModel::modelRowsRemoved); + connect(m_model, &QAbstractItemModel::layoutChanged, + this, &ProjectFileSystemModel::modelLayoutChanged); + + setRootIndex(m_model->setRootPath(path)); + + // Open the presentations folder by default + connect(this, &ProjectFileSystemModel::dataChanged, + this, &ProjectFileSystemModel::asyncExpandPresentations); + + QTimer::singleShot(0, [this]() { + // Watch the project directory for changes to .uip files. + // Note that this initial connection will notify creation for all files, so we call it + // asynchronously to ensure the subpresentations are registered. + m_directoryConnection = g_StudioApp.getDirectoryWatchingSystem().AddDirectory( + g_StudioApp.GetCore()->getProjectFile().getProjectPath(), + std::bind(&ProjectFileSystemModel::onFilesChanged, this, + std::placeholders::_1)); + }); +} + +void ProjectFileSystemModel::setRootIndex(const QModelIndex &rootIndex) +{ + if (rootIndex == m_rootIndex) + return; + + clearModelData(); + + m_rootIndex = rootIndex; + + beginInsertRows({}, 0, 0); + m_items.append({ m_rootIndex, 0, true, nullptr, 0 }); + endInsertRows(); + + showModelTopLevelItems(); +} + +void ProjectFileSystemModel::clearModelData() +{ + beginResetModel(); + m_defaultDirToAbsPathMap.clear(); + m_items.clear(); + endResetModel(); +} + +void ProjectFileSystemModel::showModelTopLevelItems() +{ + int rowCount = m_model->rowCount(m_rootIndex); + + if (rowCount == 0) { + if (m_model->hasChildren(m_rootIndex) && m_model->canFetchMore(m_rootIndex)) + m_model->fetchMore(m_rootIndex); + } else { + showModelChildItems(m_rootIndex, 0, rowCount - 1); + } +} + +void ProjectFileSystemModel::showModelChildItems(const QModelIndex &parentIndex, int start, int end) +{ + const int parentRow = modelIndexRow(parentIndex); + if (parentRow == -1) + return; + + Q_ASSERT(isVisible(parentIndex)); + + QVector<QModelIndex> rowsToInsert; + for (int i = start; i <= end; ++i) { + const auto &childIndex = m_model->index(i, 0, parentIndex); + if (isVisible(childIndex)) + rowsToInsert.append(childIndex); + } + + const int insertCount = rowsToInsert.count(); + if (insertCount == 0) + return; + + auto parent = &m_items[parentRow]; + + const int depth = parent->depth + 1; + const int startRow = parentRow + parent->childCount + 1; + + beginInsertRows({}, startRow, startRow + insertCount - 1); + + for (auto it = rowsToInsert.rbegin(); it != rowsToInsert.rend(); ++it) + m_items.insert(startRow, { *it, depth, false, parent, 0 }); + + for (; parent != nullptr; parent = parent->parent) + parent->childCount += insertCount; + + endInsertRows(); + + // also fetch children so we're notified when files are added or removed in immediate subdirs + for (const auto &childIndex : rowsToInsert) { + if (m_model->hasChildren(childIndex) && m_model->canFetchMore(childIndex)) + m_model->fetchMore(childIndex); + } +} + +void ProjectFileSystemModel::expand(int row) +{ + if (row < 0 || row > m_items.size() - 1 || m_items[row].expanded) + return; + + auto &item = m_items[row]; + const auto &modelIndex = item.index; + + const int rowCount = m_model->rowCount(modelIndex); + if (rowCount == 0) { + if (m_model->hasChildren(modelIndex) && m_model->canFetchMore(modelIndex)) + m_model->fetchMore(modelIndex); + } else { + showModelChildItems(modelIndex, 0, rowCount - 1); + } + + item.expanded = true; + Q_EMIT dataChanged(index(row), index(row)); +} + +bool ProjectFileSystemModel::hasValidUrlsForDropping(const QList<QUrl> &urls) const +{ + for (const auto &url : urls) { + if (url.isLocalFile()) { + const QString path = url.toLocalFile(); + const QFileInfo fileInfo(path); + if (fileInfo.isFile()) { + const QString extension = fileInfo.suffix(); + return extension.compare(QLatin1String(CDialogs::GetDAEFileExtension()), + Qt::CaseInsensitive) == 0 +#ifdef QT_3DSTUDIO_FBX + || extension.compare(QLatin1String(CDialogs::GetFbxFileExtension()), + Qt::CaseInsensitive) == 0 +#endif + || getIconType(path) != OBJTYPE_UNKNOWN; + } + } + } + + return false; +} + +void ProjectFileSystemModel::showInfo(int row) +{ + if (row < 0 || row >= m_items.size()) + row = 0; + + const TreeItem &item = m_items.at(row); + QString path = item.index.data(QFileSystemModel::FilePathRole).toString(); + + QFileInfo fi(path); + + if (fi.suffix() == QLatin1String("materialdef")) { + const auto doc = g_StudioApp.GetCore()->GetDoc(); + bool isDocModified = doc->isModified(); + { // Scope for the ScopedDocumentEditor + Q3DStudio::ScopedDocumentEditor sceneEditor( + Q3DStudio::SCOPED_DOCUMENT_EDITOR(*doc, QString())); + const auto material = sceneEditor->getOrCreateMaterial(path); + QString name; + QMap<QString, QString> values; + QMap<QString, QMap<QString, QString>> textureValues; + sceneEditor->getMaterialInfo(fi.absoluteFilePath(), name, values, textureValues); + sceneEditor->setMaterialValues(fi.absoluteFilePath(), values, textureValues); + if (material.Valid()) + doc->SelectDataModelObject(material); + } + // Several aspects of the editor are not updated correctly + // if the data core is changed without a transaction + // The above scope completes the transaction for creating a new material + // Next the added undo has to be popped from the stack + // and the modified flag has to be restored + // TODO: Find a way to update the editor fully without a transaction + doc->SetModifiedFlag(isDocModified); + g_StudioApp.GetCore()->GetCmdStack()->RemoveLastUndo(); + } +} + +void ProjectFileSystemModel::duplicate(int row) +{ + if (row < 0 || row >= m_items.size()) + row = 0; + + const TreeItem &item = m_items.at(row); + QString path = item.index.data(QFileSystemModel::FilePathRole).toString(); + + QFileInfo srcFile(path); + const QString destPathStart = srcFile.dir().absolutePath() + QLatin1Char('/') + + srcFile.completeBaseName() + QStringLiteral(" Copy"); + const QString destPathEnd = QStringLiteral(".") + srcFile.suffix(); + QString destPath = destPathStart + destPathEnd; + + int i = 1; + while (QFile::exists(destPath)) { + i++; + destPath = destPathStart + QString::number(i) + destPathEnd; + } + + QFile::copy(path, destPath); +} + +void ProjectFileSystemModel::importUrls(const QList<QUrl> &urls, int row, bool autoSort) +{ + if (row < 0 || row >= m_items.size()) + row = 0; // Import to root folder row not specified + + // If importing via buttons or doing in-context import to project root, + // sort imported items to default folders according to their type + const bool sortToDefaults = autoSort || row == 0; + if (sortToDefaults) + updateDefaultDirMap(); + + const TreeItem &item = m_items.at(row); + QString targetPath = item.index.data(QFileSystemModel::FilePathRole).toString(); + + QFileInfo fi(targetPath); + if (!fi.isDir()) + targetPath = fi.absolutePath(); + const QDir targetDir(targetPath); + + QStringList expandPaths; + QHash<QString, QString> presentationNodes; // <relative path to presentation, presentation id> + // List of all files that have been copied by this import. Used to avoid duplicate imports + // due to some of the imported files also being assets used by other imported files. + QStringList importedFiles; + QMap<QString, CDataInputDialogItem *> importedDataInputs; + int overrideChoice = QMessageBox::NoButton; + + for (const auto &url : urls) { + QString sortedPath = targetPath; + QDir sortedDir = targetDir; + + if (sortToDefaults) { + const QString defaultDir = m_defaultDirToAbsPathMap.value( + g_StudioApp.GetDialogs()->defaultDirForUrl(url)); + if (!defaultDir.isEmpty()) { + sortedPath = defaultDir; + sortedDir.setPath(sortedPath); + } + } + + if (sortedDir.exists()) { + importUrl(sortedDir, url, presentationNodes, importedFiles, importedDataInputs, + overrideChoice); + expandPaths << sortedDir.path(); + } + } + + // Batch update all imported presentation nodes + g_StudioApp.GetCore()->getProjectFile().addPresentationNodes(presentationNodes); + + // Add new data inputs that are missing from project's data inputs. Duplicates are ignored, + // even if they are different type. + QMapIterator<QString, CDataInputDialogItem *> diIt(importedDataInputs); + bool addedDi = false; + while (diIt.hasNext()) { + diIt.next(); + if (!g_StudioApp.m_dataInputDialogItems.contains(diIt.key())) { + g_StudioApp.m_dataInputDialogItems.insert(diIt.key(), diIt.value()); + addedDi = true; + } else { + delete diIt.value(); + } + } + if (addedDi) { + g_StudioApp.saveDataInputsToProjectFile(); + g_StudioApp.checkDeletedDatainputs(); // Updates externalPresBoundTypes + } + + for (const QString &expandPath : qAsConst(expandPaths)) { + int expandRow = rowForPath(expandPath); + if (expandRow >= 0 && !m_items[expandRow].expanded) + expand(expandRow); + } +} + +/** + * Imports a single asset and the assets it depends on. + * + * @param targetDir Target path where the asset is imported to + * @param url Source url where the asset is imported from + * @param outPresentationNodes Map where presentation node information is stored for later + * registration. The key is relative path to presentation. The value + * is presentation id. + * @param outImportedFiles List of absolute source paths of the dependent assets that are imported + * in the same import context. + * @param outDataInputs Map of data inputs that are in use in this import context. + * @param outOverrideChoice The copy skip/override choice used in this import context. + */ +void ProjectFileSystemModel::importUrl(QDir &targetDir, const QUrl &url, + QHash<QString, QString> &outPresentationNodes, + QStringList &outImportedFiles, + QMap<QString, CDataInputDialogItem *> &outDataInputs, + int &outOverrideChoice) const +{ + using namespace Q3DStudio; + using namespace qt3dsimp; + // Drag and Drop - From Explorer window to Project Palette + // For all valid Project File Types: + // - This performs a file copy from the source Explorer location to the selected Project Palette + // Folder + // - The destination copy must NOT be read-only even if the source is read-only + // For DAE, it will import the file. + + if (!url.isLocalFile()) + return; + + const QString sourceFile = url.toLocalFile(); + + const QFileInfo fileInfo(sourceFile); + if (!fileInfo.isFile()) + return; + + // Skip importing if the file has already been imported + if (!addUniqueImportFile(sourceFile, outImportedFiles)) + return; + + const auto doc = g_StudioApp.GetCore()->GetDoc(); + + const QString extension = fileInfo.suffix(); + const QString fileStem = fileInfo.baseName(); + const QString outputFileName = QStringLiteral("%1.%2").arg(fileStem).arg(CDialogs::GetImportFileExtension()); + + if (extension.compare(QLatin1String(CDialogs::GetDAEFileExtension()), Qt::CaseInsensitive) == 0) { + SColladaTranslator translator(sourceFile); + const QDir outputDir = SFileTools::FindUniqueDestDirectory(targetDir, fileStem); + const QString fullOutputFile = outputDir.filePath(outputFileName); + const SImportResult importResult = + CPerformImport::TranslateToImportFile(translator, CFilePath(fullOutputFile)); + bool forceError = QFileInfo(fullOutputFile).isFile() == false; + IDocumentEditor::DisplayImportErrors( + sourceFile, importResult.m_Error, doc->GetImportFailedHandler(), + translator.m_TranslationLog, forceError); +#ifdef QT_3DSTUDIO_FBX + } else if (extension.compare(QLatin1String(CDialogs::GetFbxFileExtension()), Qt::CaseInsensitive) == 0) { + SFbxTranslator translator(sourceFile); + const QDir outputDir = SFileTools::FindUniqueDestDirectory(targetDir, fileStem); + const QString fullOutputFile = outputDir.filePath(outputFileName); + const SImportResult importResult = + CPerformImport::TranslateToImportFile(translator, CFilePath(fullOutputFile)); + bool forceError = QFileInfo(fullOutputFile).isFile() == false; + IDocumentEditor::DisplayImportErrors( + sourceFile, importResult.m_Error, doc->GetImportFailedHandler(), + translator.m_TranslationLog, forceError); +#endif + } else { + QQmlApplicationEngine qmlEngine; + QObject *qmlRoot = nullptr; + bool isQmlStream = false; + if (extension == QLatin1String("qml")) { + qmlRoot = getQmlStreamRootNode(qmlEngine, sourceFile, isQmlStream); + if (qmlRoot) { + if (isQmlStream && targetDir.path().endsWith(QLatin1String("/scripts"))) { + const QString path(QStringLiteral("../qml")); + targetDir.mkpath(path); // create the folder if doesn't exist + targetDir.cd(path); + } + } else { + // Invalid qml file, block import + g_StudioApp.GetDialogs()->DisplayKnownErrorDialog( + tr("Failed to parse '%1'\nAborting import.").arg(sourceFile)); + return; + } + } + // Copy the file to target directory + // FindAndCopyDestFile will make sure the file name is unique and make sure it is + // not read only. + QString destPath; // final file path (after copying and renaming) + bool copyResult = SFileTools::FindAndCopyDestFile(targetDir, sourceFile, destPath); + Q_ASSERT(copyResult); + + QString presentationPath; + if (CDialogs::isPresentationFileExtension(extension.toLatin1().data())) { + presentationPath = doc->GetCore()->getProjectFile().getRelativeFilePathTo(destPath); + QSet<QString> dataInputs; + QSet<QString> dataOutputs; + importPresentationAssets(fileInfo, QFileInfo(destPath), outPresentationNodes, + outImportedFiles, dataInputs, dataOutputs, outOverrideChoice); + const QString projFile = PresentationFile::findProjectFile(fileInfo.absoluteFilePath()); + + // #TODO: Handle DataOutputs QT3DS-3510 + QMap<QString, CDataInputDialogItem *> allDataInputs; + ProjectFile::loadDataInputs(projFile, allDataInputs); + for (auto &di : dataInputs) { + if (allDataInputs.contains(di)) + outDataInputs.insert(di, allDataInputs[di]); + } + } else if (qmlRoot && isQmlStream) { // importing a qml stream + presentationPath = doc->GetCore()->getProjectFile().getRelativeFilePathTo(destPath); + importQmlAssets(qmlRoot, fileInfo.dir(), targetDir, outImportedFiles, + outOverrideChoice); + } + + // outPresentationNodes can already contain this presentation in case of multi-importing + // both a presentation and its subpresentation + if (!presentationPath.isEmpty() && !outPresentationNodes.contains(presentationPath)) { + const QString srcProjFile = PresentationFile::findProjectFile(sourceFile); + QString presId; + if (!srcProjFile.isEmpty()) { + QVector<SubPresentationRecord> subpresentations; + ProjectFile::getPresentations(srcProjFile, subpresentations); + QDir srcProjDir(QFileInfo(srcProjFile).path()); + const QString relSrcPresFilePath = srcProjDir.relativeFilePath(sourceFile); + auto *sp = std::find_if( + subpresentations.begin(), subpresentations.end(), + [&relSrcPresFilePath](const SubPresentationRecord &spr) -> bool { + return spr.m_argsOrSrc == relSrcPresFilePath; + }); + // Make sure we are not adding a duplicate id. In that case presId will be empty + // which causes autogeneration of an unique id. + if (sp != subpresentations.end() + && g_StudioApp.GetCore()->getProjectFile().isUniquePresentationId(sp->m_id)) { + presId = sp->m_id; + } + } + outPresentationNodes.insert(presentationPath, presId); + } + + // For effect and custom material files, automatically copy related resources + if (CDialogs::IsEffectFileExtension(extension.toLatin1().data()) + || CDialogs::IsMaterialFileExtension(extension.toLatin1().data())) { + QHash<QString, QString> effectFileSourcePaths; + QString absSrcPath = fileInfo.absoluteFilePath(); + QString projectPath + = QFileInfo(PresentationFile::findProjectFile(absSrcPath)).absolutePath(); + // Since we are importing a bare material/effect, we don't care about possible dynamic + // values of texture properties + QSet<QString> dummyPropertySet; + g_StudioApp.GetCore()->GetDoc()->GetDocumentReader() + .ParseSourcePathsOutOfEffectFile(absSrcPath, projectPath, true, + effectFileSourcePaths, dummyPropertySet); + + QHashIterator<QString, QString> pathIter(effectFileSourcePaths); + while (pathIter.hasNext()) { + pathIter.next(); + overridableCopyFile(pathIter.value(), + QDir(g_StudioApp.GetCore()->getProjectFile().getProjectPath()) + .absoluteFilePath(pathIter.key()), + outImportedFiles, outOverrideChoice); + } + } + } +} + +/** + * Import all assets used in a uip file, this includes materials and effects (and their assets), + * images, fonts, subpresentations (and their recursive assets), models and scripts. Assets are + * imported in the same relative structure in the imported-from folder in order not to break the + * assets paths. + * + * @param uipSrc source file path where the uip is imported from + * @param uipTarget target path where the uip is imported to + * @param outPresentationNodes map where presentation node information is stored for later + * registration. The key is relative path to presentation. The value + * is presentation id. + * @param overrideChoice tracks user choice (yes to all / no to all) to maintain the value through + * recursive calls + * @param outImportedFiles list of absolute source paths of the dependent assets that are imported + * in the same import context. + * @param outDataInputs set of data input identifiers that are in use by this presentation and its + * subpresentations. + * @param outOverrideChoice The copy skip/override choice used in this import context. + */ +void ProjectFileSystemModel::importPresentationAssets( + const QFileInfo &uipSrc, const QFileInfo &uipTarget, + QHash<QString, QString> &outPresentationNodes, QStringList &outImportedFiles, + QSet<QString> &outDataInputs, QSet<QString> &outDataOutputs, int &outOverrideChoice) const +{ + QHash<QString, QString> importPathMap; + QString projPathSrc; // project absolute path for the source uip + PresentationFile::getSourcePaths(uipSrc, uipTarget, importPathMap, projPathSrc, + outPresentationNodes, outDataInputs, outDataOutputs); + const QDir projDir(g_StudioApp.GetCore()->getProjectFile().getProjectPath()); + const QDir uipSrcDir = uipSrc.dir(); + const QDir uipTargetDir = uipTarget.dir(); + + QHashIterator<QString, QString> pathIter(importPathMap); + while (pathIter.hasNext()) { + pathIter.next(); + QString srcAssetPath = pathIter.value(); + const QString path = pathIter.key(); + QString targetAssetPath; + if (srcAssetPath.isEmpty()) + srcAssetPath = uipSrcDir.absoluteFilePath(path); + targetAssetPath = uipTargetDir.absoluteFilePath(path); + + overridableCopyFile(srcAssetPath, targetAssetPath, outImportedFiles, outOverrideChoice); + + if (path.endsWith(QLatin1String(".uip"))) { + // recursively load any uip asset's assets + importPresentationAssets(QFileInfo(srcAssetPath), QFileInfo(targetAssetPath), + outPresentationNodes, outImportedFiles, outDataInputs, + outDataOutputs, outOverrideChoice); + + // update the path in outPresentationNodes to be correctly relative in target project + const QString subId = outPresentationNodes.take(path); + if (!subId.isEmpty()) + outPresentationNodes.insert(projDir.relativeFilePath(targetAssetPath), subId); + } else if (path.endsWith(QLatin1String(".qml"))) { + // recursively load any qml stream assets + QQmlApplicationEngine qmlEngine; + bool isQmlStream = false; + QObject *qmlRoot = getQmlStreamRootNode(qmlEngine, srcAssetPath, isQmlStream); + if (qmlRoot && isQmlStream) { + importQmlAssets(qmlRoot, QFileInfo(srcAssetPath).dir(), + QFileInfo(targetAssetPath).dir(), outImportedFiles, + outOverrideChoice); + // update path in outPresentationNodes to be correctly relative in target project + const QString subId = outPresentationNodes.take(path); + if (!subId.isEmpty()) + outPresentationNodes.insert(projDir.relativeFilePath(targetAssetPath), subId); + } + } + } +} + +/** + * Import all assets specified in "source" properties in a qml file. + * + * @param qmlNode The qml node to checkfor assets. Recursively checks all child nodes, too. + * @param srcDir target dir where the assets are imported to + * @param outImportedFiles list of absolute source paths of the dependent assets that are imported + * in the same import context. + * @param outOverrideChoice The copy skip/override choice used in this import context. + */ +void ProjectFileSystemModel::importQmlAssets(const QObject *qmlNode, const QDir &srcDir, + const QDir &targetDir, + QStringList &outImportedFiles, + int &outOverrideChoice) const +{ + QSet<QString> assetPaths; + getQmlAssets(qmlNode, assetPaths); + + for (auto &assetSrc : qAsConst(assetPaths)) { + overridableCopyFile(srcDir.absoluteFilePath(assetSrc), + targetDir.absoluteFilePath(srcDir.relativeFilePath(assetSrc)), + outImportedFiles, outOverrideChoice); + } +} + +int ProjectFileSystemModel::rowForPath(const QString &path) const +{ + for (int i = m_items.size() - 1; i >= 0 ; --i) { + const QString itemPath = m_items[i].index.data(QFileSystemModel::FilePathRole).toString(); + if (path == itemPath) + return i; + } + return -1; +} + +void ProjectFileSystemModel::updateRoles(const QVector<int> &roles, int startRow, int endRow) +{ + Q_EMIT dataChanged(index(startRow, 0), + index(endRow < 0 ? rowCount() - 1 : endRow, 0), roles); +} + +void ProjectFileSystemModel::collapse(int row) +{ + Q_ASSERT(row >= 0 && row < m_items.size()); + + auto &item = m_items[row]; + Q_ASSERT(item.expanded == true); + + const int childCount = item.childCount; + + if (childCount > 0) { + beginRemoveRows({}, row + 1, row + childCount); + + m_items.erase(std::begin(m_items) + row + 1, std::begin(m_items) + row + 1 + childCount); + + for (auto parent = &item; parent != nullptr; parent = parent->parent) + parent->childCount -= childCount; + + endRemoveRows(); + } + + item.expanded = false; + Q_EMIT dataChanged(index(row), index(row)); +} + +int ProjectFileSystemModel::modelIndexRow(const QModelIndex &modelIndex) const +{ + auto it = std::find_if( + std::begin(m_items), + std::end(m_items), + [&modelIndex](const TreeItem &item) + { + return item.index == modelIndex; + }); + + return it != std::end(m_items) ? std::distance(std::begin(m_items), it) : -1; +} + +bool ProjectFileSystemModel::isExpanded(const QModelIndex &modelIndex) const +{ + if (modelIndex == m_rootIndex) + return true; + const int row = modelIndexRow(modelIndex); + return row != -1 && m_items.at(row).expanded; +} + +EStudioObjectType ProjectFileSystemModel::getIconType(const QString &path) const +{ + return Q3DStudio::ImportUtils::GetObjectFileTypeForFile(path).m_IconType; +} + +QString ProjectFileSystemModel::getIconName(const QString &path) const +{ + QString iconName; + + bool referenced = m_references.contains(path); + + QFileInfo fileInfo(path); + if (fileInfo.isFile()) { + EStudioObjectType type = getIconType(path); + + if (type == OBJTYPE_PRESENTATION) { + const bool isCurrent = isCurrentPresentation(path); + const bool isInitial = isInitialPresentation(path); + if (isInitial) { + iconName = isCurrent ? QStringLiteral("initial_used.png") + : QStringLiteral("initial_notUsed.png"); + } else if (isCurrent) { + iconName = QStringLiteral("presentation_edit.png"); + } + } + + if (iconName.isEmpty()) { + if (type != OBJTYPE_UNKNOWN) { + iconName = referenced ? CStudioObjectTypes::GetNormalIconName(type) + : CStudioObjectTypes::GetDisabledIconName(type); + } else { + iconName = referenced ? QStringLiteral("Objects-Layer-Normal.png") + : QStringLiteral("Objects-Layer-Disabled.png"); + } + } + } else { + iconName = referenced ? QStringLiteral("Objects-Folder-Normal.png") + : QStringLiteral("Objects-Folder-Disabled.png"); + } + + return iconName; +} + +bool ProjectFileSystemModel::hasVisibleChildren(const QModelIndex &modelIndex) const +{ + const QDir dir(modelIndex.data(QFileSystemModel::FilePathRole).toString()); + if (!dir.exists() || dir.isEmpty()) + return false; + + const auto fileInfoList = dir.entryInfoList(QDir::Dirs|QDir::Files|QDir::NoDotAndDotDot); + for (const auto &fileInfo : fileInfoList) { + if (fileInfo.isDir() || getIconType(fileInfo.filePath()) != OBJTYPE_UNKNOWN) + return true; + } + + return false; +} + +bool ProjectFileSystemModel::isVisible(const QModelIndex &modelIndex) const +{ + QString path = modelIndex.data(QFileSystemModel::FilePathRole).toString(); + + if (modelIndex == m_rootIndex || QFileInfo(path).isDir()) + return true; + + if (path.endsWith(QLatin1String("_autosave.uip")) + || path.endsWith(QLatin1String("_@preview@.uip")) + || path.endsWith(QLatin1String(".uia"))) { + return false; + } + + return getIconType(path) != OBJTYPE_UNKNOWN; +} + +void ProjectFileSystemModel::modelRowsInserted(const QModelIndex &parent, int start, int end) +{ + if (!m_rootIndex.isValid()) + return; + + if (isExpanded(parent)) { + showModelChildItems(parent, start, end); + } else { + if (hasVisibleChildren(parent)) { + // show expand arrow + const int row = modelIndexRow(parent); + Q_EMIT dataChanged(index(row), index(row)); + } + } +} + +void ProjectFileSystemModel::modelRowsRemoved(const QModelIndex &parent, int start, int end) +{ + if (!m_rootIndex.isValid()) + return; + + if (isExpanded(parent)) { + for (int i = start; i <= end; ++i) { + const int row = modelIndexRow(m_model->index(i, 0, parent)); + + if (row != -1) { + const auto &item = m_items.at(row); + + beginRemoveRows({}, row, row + item.childCount); + + for (auto parent = item.parent; parent != nullptr; parent = parent->parent) + parent->childCount -= 1 + item.childCount; + + m_items.erase(std::begin(m_items) + row, + std::begin(m_items) + row + item.childCount + 1); + + endRemoveRows(); + } + } + } + + if (!hasVisibleChildren(parent)) { + // collapse the now empty folder + const int row = modelIndexRow(parent); + if (m_items[row].expanded) + collapse(row); + else + Q_EMIT dataChanged(index(row), index(row)); + } +} + +void ProjectFileSystemModel::modelLayoutChanged() +{ + if (!m_rootIndex.isValid()) + return; + + QSet<QPersistentModelIndex> expandedItems; + for (const auto &item : m_items) { + if (item.expanded) + expandedItems.insert(item.index); + } + + const std::function<int(const QModelIndex &, TreeItem *)> insertChildren = [this, &expandedItems, &insertChildren](const QModelIndex &parentIndex, TreeItem *parent) + { + Q_ASSERT(isVisible(parentIndex)); + + const int rowCount = m_model->rowCount(parentIndex); + const int depth = parent->depth + 1; + + int childCount = 0; + + for (int i = 0; i < rowCount; ++i) { + const auto &childIndex = m_model->index(i, 0, parentIndex); + if (isVisible(childIndex)) { + const bool expanded = expandedItems.contains(childIndex); + m_items.append({ childIndex, depth, expanded, parent, 0 }); + auto &item = m_items.last(); + if (expanded) { + item.childCount = insertChildren(childIndex, &item); + childCount += item.childCount; + } + ++childCount; + } + } + + return childCount; + }; + + const int itemCount = m_items.count(); + + m_items.erase(std::begin(m_items) + 1, std::end(m_items)); + m_items.reserve(itemCount); + insertChildren(m_rootIndex, &m_items.first()); + + Q_ASSERT(m_items.count() == itemCount); + + Q_EMIT dataChanged(index(0), index(itemCount - 1)); +} + +void ProjectFileSystemModel::updateDefaultDirMap() +{ + if (m_defaultDirToAbsPathMap.isEmpty()) { + m_defaultDirToAbsPathMap.insert(QStringLiteral("effects"), QString()); + m_defaultDirToAbsPathMap.insert(QStringLiteral("fonts"), QString()); + m_defaultDirToAbsPathMap.insert(QStringLiteral("maps"), QString()); + m_defaultDirToAbsPathMap.insert(QStringLiteral("materials"), QString()); + m_defaultDirToAbsPathMap.insert(QStringLiteral("models"), QString()); + m_defaultDirToAbsPathMap.insert(QStringLiteral("scripts"), QString()); + m_defaultDirToAbsPathMap.insert(QStringLiteral("presentations"), QString()); + m_defaultDirToAbsPathMap.insert(QStringLiteral("qml"), QString()); + } + + const QString rootPath = m_items[0].index.data(QFileSystemModel::FilePathRole).toString(); + const QStringList keys = m_defaultDirToAbsPathMap.keys(); + for (const QString &key : keys) { + QString currentValue = m_defaultDirToAbsPathMap[key]; + if (currentValue.isEmpty()) { + const QString defaultPath = rootPath + QLatin1Char('/') + key; + const QFileInfo fi(defaultPath); + if (fi.exists() && fi.isDir()) + m_defaultDirToAbsPathMap.insert(key, defaultPath); + } else { + const QFileInfo fi(currentValue); + if (!fi.exists()) + m_defaultDirToAbsPathMap.insert(key, QString()); + } + } +} + +void ProjectFileSystemModel::addPathsToReferences(QSet<QString> &references, + const QString &projectPath, + const QString &origPath) +{ + references.insert(origPath); + QString path = origPath; + QString parentPath = QFileInfo(path).path(); + do { + references.insert(path); + path = parentPath; + parentPath = QFileInfo(path).path(); + } while (path != projectPath && parentPath != path); +} + +void ProjectFileSystemModel::handlePresentationIdChange(const QString &path, const QString &id) +{ + const QString cleanPath = QDir::cleanPath( + QDir(g_StudioApp.GetCore()->GetDoc()->GetCore()->getProjectFile() + .getProjectPath()).absoluteFilePath(path)); + int row = rowForPath(cleanPath); + m_projectReferencesUpdateMap.insert(cleanPath, true); + m_projectReferencesUpdateTimer.start(); + updateRoles({FileIdRole, ExtraIconRole}, row, row); +} + +void ProjectFileSystemModel::asyncExpandPresentations() +{ + disconnect(this, &ProjectFileSystemModel::dataChanged, + this, &ProjectFileSystemModel::asyncExpandPresentations); + + // expand presentation folder by default (if it exists). + QTimer::singleShot(0, [this]() { + QString path = g_StudioApp.GetCore()->getProjectFile().getProjectPath() + + QStringLiteral("/presentations"); + expand(rowForPath(path)); + }); +} + +void ProjectFileSystemModel::asyncUpdateReferences() +{ + QTimer::singleShot(0, this, &ProjectFileSystemModel::updateReferences); +} + +void ProjectFileSystemModel::onFilesChanged( + const Q3DStudio::TFileModificationList &inFileModificationList) +{ + // If any presentation file changes, update asset reference caches + for (size_t idx = 0, end = inFileModificationList.size(); idx < end; ++idx) { + const Q3DStudio::SFileModificationRecord &record(inFileModificationList[idx]); + if (record.m_File.isFile()) { + const QString suffix = record.m_File.suffix(); + const bool isQml = CDialogs::qmlStreamExtensions().contains(suffix); + if (isQml || CDialogs::presentationExtensions().contains(suffix) + || CDialogs::materialExtensions().contains(suffix) + || CDialogs::effectExtensions().contains(suffix)) { + const QString filePath = record.m_File.absoluteFilePath(); + if (record.m_ModificationType == Q3DStudio::FileModificationType::Created + || record.m_ModificationType == Q3DStudio::FileModificationType::Modified) { + if (isQml && !g_StudioApp.isQmlStream(filePath)) + continue; // Skip non-stream qml's to match import logic + m_projectReferencesUpdateMap.insert(filePath, true); + } else if (record.m_ModificationType + == Q3DStudio::FileModificationType::Destroyed) { + m_projectReferencesUpdateMap.insert(filePath, false); + } + m_projectReferencesUpdateTimer.start(); + } + } + } +} + +bool ProjectFileSystemModel::isCurrentPresentation(const QString &path) const +{ + return path == g_StudioApp.GetCore()->GetDoc()->GetDocumentPath(); +} + +bool ProjectFileSystemModel::isInitialPresentation(const QString &path) const +{ + QString checkId = presentationId(path); + + return !checkId.isEmpty() + && checkId == g_StudioApp.GetCore()->getProjectFile().initialPresentation(); +} + +QString ProjectFileSystemModel::presentationId(const QString &path) const +{ + QString presId; + if (isCurrentPresentation(path)) + presId = g_StudioApp.GetCore()->GetDoc()->getPresentationId(); + else + presId = g_StudioApp.getRenderableId(QFileInfo(path).absoluteFilePath()); + + return presId; +} |