#include "ProjectBrowserModel.h" #include "ProjectBrowserModelInternal.h" #include "Internal/StringEncoding.h" #include namespace XCEngine::UI::Editor::App { using namespace ProjectBrowserModelInternal; 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 = App::Internal::Utf8ToWide(trimmedName); if (!requestedPath.has_extension()) { requestedPath += L".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() / App::Internal::Utf8ToWide(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