#include "Project/ProductProjectBrowserModel.h" #include #include #include #include #include namespace XCEngine::UI::Editor::App::Project { namespace { constexpr std::string_view kAssetsRootId = "Assets"; 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 WideToUtf8(std::wstring_view value) { if (value.empty()) { return {}; } const int requiredSize = WideCharToMultiByte( CP_UTF8, 0, value.data(), static_cast(value.size()), nullptr, 0, nullptr, nullptr); if (requiredSize <= 0) { return {}; } std::string result(static_cast(requiredSize), '\0'); WideCharToMultiByte( CP_UTF8, 0, value.data(), static_cast(value.size()), result.data(), requiredSize, nullptr, nullptr); return result; } std::string PathToUtf8String(const std::filesystem::path& path) { return WideToUtf8(path.native()); } 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 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); } 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; } } // namespace void ProductProjectBrowserModel::Initialize(const std::filesystem::path& repoRoot) { m_assetsRootPath = (repoRoot / "project/Assets").lexically_normal(); Refresh(); } void ProductProjectBrowserModel::SetFolderIcon(const ::XCEngine::UI::UITextureHandle& icon) { m_folderIcon = icon; if (!m_assetsRootPath.empty()) { RefreshFolderTree(); } } void ProductProjectBrowserModel::Refresh() { RefreshFolderTree(); EnsureValidCurrentFolder(); RefreshAssetList(); } bool ProductProjectBrowserModel::Empty() const { return m_treeItems.empty(); } const std::filesystem::path& ProductProjectBrowserModel::GetAssetsRootPath() const { return m_assetsRootPath; } const std::vector& ProductProjectBrowserModel::GetFolderEntries() const { return m_folderEntries; } const std::vector& ProductProjectBrowserModel::GetTreeItems() const { return m_treeItems; } const std::vector& ProductProjectBrowserModel::GetAssetEntries() const { return m_assetEntries; } const std::string& ProductProjectBrowserModel::GetCurrentFolderId() const { return m_currentFolderId; } const ProductProjectBrowserModel::FolderEntry* ProductProjectBrowserModel::FindFolderEntry( std::string_view itemId) const { for (const FolderEntry& entry : m_folderEntries) { if (entry.itemId == itemId) { return &entry; } } return nullptr; } const ProductProjectBrowserModel::AssetEntry* ProductProjectBrowserModel::FindAssetEntry( std::string_view itemId) const { for (const AssetEntry& entry : m_assetEntries) { if (entry.itemId == itemId) { return &entry; } } return nullptr; } bool ProductProjectBrowserModel::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 ProductProjectBrowserModel::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; } std::vector ProductProjectBrowserModel::CollectCurrentFolderAncestorIds() const { return BuildAncestorFolderIds(m_currentFolderId); } std::vector ProductProjectBrowserModel::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; } void ProductProjectBrowserModel::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); } void ProductProjectBrowserModel::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.displayName = BuildAssetDisplayName(entry.path(), entry.is_directory()); assetEntry.directory = entry.is_directory(); m_assetEntries.push_back(std::move(assetEntry)); } } void ProductProjectBrowserModel::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::Project