#include "ProjectBrowserModelInternal.h" #include #include #include #include #include namespace XCEngine::UI::Editor::App::ProjectBrowserModelInternal { 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 CanPreviewItemKind(ProjectBrowserModel::ItemKind kind) { return kind == ProjectBrowserModel::ItemKind::Texture; } } // namespace XCEngine::UI::Editor::App::ProjectBrowserModelInternal