#include "ProjectBrowserModel.h" #include #include #include #include #include #include #include #include #include #include namespace XCEngine::UI::Editor::App { inline constexpr std::string_view kAssetsRootId = "Assets"; std::string ToLowerCopy(std::string value); std::string PathToUtf8String(const std::filesystem::path& path); std::filesystem::path BuildPathFromUtf8(std::string_view value); std::string NormalizePathSeparators(std::string value); std::string BuildRelativeItemId( const std::filesystem::path& path, const std::filesystem::path& assetsRoot); std::string BuildRelativeProjectPath( const std::filesystem::path& path, const std::filesystem::path& projectRoot); std::string BuildAssetDisplayName(const std::filesystem::path& path, bool directory); std::string BuildAssetNameWithExtension(const std::filesystem::path& path, bool directory); bool IsMetaFile(const std::filesystem::path& path); bool HasChildDirectories(const std::filesystem::path& folderPath); std::vector CollectSortedChildDirectories( const std::filesystem::path& folderPath); std::wstring MakePathKey(const std::filesystem::path& path); bool IsSameOrDescendantPath( const std::filesystem::path& path, const std::filesystem::path& ancestor); std::string TrimAssetName(std::string_view name); bool HasInvalidAssetName(std::string_view name); std::filesystem::path MakeUniqueFolderPath( const std::filesystem::path& parentPath, std::string_view preferredName); std::filesystem::path MakeUniqueFilePath( const std::filesystem::path& parentPath, const std::filesystem::path& preferredFileName); std::string BuildRenamedEntryName( const std::filesystem::path& sourcePath, std::string_view requestedName); std::filesystem::path GetMetaSidecarPath(const std::filesystem::path& assetPath); void RemoveMetaSidecarIfPresent(const std::filesystem::path& assetPath); bool RenamePathCaseAware( const std::filesystem::path& sourcePath, const std::filesystem::path& destPath); void MoveMetaSidecarIfPresent( const std::filesystem::path& sourcePath, const std::filesystem::path& destPath); bool MovePathWithOptionalMeta( const std::filesystem::path& sourcePath, const std::filesystem::path& destinationPath); ProjectBrowserModel::ItemKind ResolveItemKind( const std::filesystem::path& path, bool directory); bool CanOpenItemKind(ProjectBrowserModel::ItemKind kind); bool CanPreviewItem( const std::filesystem::path& path, ProjectBrowserModel::ItemKind kind, bool directory); } // namespace XCEngine::UI::Editor::App namespace XCEngine::UI::Editor::App { namespace { std::string BuildUtf8String(std::u8string_view value) { std::string result = {}; result.reserve(value.size()); for (const char8_t character : value) { result.push_back(static_cast(character)); } return result; } std::u8string BuildU8String(std::string_view value) { std::u8string result = {}; result.reserve(value.size()); for (const char character : value) { result.push_back(static_cast(character)); } return result; } } // namespace std::string ToLowerCopy(std::string value) { std::transform( value.begin(), value.end(), value.begin(), [](unsigned char character) { return static_cast(std::tolower(character)); }); return value; } std::string PathToUtf8String(const std::filesystem::path& path) { return BuildUtf8String(path.u8string()); } std::filesystem::path BuildPathFromUtf8(std::string_view value) { return std::filesystem::path(BuildU8String(value)); } std::string NormalizePathSeparators(std::string value) { std::replace(value.begin(), value.end(), '\\', '/'); return value; } std::string BuildRelativeItemId( const std::filesystem::path& path, const std::filesystem::path& assetsRoot) { const std::filesystem::path relative = std::filesystem::relative(path, assetsRoot.parent_path()); const std::string normalized = NormalizePathSeparators(PathToUtf8String(relative.lexically_normal())); return normalized.empty() ? std::string(kAssetsRootId) : normalized; } std::string BuildRelativeProjectPath( const std::filesystem::path& path, const std::filesystem::path& projectRoot) { if (projectRoot.empty() || path.empty()) { return {}; } const std::filesystem::path relative = std::filesystem::relative(path, projectRoot); const std::string normalized = NormalizePathSeparators(PathToUtf8String(relative.lexically_normal())); return normalized; } std::string BuildAssetDisplayName(const std::filesystem::path& path, bool directory) { if (directory) { return PathToUtf8String(path.filename()); } const std::string filename = PathToUtf8String(path.filename()); const std::size_t extensionOffset = filename.find_last_of('.'); if (extensionOffset == std::string::npos || extensionOffset == 0u) { return filename; } return filename.substr(0u, extensionOffset); } std::string BuildAssetNameWithExtension(const std::filesystem::path& path, bool directory) { return directory ? PathToUtf8String(path.filename()) : PathToUtf8String(path.filename()); } bool IsMetaFile(const std::filesystem::path& path) { return ToLowerCopy(path.extension().string()) == ".meta"; } bool HasChildDirectories(const std::filesystem::path& folderPath) { std::error_code errorCode = {}; const std::filesystem::directory_iterator end = {}; for (std::filesystem::directory_iterator iterator(folderPath, errorCode); !errorCode && iterator != end; iterator.increment(errorCode)) { if (iterator->is_directory(errorCode)) { return true; } } return false; } std::vector CollectSortedChildDirectories( const std::filesystem::path& folderPath) { std::vector paths = {}; std::error_code errorCode = {}; const std::filesystem::directory_iterator end = {}; for (std::filesystem::directory_iterator iterator(folderPath, errorCode); !errorCode && iterator != end; iterator.increment(errorCode)) { if (iterator->is_directory(errorCode)) { paths.push_back(iterator->path()); } } std::sort( paths.begin(), paths.end(), [](const std::filesystem::path& lhs, const std::filesystem::path& rhs) { return ToLowerCopy(PathToUtf8String(lhs.filename())) < ToLowerCopy(PathToUtf8String(rhs.filename())); }); return paths; } std::wstring MakePathKey(const std::filesystem::path& path) { std::wstring key = path.lexically_normal().generic_wstring(); std::transform(key.begin(), key.end(), key.begin(), ::towlower); return key; } bool IsSameOrDescendantPath( const std::filesystem::path& path, const std::filesystem::path& ancestor) { const std::wstring pathKey = MakePathKey(path); std::wstring ancestorKey = MakePathKey(ancestor); if (pathKey.empty() || ancestorKey.empty()) { return false; } if (pathKey == ancestorKey) { return true; } if (ancestorKey.back() != L'/') { ancestorKey += L'/'; } return pathKey.rfind(ancestorKey, 0) == 0; } std::string TrimAssetName(std::string_view name) { const auto first = std::find_if_not(name.begin(), name.end(), [](unsigned char character) { return std::isspace(character) != 0; }); if (first == name.end()) { return {}; } const auto last = std::find_if_not(name.rbegin(), name.rend(), [](unsigned char character) { return std::isspace(character) != 0; }).base(); return std::string(first, last); } bool HasInvalidAssetName(std::string_view name) { if (name.empty() || name == "." || name == "..") { return true; } return name.find_first_of("\\/:*?\"<>|") != std::string_view::npos; } std::filesystem::path MakeUniqueFolderPath( const std::filesystem::path& parentPath, std::string_view preferredName) { std::filesystem::path candidatePath = parentPath / BuildPathFromUtf8(preferredName); if (!std::filesystem::exists(candidatePath)) { return candidatePath; } for (std::size_t suffix = 1u;; ++suffix) { candidatePath = parentPath / BuildPathFromUtf8( std::string(preferredName) + " " + std::to_string(suffix)); if (!std::filesystem::exists(candidatePath)) { return candidatePath; } } } std::filesystem::path MakeUniqueFilePath( const std::filesystem::path& parentPath, const std::filesystem::path& preferredFileName) { std::filesystem::path candidatePath = parentPath / preferredFileName; if (!std::filesystem::exists(candidatePath)) { return candidatePath; } const std::wstring stem = preferredFileName.stem().wstring(); const std::wstring extension = preferredFileName.extension().wstring(); for (std::size_t suffix = 1u;; ++suffix) { candidatePath = parentPath / std::filesystem::path( stem + L" " + std::to_wstring(suffix) + extension); if (!std::filesystem::exists(candidatePath)) { return candidatePath; } } } std::string BuildRenamedEntryName( const std::filesystem::path& sourcePath, std::string_view requestedName) { if (std::filesystem::is_directory(sourcePath)) { return std::string(requestedName); } const std::filesystem::path requestedPath = BuildPathFromUtf8(requestedName); if (requestedPath.has_extension()) { return PathToUtf8String(requestedPath.filename()); } return std::string(requestedName) + PathToUtf8String(sourcePath.extension()); } std::filesystem::path GetMetaSidecarPath(const std::filesystem::path& assetPath) { return std::filesystem::path(assetPath.native() + std::filesystem::path(L".meta").native()); } void RemoveMetaSidecarIfPresent(const std::filesystem::path& assetPath) { std::error_code errorCode = {}; std::filesystem::remove_all(GetMetaSidecarPath(assetPath), errorCode); } namespace { std::filesystem::path MakeCaseOnlyRenameTempPath(const std::filesystem::path& sourcePath) { const std::filesystem::path parentPath = sourcePath.parent_path(); const std::wstring sourceStem = sourcePath.filename().wstring(); for (std::size_t suffix = 0u;; ++suffix) { std::wstring tempName = sourceStem + L".xc_tmp_rename"; if (suffix > 0u) { tempName += std::to_wstring(suffix); } const std::filesystem::path tempPath = parentPath / tempName; if (!std::filesystem::exists(tempPath)) { return tempPath; } } } bool MatchesExtension( std::wstring_view extension, std::initializer_list candidates) { for (const std::wstring_view candidate : candidates) { if (extension == candidate) { return true; } } return false; } } // namespace bool RenamePathCaseAware( const std::filesystem::path& sourcePath, const std::filesystem::path& destPath) { if (MakePathKey(sourcePath) != MakePathKey(destPath)) { if (std::filesystem::exists(destPath)) { return false; } std::filesystem::rename(sourcePath, destPath); return true; } if (sourcePath.filename() == destPath.filename()) { return true; } const std::filesystem::path tempPath = MakeCaseOnlyRenameTempPath(sourcePath); std::filesystem::rename(sourcePath, tempPath); std::filesystem::rename(tempPath, destPath); return true; } void MoveMetaSidecarIfPresent( const std::filesystem::path& sourcePath, const std::filesystem::path& destPath) { const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourcePath); if (!std::filesystem::exists(sourceMetaPath)) { return; } const std::filesystem::path destMetaPath = GetMetaSidecarPath(destPath); RenamePathCaseAware(sourceMetaPath, destMetaPath); } bool MovePathWithOptionalMeta( const std::filesystem::path& sourcePath, const std::filesystem::path& destinationPath) { std::error_code errorCode = {}; const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourcePath); const std::filesystem::path destinationMetaPath = GetMetaSidecarPath(destinationPath); const bool moveMeta = std::filesystem::exists(sourceMetaPath, errorCode); if (errorCode) { return false; } std::filesystem::rename(sourcePath, destinationPath, errorCode); if (errorCode) { return false; } if (!moveMeta) { return true; } std::filesystem::rename(sourceMetaPath, destinationMetaPath, errorCode); if (!errorCode) { return true; } std::error_code rollbackError = {}; std::filesystem::rename(destinationPath, sourcePath, rollbackError); return false; } ProjectBrowserModel::ItemKind ResolveItemKind( const std::filesystem::path& path, bool directory) { if (directory) { return ProjectBrowserModel::ItemKind::Folder; } std::wstring extension = path.extension().wstring(); std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower); if (MatchesExtension(extension, { L".xc", L".unity", L".scene" })) { return ProjectBrowserModel::ItemKind::Scene; } if (MatchesExtension(extension, { L".fbx", L".obj", L".gltf", L".glb" })) { return ProjectBrowserModel::ItemKind::Model; } if (extension == L".mat") { return ProjectBrowserModel::ItemKind::Material; } if (MatchesExtension(extension, { L".png", L".jpg", L".jpeg", L".tga", L".bmp", L".gif", L".psd", L".hdr", L".pic", L".ppm", L".pgm", L".pbm", L".pnm", L".dds", L".ktx", L".ktx2", L".webp" })) { return ProjectBrowserModel::ItemKind::Texture; } if (MatchesExtension(extension, { L".cs", L".cpp", L".c", L".h", L".hpp" })) { return ProjectBrowserModel::ItemKind::Script; } return ProjectBrowserModel::ItemKind::File; } bool CanOpenItemKind(ProjectBrowserModel::ItemKind kind) { return kind == ProjectBrowserModel::ItemKind::Folder || kind == ProjectBrowserModel::ItemKind::Scene; } bool CanPreviewItem( const std::filesystem::path& path, ProjectBrowserModel::ItemKind kind, bool directory) { if (directory || kind != ProjectBrowserModel::ItemKind::Texture) { return false; } std::wstring extension = path.extension().wstring(); std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower); return MatchesExtension(extension, { L".png", L".jpg", L".jpeg", L".tga", L".bmp", L".gif", L".psd", L".hdr", L".pic", L".ppm", L".pgm", L".pbm", L".pnm" }); } } // namespace XCEngine::UI::Editor::App namespace XCEngine::UI::Editor::App { namespace { bool IsSameOrDescendantFolderId( std::string_view candidateId, std::string_view ancestorId) { if (candidateId == ancestorId) { return true; } if (candidateId.size() <= ancestorId.size() || candidateId.substr(0u, ancestorId.size()) != ancestorId) { return false; } return candidateId[ancestorId.size()] == '/'; } std::string RemapMovedFolderId( std::string_view itemId, std::string_view sourceFolderId, std::string_view destinationFolderId) { if (!IsSameOrDescendantFolderId(itemId, sourceFolderId)) { return std::string(itemId); } if (itemId == sourceFolderId) { return std::string(destinationFolderId); } std::string remapped = std::string(destinationFolderId); remapped += std::string(itemId.substr(sourceFolderId.size())); return remapped; } bool IsSameOrDescendantItemId( std::string_view candidateId, std::string_view ancestorId) { if (candidateId == ancestorId) { return true; } if (candidateId.size() <= ancestorId.size() || candidateId.substr(0u, ancestorId.size()) != ancestorId) { return false; } return candidateId[ancestorId.size()] == '/'; } std::string RemapMovedItemId( std::string_view itemId, std::string_view sourceItemId, std::string_view destinationItemId) { if (!IsSameOrDescendantItemId(itemId, sourceItemId)) { return std::string(itemId); } if (itemId == sourceItemId) { return std::string(destinationItemId); } std::string remapped = std::string(destinationItemId); remapped += std::string(itemId.substr(sourceItemId.size())); return remapped; } } // namespace void ProjectBrowserModel::Initialize(const std::filesystem::path& repoRoot) { m_assetsRootPath = (repoRoot / "project/Assets").lexically_normal(); std::error_code errorCode = {}; if (!std::filesystem::exists(m_assetsRootPath, errorCode)) { std::filesystem::create_directories(m_assetsRootPath / "Scenes", errorCode); } Refresh(); } void ProjectBrowserModel::SetFolderIcon(const ::XCEngine::UI::UITextureHandle& icon) { m_folderIcon = icon; if (!m_assetsRootPath.empty()) { RefreshFolderTree(); } } void ProjectBrowserModel::Refresh() { RefreshFolderTree(); EnsureValidCurrentFolder(); RefreshAssetList(); } bool ProjectBrowserModel::Empty() const { return m_treeItems.empty(); } std::filesystem::path ProjectBrowserModel::GetProjectRootPath() const { return m_assetsRootPath.empty() ? std::filesystem::path() : m_assetsRootPath.parent_path(); } const std::filesystem::path& ProjectBrowserModel::GetAssetsRootPath() const { return m_assetsRootPath; } const std::vector& ProjectBrowserModel::GetFolderEntries() const { return m_folderEntries; } const std::vector& ProjectBrowserModel::GetTreeItems() const { return m_treeItems; } const std::vector& ProjectBrowserModel::GetAssetEntries() const { return m_assetEntries; } const std::string& ProjectBrowserModel::GetCurrentFolderId() const { return m_currentFolderId; } bool ProjectBrowserModel::IsAssetsRoot(std::string_view itemId) const { return itemId == kAssetsRootId; } const ProjectBrowserModel::FolderEntry* ProjectBrowserModel::FindFolderEntry( std::string_view itemId) const { for (const FolderEntry& entry : m_folderEntries) { if (entry.itemId == itemId) { return &entry; } } return nullptr; } const ProjectBrowserModel::AssetEntry* ProjectBrowserModel::FindAssetEntry( std::string_view itemId) const { for (const AssetEntry& entry : m_assetEntries) { if (entry.itemId == itemId) { return &entry; } } return nullptr; } std::optional ProjectBrowserModel::ResolveItemAbsolutePath( std::string_view itemId) const { if (itemId.empty() || m_assetsRootPath.empty()) { return std::nullopt; } const std::filesystem::path rootPath = m_assetsRootPath.parent_path(); const std::filesystem::path candidatePath = (rootPath / std::filesystem::path(std::string(itemId))).lexically_normal(); if (!IsSameOrDescendantPath(candidatePath, m_assetsRootPath)) { return std::nullopt; } return candidatePath; } std::string ProjectBrowserModel::BuildProjectRelativePath(std::string_view itemId) const { const std::optional absolutePath = ResolveItemAbsolutePath(itemId); if (!absolutePath.has_value()) { return {}; } return BuildRelativeProjectPath(absolutePath.value(), GetProjectRootPath()); } bool ProjectBrowserModel::CreateFolder( std::string_view parentFolderId, std::string_view requestedName, std::string* createdFolderId) { if (createdFolderId != nullptr) { createdFolderId->clear(); } const FolderEntry* parentFolder = FindFolderEntry(parentFolderId); if (parentFolder == nullptr) { return false; } try { const std::string trimmedName = TrimAssetName(requestedName); if (HasInvalidAssetName(trimmedName)) { return false; } const std::filesystem::path newFolderPath = MakeUniqueFolderPath(parentFolder->absolutePath, trimmedName); if (!std::filesystem::create_directory(newFolderPath)) { return false; } RefreshFolderTree(); EnsureValidCurrentFolder(); RefreshAssetList(); if (createdFolderId != nullptr) { *createdFolderId = BuildRelativeItemId(newFolderPath, m_assetsRootPath); } return true; } catch (...) { return false; } } bool ProjectBrowserModel::CreateMaterial( std::string_view parentFolderId, std::string_view requestedName, std::string* createdItemId) { if (createdItemId != nullptr) { createdItemId->clear(); } const FolderEntry* parentFolder = FindFolderEntry(parentFolderId); if (parentFolder == nullptr) { return false; } try { const std::string trimmedName = TrimAssetName(requestedName); if (HasInvalidAssetName(trimmedName)) { return false; } std::filesystem::path requestedPath = BuildPathFromUtf8(trimmedName); if (!requestedPath.has_extension()) { requestedPath.replace_extension(".mat"); } const std::filesystem::path materialPath = MakeUniqueFilePath(parentFolder->absolutePath, requestedPath); std::ofstream output(materialPath, std::ios::out | std::ios::trunc); if (!output.is_open()) { return false; } output << "{\n" " \"renderQueue\": \"geometry\",\n" " \"renderState\": {\n" " \"cull\": \"none\",\n" " \"depthTest\": true,\n" " \"depthWrite\": true,\n" " \"depthFunc\": \"less\",\n" " \"blendEnable\": false,\n" " \"srcBlend\": \"one\",\n" " \"dstBlend\": \"zero\",\n" " \"srcBlendAlpha\": \"one\",\n" " \"dstBlendAlpha\": \"zero\",\n" " \"blendOp\": \"add\",\n" " \"blendOpAlpha\": \"add\",\n" " \"colorWriteMask\": 15\n" " }\n" "}\n"; output.close(); if (!output.good()) { return false; } RefreshFolderTree(); EnsureValidCurrentFolder(); RefreshAssetList(); if (createdItemId != nullptr) { *createdItemId = BuildRelativeItemId(materialPath, m_assetsRootPath); } return true; } catch (...) { return false; } } bool ProjectBrowserModel::RenameItem( std::string_view itemId, std::string_view newName, std::string* renamedItemId) { if (renamedItemId != nullptr) { renamedItemId->clear(); } if (itemId.empty() || IsAssetsRoot(itemId)) { return false; } const std::optional sourcePath = ResolveItemAbsolutePath(itemId); if (!sourcePath.has_value()) { return false; } try { const std::string trimmedName = TrimAssetName(newName); if (HasInvalidAssetName(trimmedName) || !std::filesystem::exists(sourcePath.value())) { return false; } const std::string targetName = BuildRenamedEntryName(sourcePath.value(), trimmedName); if (HasInvalidAssetName(targetName)) { return false; } const std::filesystem::path destinationPath = (sourcePath->parent_path() / BuildPathFromUtf8(targetName)) .lexically_normal(); const std::string destinationItemId = BuildRelativeItemId(destinationPath, m_assetsRootPath); if (!RenamePathCaseAware(sourcePath.value(), destinationPath)) { return false; } MoveMetaSidecarIfPresent(sourcePath.value(), destinationPath); if (std::filesystem::is_directory(destinationPath)) { m_currentFolderId = RemapMovedItemId( m_currentFolderId, itemId, destinationItemId); } RefreshFolderTree(); EnsureValidCurrentFolder(); RefreshAssetList(); if (renamedItemId != nullptr) { *renamedItemId = destinationItemId; } return true; } catch (...) { return false; } } bool ProjectBrowserModel::DeleteItem(std::string_view itemId) { if (itemId.empty() || IsAssetsRoot(itemId)) { return false; } const std::optional sourcePath = ResolveItemAbsolutePath(itemId); if (!sourcePath.has_value()) { return false; } try { if (!std::filesystem::exists(sourcePath.value())) { return false; } if (std::filesystem::is_directory(sourcePath.value()) && IsSameOrDescendantFolderId(m_currentFolderId, itemId)) { m_currentFolderId = BuildRelativeItemId(sourcePath->parent_path(), m_assetsRootPath); } std::filesystem::remove_all(sourcePath.value()); RemoveMetaSidecarIfPresent(sourcePath.value()); RefreshFolderTree(); EnsureValidCurrentFolder(); RefreshAssetList(); return true; } catch (...) { return false; } } bool ProjectBrowserModel::CanMoveItemToFolder( std::string_view itemId, std::string_view targetFolderId) const { if (itemId.empty() || targetFolderId.empty() || IsAssetsRoot(itemId)) { return false; } const std::optional sourcePath = ResolveItemAbsolutePath(itemId); const FolderEntry* targetFolder = FindFolderEntry(targetFolderId); if (!sourcePath.has_value() || targetFolder == nullptr) { return false; } std::error_code errorCode = {}; if (!std::filesystem::exists(sourcePath.value(), errorCode) || errorCode) { return false; } const std::filesystem::path destinationPath = (targetFolder->absolutePath / sourcePath->filename()).lexically_normal(); if (destinationPath == sourcePath->lexically_normal()) { return false; } if (std::filesystem::exists(destinationPath, errorCode) || errorCode) { return false; } if (std::filesystem::is_directory(sourcePath.value(), errorCode)) { if (errorCode) { return false; } if (IsSameOrDescendantPath(targetFolder->absolutePath, sourcePath.value())) { return false; } } const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourcePath.value()); const std::filesystem::path destinationMetaPath = GetMetaSidecarPath(destinationPath); const bool sourceMetaExists = std::filesystem::exists(sourceMetaPath, errorCode); if (errorCode) { return false; } if (sourceMetaExists && std::filesystem::exists(destinationMetaPath, errorCode)) { return false; } return !errorCode; } bool ProjectBrowserModel::MoveItemToFolder( std::string_view itemId, std::string_view targetFolderId, std::string* movedItemId) { if (movedItemId != nullptr) { movedItemId->clear(); } if (!CanMoveItemToFolder(itemId, targetFolderId)) { return false; } const std::optional sourcePath = ResolveItemAbsolutePath(itemId); const FolderEntry* targetFolder = FindFolderEntry(targetFolderId); if (!sourcePath.has_value() || targetFolder == nullptr) { return false; } try { const std::filesystem::path destinationPath = (targetFolder->absolutePath / sourcePath->filename()).lexically_normal(); const std::string destinationItemId = BuildRelativeItemId(destinationPath, m_assetsRootPath); if (!MovePathWithOptionalMeta(sourcePath.value(), destinationPath)) { return false; } if (std::filesystem::is_directory(destinationPath)) { m_currentFolderId = RemapMovedItemId( m_currentFolderId, itemId, destinationItemId); } RefreshFolderTree(); EnsureValidCurrentFolder(); RefreshAssetList(); if (movedItemId != nullptr) { *movedItemId = destinationItemId; } return true; } catch (...) { return false; } } std::optional ProjectBrowserModel::GetParentFolderId(std::string_view itemId) const { const FolderEntry* folderEntry = FindFolderEntry(itemId); if (folderEntry == nullptr || itemId == kAssetsRootId) { return std::nullopt; } const std::filesystem::path parentPath = folderEntry->absolutePath.parent_path(); if (parentPath.empty() || parentPath == folderEntry->absolutePath) { return std::nullopt; } return BuildRelativeItemId(parentPath, m_assetsRootPath); } bool ProjectBrowserModel::CanReparentFolder( std::string_view sourceFolderId, std::string_view targetParentId) const { if (sourceFolderId.empty() || targetParentId.empty() || sourceFolderId == kAssetsRootId) { return false; } const FolderEntry* sourceFolder = FindFolderEntry(sourceFolderId); const FolderEntry* targetFolder = FindFolderEntry(targetParentId); if (sourceFolder == nullptr || targetFolder == nullptr) { return false; } if (IsSameOrDescendantFolderId(targetParentId, sourceFolderId)) { return false; } const std::optional currentParentId = GetParentFolderId(sourceFolderId); if (currentParentId.has_value() && currentParentId.value() == targetParentId) { return false; } const std::filesystem::path destinationPath = targetFolder->absolutePath / sourceFolder->absolutePath.filename(); if (destinationPath.lexically_normal() == sourceFolder->absolutePath.lexically_normal()) { return false; } std::error_code errorCode = {}; if (std::filesystem::exists(destinationPath, errorCode)) { return false; } if (errorCode) { return false; } const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourceFolder->absolutePath); const std::filesystem::path destinationMetaPath = GetMetaSidecarPath(destinationPath); const bool sourceMetaExists = std::filesystem::exists(sourceMetaPath, errorCode); if (errorCode) { return false; } if (sourceMetaExists && std::filesystem::exists(destinationMetaPath, errorCode)) { return false; } return !errorCode; } bool ProjectBrowserModel::ReparentFolder( std::string_view sourceFolderId, std::string_view targetParentId, std::string* movedFolderId) { if (!CanReparentFolder(sourceFolderId, targetParentId)) { return false; } const FolderEntry* sourceFolder = FindFolderEntry(sourceFolderId); const FolderEntry* targetFolder = FindFolderEntry(targetParentId); if (sourceFolder == nullptr || targetFolder == nullptr) { return false; } const std::filesystem::path destinationPath = (targetFolder->absolutePath / sourceFolder->absolutePath.filename()).lexically_normal(); const std::string destinationFolderId = BuildRelativeItemId(destinationPath, m_assetsRootPath); if (!MovePathWithOptionalMeta(sourceFolder->absolutePath, destinationPath)) { return false; } m_currentFolderId = RemapMovedFolderId( m_currentFolderId, sourceFolderId, destinationFolderId); RefreshFolderTree(); EnsureValidCurrentFolder(); RefreshAssetList(); if (movedFolderId != nullptr) { *movedFolderId = destinationFolderId; } return true; } bool ProjectBrowserModel::MoveFolderToRoot( std::string_view sourceFolderId, std::string* movedFolderId) { if (sourceFolderId.empty() || sourceFolderId == kAssetsRootId) { return false; } const FolderEntry* sourceFolder = FindFolderEntry(sourceFolderId); if (sourceFolder == nullptr) { return false; } const std::optional currentParentId = GetParentFolderId(sourceFolderId); if (!currentParentId.has_value() || currentParentId.value() == kAssetsRootId) { return false; } const std::filesystem::path destinationPath = (m_assetsRootPath / sourceFolder->absolutePath.filename()).lexically_normal(); if (destinationPath == sourceFolder->absolutePath.lexically_normal()) { return false; } std::error_code errorCode = {}; if (std::filesystem::exists(destinationPath, errorCode)) { return false; } if (errorCode) { return false; } const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourceFolder->absolutePath); const std::filesystem::path destinationMetaPath = GetMetaSidecarPath(destinationPath); const bool sourceMetaExists = std::filesystem::exists(sourceMetaPath, errorCode); if (errorCode) { return false; } if (sourceMetaExists && std::filesystem::exists(destinationMetaPath, errorCode)) { return false; } if (errorCode) { return false; } const std::string destinationFolderId = BuildRelativeItemId(destinationPath, m_assetsRootPath); if (!MovePathWithOptionalMeta(sourceFolder->absolutePath, destinationPath)) { return false; } m_currentFolderId = RemapMovedFolderId( m_currentFolderId, sourceFolderId, destinationFolderId); RefreshFolderTree(); EnsureValidCurrentFolder(); RefreshAssetList(); if (movedFolderId != nullptr) { *movedFolderId = destinationFolderId; } return true; } bool ProjectBrowserModel::NavigateToFolder(std::string_view itemId) { if (itemId.empty() || FindFolderEntry(itemId) == nullptr || itemId == m_currentFolderId) { return false; } m_currentFolderId = std::string(itemId); EnsureValidCurrentFolder(); RefreshAssetList(); return true; } std::vector ProjectBrowserModel::BuildBreadcrumbSegments() const { std::vector segments = {}; if (m_currentFolderId.empty()) { segments.push_back(BreadcrumbSegment{ std::string(kAssetsRootId), std::string(kAssetsRootId), true }); return segments; } std::string cumulativeFolderId = {}; std::size_t segmentStart = 0u; while (segmentStart < m_currentFolderId.size()) { const std::size_t separator = m_currentFolderId.find('/', segmentStart); const std::size_t segmentLength = separator == std::string_view::npos ? m_currentFolderId.size() - segmentStart : separator - segmentStart; if (segmentLength > 0u) { std::string label = std::string(m_currentFolderId.substr(segmentStart, segmentLength)); if (cumulativeFolderId.empty()) { cumulativeFolderId = label; } else { cumulativeFolderId += "/"; cumulativeFolderId += label; } segments.push_back(BreadcrumbSegment{ std::move(label), cumulativeFolderId, false }); } if (separator == std::string_view::npos) { break; } segmentStart = separator + 1u; } if (segments.empty()) { segments.push_back(BreadcrumbSegment{ std::string(kAssetsRootId), std::string(kAssetsRootId), true }); return segments; } segments.back().current = true; return segments; } void ProjectBrowserModel::EnsureValidCurrentFolder() { if (m_currentFolderId.empty()) { m_currentFolderId = std::string(kAssetsRootId); } if (FindFolderEntry(m_currentFolderId) == nullptr) { m_currentFolderId = m_treeItems.empty() ? std::string() : m_treeItems.front().itemId; } } } // namespace XCEngine::UI::Editor::App namespace XCEngine::UI::Editor::App { void ProjectBrowserModel::RefreshAssetList() { EnsureValidCurrentFolder(); m_assetEntries.clear(); const FolderEntry* currentFolder = FindFolderEntry(m_currentFolderId); if (currentFolder == nullptr) { return; } std::vector entries = {}; std::error_code errorCode = {}; const std::filesystem::directory_iterator end = {}; for (std::filesystem::directory_iterator iterator(currentFolder->absolutePath, errorCode); !errorCode && iterator != end; iterator.increment(errorCode)) { if (!iterator->exists(errorCode) || IsMetaFile(iterator->path())) { continue; } if (!iterator->is_directory(errorCode) && !iterator->is_regular_file(errorCode)) { continue; } entries.push_back(*iterator); } std::sort( entries.begin(), entries.end(), [](const std::filesystem::directory_entry& lhs, const std::filesystem::directory_entry& rhs) { const bool lhsDirectory = lhs.is_directory(); const bool rhsDirectory = rhs.is_directory(); if (lhsDirectory != rhsDirectory) { return lhsDirectory && !rhsDirectory; } return ToLowerCopy(PathToUtf8String(lhs.path().filename())) < ToLowerCopy(PathToUtf8String(rhs.path().filename())); }); for (const std::filesystem::directory_entry& entry : entries) { AssetEntry assetEntry = {}; assetEntry.itemId = BuildRelativeItemId(entry.path(), m_assetsRootPath); assetEntry.absolutePath = entry.path(); assetEntry.nameWithExtension = BuildAssetNameWithExtension(entry.path(), entry.is_directory()); assetEntry.displayName = BuildAssetDisplayName(entry.path(), entry.is_directory()); assetEntry.extensionLower = ToLowerCopy(PathToUtf8String(entry.path().extension())); assetEntry.kind = ResolveItemKind(entry.path(), entry.is_directory()); assetEntry.directory = entry.is_directory(); assetEntry.canOpen = CanOpenItemKind(assetEntry.kind); assetEntry.canPreview = CanPreviewItem(entry.path(), assetEntry.kind, assetEntry.directory); m_assetEntries.push_back(std::move(assetEntry)); } } } // namespace XCEngine::UI::Editor::App namespace XCEngine::UI::Editor::App { void ProjectBrowserModel::RefreshFolderTree() { m_folderEntries.clear(); m_treeItems.clear(); if (m_assetsRootPath.empty() || !std::filesystem::exists(m_assetsRootPath)) { return; } const auto appendFolderRecursive = [&](auto&& self, const std::filesystem::path& folderPath, std::uint32_t depth) -> void { const std::string itemId = BuildRelativeItemId(folderPath, m_assetsRootPath); FolderEntry folderEntry = {}; folderEntry.itemId = itemId; folderEntry.absolutePath = folderPath; folderEntry.label = PathToUtf8String(folderPath.filename()); m_folderEntries.push_back(folderEntry); Widgets::UIEditorTreeViewItem item = {}; item.itemId = itemId; item.label = folderEntry.label; item.depth = depth; item.forceLeaf = !HasChildDirectories(folderPath); item.leadingIcon = m_folderIcon; m_treeItems.push_back(std::move(item)); const std::vector childFolders = CollectSortedChildDirectories(folderPath); for (const std::filesystem::path& childPath : childFolders) { self(self, childPath, depth + 1u); } }; appendFolderRecursive(appendFolderRecursive, m_assetsRootPath, 0u); } std::vector ProjectBrowserModel::CollectCurrentFolderAncestorIds() const { return BuildAncestorFolderIds(m_currentFolderId); } std::vector ProjectBrowserModel::BuildAncestorFolderIds( std::string_view itemId) const { std::vector ancestors = {}; const FolderEntry* folderEntry = FindFolderEntry(itemId); if (folderEntry == nullptr) { return ancestors; } std::filesystem::path path = folderEntry->absolutePath; while (true) { ancestors.push_back(BuildRelativeItemId(path, m_assetsRootPath)); if (path == m_assetsRootPath) { break; } path = path.parent_path(); } std::reverse(ancestors.begin(), ancestors.end()); return ancestors; } } // namespace XCEngine::UI::Editor::App