From cd309736f92061cda458a8c29a7c1add3e8e0a96 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 1 May 2026 00:55:11 +0800 Subject: [PATCH] Enforce single source of truth for editor project folder state --- .../Workspace/Project/ProjectPanel.cpp | 79 ++++--- .../Features/Workspace/Project/ProjectPanel.h | 6 +- .../Services/Project/EditorProjectRuntime.cpp | 155 +++++++++---- .../Services/Project/EditorProjectRuntime.h | 14 +- .../Services/Project/ProjectBrowserModel.cpp | 210 ++++++++++-------- .../Services/Project/ProjectBrowserModel.h | 38 ++-- .../unit/test_editor_project_runtime.cpp | 8 +- .../unit/test_project_browser_model.cpp | 72 +++--- 8 files changed, 355 insertions(+), 227 deletions(-) diff --git a/editor/src/Product/Features/Workspace/Project/ProjectPanel.cpp b/editor/src/Product/Features/Workspace/Project/ProjectPanel.cpp index c50eb773..77dc5cd3 100644 --- a/editor/src/Product/Features/Workspace/Project/ProjectPanel.cpp +++ b/editor/src/Product/Features/Workspace/Project/ProjectPanel.cpp @@ -319,12 +319,29 @@ bool ProjectPanel::HasProjectRuntime() const { return m_projectRuntime != nullptr; } -ProjectPanel::BrowserModel& ProjectPanel::GetBrowserModel() { +const ProjectPanel::BrowserModel& ProjectPanel::GetBrowserModel() const { return m_projectRuntime->GetBrowserModel(); } -const ProjectPanel::BrowserModel& ProjectPanel::GetBrowserModel() const { - return m_projectRuntime->GetBrowserModel(); +std::string_view ProjectPanel::GetCurrentFolderId() const { + return m_projectRuntime != nullptr ? m_projectRuntime->GetCurrentFolderId() : std::string_view{}; +} + +const std::vector& ProjectPanel::GetCurrentFolderAssets() const { + static const std::vector kEmptyAssets = {}; + return m_projectRuntime != nullptr ? m_projectRuntime->GetCurrentFolderAssets() : kEmptyAssets; +} + +std::vector ProjectPanel::BuildCurrentFolderBreadcrumbs() const { + return m_projectRuntime != nullptr + ? m_projectRuntime->BuildCurrentFolderBreadcrumbs() + : std::vector{}; +} + +std::vector ProjectPanel::BuildCurrentFolderAncestorIds() const { + return m_projectRuntime != nullptr + ? m_projectRuntime->BuildCurrentFolderAncestorIds() + : std::vector{}; } void ProjectPanel::RebuildWindowTreeItems() { @@ -472,7 +489,7 @@ const ProjectPanel::FolderEntry* ProjectPanel::GetSelectedFolderEntry() const { return nullptr; } - return FindFolderEntry(GetBrowserModel().GetCurrentFolderId()); + return FindFolderEntry(GetCurrentFolderId()); } void ProjectPanel::ClearRenameState() { @@ -530,12 +547,12 @@ UIRect ProjectPanel::BuildRenameBounds( if (surface == RenameSurface::Grid) { for (const AssetTileLayout& tile : m_layout.assetTiles) { - if (tile.itemIndex >= GetBrowserModel().GetAssetEntries().size()) { + if (tile.itemIndex >= GetCurrentFolderAssets().size()) { continue; } const AssetEntry& assetEntry = - GetBrowserModel().GetAssetEntries()[tile.itemIndex]; + GetCurrentFolderAssets()[tile.itemIndex]; if (assetEntry.itemId != itemId) { continue; } @@ -650,7 +667,7 @@ void ProjectPanel::UpdateRenameSession( EmitEvent( EventKind::FolderNavigated, EventSource::Tree, - FindFolderEntry(GetBrowserModel().GetCurrentFolderId())); + FindFolderEntry(GetCurrentFolderId())); } } @@ -674,18 +691,18 @@ void ProjectPanel::SyncCurrentFolderSelection() { } RebuildWindowTreeItems(); - const std::string& currentFolderId = GetBrowserModel().GetCurrentFolderId(); + const std::string_view currentFolderId = GetCurrentFolderId(); if (currentFolderId.empty()) { m_folderSelection.ClearSelection(); return; } const std::vector ancestorFolderIds = - GetBrowserModel().CollectCurrentFolderAncestorIds(); + BuildCurrentFolderAncestorIds(); for (const std::string& ancestorFolderId : ancestorFolderIds) { m_folderExpansion.Expand(ancestorFolderId); } - m_folderSelection.SetSelection(currentFolderId); + m_folderSelection.SetSelection(std::string(currentFolderId)); } void ProjectPanel::SyncSelectionsFromRuntime() { @@ -760,7 +777,7 @@ float ProjectPanel::MeasureBrowserContentHeight(const UIRect& browserContentRect return 0.0f; } - const std::size_t assetCount = GetBrowserModel().GetAssetEntries().size(); + const std::size_t assetCount = GetCurrentFolderAssets().size(); const int columnCount = ResolveAssetGridColumnCount(gridWidth); const std::size_t rowCount = assetCount == 0u @@ -825,10 +842,10 @@ bool ProjectPanel::NavigateToFolder(std::string_view itemId, EventSource source) SyncSelectionsFromRuntime(); m_hoveredAssetItemId.clear(); m_lastPrimaryClickedAssetId.clear(); - EmitEvent( - EventKind::FolderNavigated, - source, - FindFolderEntry(GetBrowserModel().GetCurrentFolderId())); + EmitEvent( + EventKind::FolderNavigated, + source, + FindFolderEntry(GetCurrentFolderId())); return true; } @@ -857,7 +874,7 @@ bool ProjectPanel::OpenProjectItem(std::string_view itemId, EventSource source) EmitEvent( EventKind::FolderNavigated, source, - FindFolderEntry(GetBrowserModel().GetCurrentFolderId())); + FindFolderEntry(GetCurrentFolderId())); } return navigated; } @@ -1381,7 +1398,7 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand( return BuildDispatchResult(false, "Failed to create a folder in the current Project directory."); } - if (target.containerFolder->itemId != GetBrowserModel().GetCurrentFolderId()) { + if (target.containerFolder->itemId != GetCurrentFolderId()) { NavigateToFolder(target.containerFolder->itemId, EventSource::GridSecondary); if (HasValidBounds(m_layout.bounds)) { RebuildPanelLayout(m_layout.bounds); @@ -1418,7 +1435,7 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand( return BuildDispatchResult(false, "Failed to create a material in the current Project directory."); } - if (target.containerFolder->itemId != GetBrowserModel().GetCurrentFolderId()) { + if (target.containerFolder->itemId != GetCurrentFolderId()) { NavigateToFolder(target.containerFolder->itemId, EventSource::GridSecondary); if (HasValidBounds(m_layout.bounds)) { RebuildPanelLayout(m_layout.bounds); @@ -1573,7 +1590,7 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchEditCommand( } if (commandId == "edit.delete") { - const std::string previousCurrentFolderId = GetBrowserModel().GetCurrentFolderId(); + const std::string previousCurrentFolderId(GetCurrentFolderId()); const bool hadAssetSelection = m_projectRuntime->HasSelection(); if (!m_projectRuntime->DeleteItem(target->itemId)) { return BuildDispatchResult(false, "Failed to delete the selected project item."); @@ -1595,11 +1612,11 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchEditCommand( if (hadAssetSelection && !m_projectRuntime->HasSelection()) { EmitSelectionClearedEvent(EventSource::GridPrimary); } - if (previousCurrentFolderId != GetBrowserModel().GetCurrentFolderId()) { + if (previousCurrentFolderId != GetCurrentFolderId()) { EmitEvent( EventKind::FolderNavigated, EventSource::Tree, - FindFolderEntry(GetBrowserModel().GetCurrentFolderId())); + FindFolderEntry(GetCurrentFolderId())); } return BuildDispatchResult( true, @@ -1782,7 +1799,7 @@ void ProjectPanel::Update( if (m_treeFrame.result.selectionChanged && !m_treeFrame.result.selectedItemId.empty() && - m_treeFrame.result.selectedItemId != GetBrowserModel().GetCurrentFolderId()) { + m_treeFrame.result.selectedItemId != GetCurrentFolderId()) { CloseContextMenu(); NavigateToFolder(m_treeFrame.result.selectedItemId, EventSource::Tree); RebuildPanelLayout(dispatchEntry.bounds); @@ -1943,7 +1960,7 @@ void ProjectPanel::Update( m_folderExpansion, *m_projectRuntime, m_layout, - GetBrowserModel().GetAssetEntries(), + GetCurrentFolderAssets(), [this](const UIPoint& point, DropTargetSurface* surface) { return ResolveAssetDropTargetItemId(point, surface); } @@ -2051,7 +2068,7 @@ void ProjectPanel::Update( m_splitterDragging || ContainsPoint(m_layout.dividerRect, event.position); m_hoveredBreadcrumbIndex = HitTestBreadcrumbItem(event.position); const std::size_t hoveredAssetIndex = HitTestAssetTile(event.position); - const auto& assetEntries = GetBrowserModel().GetAssetEntries(); + const auto& assetEntries = GetCurrentFolderAssets(); m_hoveredAssetItemId = hoveredAssetIndex < assetEntries.size() ? assetEntries[hoveredAssetIndex].itemId @@ -2083,7 +2100,7 @@ void ProjectPanel::Update( break; } - const auto& assetEntries = GetBrowserModel().GetAssetEntries(); + const auto& assetEntries = GetCurrentFolderAssets(); const std::size_t hitIndex = HitTestAssetTile(event.position); if (hitIndex >= assetEntries.size()) { if (m_projectRuntime->HasSelection()) { @@ -2121,7 +2138,7 @@ void ProjectPanel::Update( if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Right && ContainsPoint(m_layout.gridRect, event.position)) { - const auto& assetEntries = GetBrowserModel().GetAssetEntries(); + const auto& assetEntries = GetCurrentFolderAssets(); const std::size_t hitIndex = HitTestAssetTile(event.position); if (hitIndex >= assetEntries.size()) { EmitEvent( @@ -2184,9 +2201,9 @@ ProjectPanel::Layout ProjectPanel::BuildLayout( const UIRect& browserContentRect, float browserVerticalOffset) const { Layout layout = {}; - const auto& assetEntries = GetBrowserModel().GetAssetEntries(); - const std::vector breadcrumbSegments = - GetBrowserModel().BuildBreadcrumbSegments(); + const auto& assetEntries = GetCurrentFolderAssets(); + const std::vector breadcrumbSegments = + BuildCurrentFolderBreadcrumbs(); const float dividerThickness = ResolveUIEditorDockHostMetrics().splitterMetrics.thickness; layout.bounds = UIRect( bounds.x, @@ -2359,7 +2376,7 @@ std::string ProjectPanel::ResolveAssetDropTargetItemId( } } - const auto& assetEntries = GetBrowserModel().GetAssetEntries(); + const auto& assetEntries = GetCurrentFolderAssets(); const std::size_t assetIndex = HitTestAssetTile(point); if (assetIndex < assetEntries.size() && assetEntries[assetIndex].directory) { @@ -2382,7 +2399,7 @@ void ProjectPanel::Append(UIDrawList& drawList) const { } const BrowserModel& browserModel = GetBrowserModel(); - const auto& assetEntries = browserModel.GetAssetEntries(); + const auto& assetEntries = GetCurrentFolderAssets(); const std::filesystem::path projectRootPath = browserModel.GetProjectRootPath(); const std::vector& presentedItems = GetPresentedWindowTreeItems(); diff --git a/editor/src/Product/Features/Workspace/Project/ProjectPanel.h b/editor/src/Product/Features/Workspace/Project/ProjectPanel.h index 93bffbd1..15b7042f 100644 --- a/editor/src/Product/Features/Workspace/Project/ProjectPanel.h +++ b/editor/src/Product/Features/Workspace/Project/ProjectPanel.h @@ -115,6 +115,7 @@ private: using BrowserModel = ::XCEngine::UI::Editor::App::ProjectBrowserModel; using FolderEntry = BrowserModel::FolderEntry; using AssetEntry = BrowserModel::AssetEntry; + using BreadcrumbSegment = BrowserModel::BreadcrumbSegment; using EditCommandTarget = EditorProjectRuntime::EditCommandTarget; using AssetCommandTarget = EditorProjectRuntime::AssetCommandTarget; enum class RenameSurface : std::uint8_t { @@ -170,8 +171,11 @@ private: }; bool HasProjectRuntime() const; - BrowserModel& GetBrowserModel(); const BrowserModel& GetBrowserModel() const; + std::string_view GetCurrentFolderId() const; + const std::vector& GetCurrentFolderAssets() const; + std::vector BuildCurrentFolderBreadcrumbs() const; + std::vector BuildCurrentFolderAncestorIds() const; void RebuildWindowTreeItems(); const std::vector& GetWindowTreeItems() const; const std::vector& GetPresentedWindowTreeItems() const; diff --git a/editor/src/Product/Services/Project/EditorProjectRuntime.cpp b/editor/src/Product/Services/Project/EditorProjectRuntime.cpp index 845675ad..d71b53a2 100644 --- a/editor/src/Product/Services/Project/EditorProjectRuntime.cpp +++ b/editor/src/Product/Services/Project/EditorProjectRuntime.cpp @@ -80,24 +80,46 @@ EditCommandTarget BuildEditCommandTarget( } // namespace +EditorProjectRuntime::~EditorProjectRuntime() { + UnbindStore(); +} + void EditorProjectRuntime::Reset() { + UnbindStore(); + m_store = nullptr; m_browserModel.Reset(); + m_folderViewState = {}; m_ownedSelectionService.ClearSelection(); m_selectionService = &m_ownedSelectionService; } bool EditorProjectRuntime::Initialize(const std::filesystem::path& projectRoot) { + Product::EditorStore* const boundStore = m_store; + EditorSelectionService* const boundSelectionService = + m_selectionService != &m_ownedSelectionService + ? m_selectionService + : nullptr; Reset(); + if (boundStore != nullptr) { + BindStore(boundStore); + } + if (boundSelectionService != nullptr) { + BindSelectionService(boundSelectionService); + } m_browserModel.Initialize(projectRoot); - ProjectAuthoritativeCurrentFolderToBrowser(); - CommitProjectedCurrentFolderToStore(); + RebuildFolderViewState(); return true; } void EditorProjectRuntime::BindStore(Product::EditorStore* store) { + UnbindStore(); m_store = store; - ProjectAuthoritativeCurrentFolderToBrowser(); - CommitProjectedCurrentFolderToStore(); + if (m_store != nullptr) { + m_storeSubscriptionToken = m_store->Subscribe([this](const Product::EditorState&) { + HandleStoreStateChanged(); + }); + } + RebuildFolderViewState(); } void EditorProjectRuntime::BindSelectionService( @@ -107,19 +129,30 @@ void EditorProjectRuntime::BindSelectionService( } void EditorProjectRuntime::Refresh() { - m_browserModel.Refresh(AuthoritativeCurrentFolderId()); - CommitProjectedCurrentFolderToStore(); - ProjectAuthoritativeCurrentFolderToBrowser(); + RebuildFolderViewState(); RevalidateSelection(); } const ProjectBrowserModel& EditorProjectRuntime::GetBrowserModel() const { - return const_cast(this)->GetBrowserModel(); + return m_browserModel; } -ProjectBrowserModel& EditorProjectRuntime::GetBrowserModel() { - ProjectAuthoritativeCurrentFolderToBrowser(); - return m_browserModel; +std::string_view EditorProjectRuntime::GetCurrentFolderId() const { + return m_folderViewState.currentFolderId; +} + +const std::vector& +EditorProjectRuntime::GetCurrentFolderAssets() const { + return m_folderViewState.assetEntries; +} + +std::vector +EditorProjectRuntime::BuildCurrentFolderBreadcrumbs() const { + return m_folderViewState.breadcrumbs; +} + +std::vector EditorProjectRuntime::BuildCurrentFolderAncestorIds() const { + return m_folderViewState.ancestorFolderIds; } bool EditorProjectRuntime::HasSelection() const { @@ -164,7 +197,7 @@ bool EditorProjectRuntime::NavigateToFolder(std::string_view itemId) { } SetAuthoritativeCurrentFolderId(std::string(itemId)); - ProjectAuthoritativeCurrentFolderToBrowser(); + RebuildFolderViewState(); ClearSelection(); return true; } @@ -315,8 +348,7 @@ bool EditorProjectRuntime::CreateFolder( std::string* createdFolderId) { const bool created = m_browserModel.CreateFolder(parentFolderId, requestedName, createdFolderId); - CommitProjectedCurrentFolderToStore(); - ProjectAuthoritativeCurrentFolderToBrowser(); + RebuildFolderViewState(); RevalidateSelection(); return created; } @@ -327,8 +359,7 @@ bool EditorProjectRuntime::CreateMaterial( std::string* createdItemId) { const bool created = m_browserModel.CreateMaterial(parentFolderId, requestedName, createdItemId); - CommitProjectedCurrentFolderToStore(); - ProjectAuthoritativeCurrentFolderToBrowser(); + RebuildFolderViewState(); RevalidateSelection(); return created; } @@ -340,12 +371,13 @@ bool EditorProjectRuntime::RenameItem( std::string resolvedRenamedItemId = {}; std::string* renameOutput = renamedItemId != nullptr ? renamedItemId : &resolvedRenamedItemId; - if (!m_browserModel.RenameItem(itemId, newName, renameOutput)) { + std::string currentFolderId = std::string(AuthoritativeCurrentFolderId()); + if (!m_browserModel.RenameItem(itemId, newName, renameOutput, ¤tFolderId)) { return false; } - CommitProjectedCurrentFolderToStore(); - ProjectAuthoritativeCurrentFolderToBrowser(); + SetAuthoritativeCurrentFolderId(std::move(currentFolderId)); + RebuildFolderViewState(); if (SelectionTargetsItem(itemId)) { if (!renameOutput->empty() && !SetSelection(*renameOutput)) { ClearSelection(); @@ -358,12 +390,13 @@ bool EditorProjectRuntime::RenameItem( bool EditorProjectRuntime::DeleteItem(std::string_view itemId) { const bool selectionAffected = SelectionTargetsItem(itemId); - if (!m_browserModel.DeleteItem(itemId)) { + std::string currentFolderId = std::string(AuthoritativeCurrentFolderId()); + if (!m_browserModel.DeleteItem(itemId, ¤tFolderId)) { return false; } - CommitProjectedCurrentFolderToStore(); - ProjectAuthoritativeCurrentFolderToBrowser(); + SetAuthoritativeCurrentFolderId(std::move(currentFolderId)); + RebuildFolderViewState(); if (selectionAffected) { ClearSelection(); } else { @@ -383,12 +416,17 @@ bool EditorProjectRuntime::MoveItemToFolder( std::string_view targetFolderId, std::string* movedItemId) { const bool selectionAffected = SelectionTargetsItem(itemId); - if (!m_browserModel.MoveItemToFolder(itemId, targetFolderId, movedItemId)) { + std::string currentFolderId = std::string(AuthoritativeCurrentFolderId()); + if (!m_browserModel.MoveItemToFolder( + itemId, + targetFolderId, + movedItemId, + ¤tFolderId)) { return false; } - CommitProjectedCurrentFolderToStore(); - ProjectAuthoritativeCurrentFolderToBrowser(); + SetAuthoritativeCurrentFolderId(std::move(currentFolderId)); + RebuildFolderViewState(); if (selectionAffected) { ClearSelection(); } else { @@ -413,12 +451,17 @@ bool EditorProjectRuntime::ReparentFolder( std::string_view targetParentId, std::string* movedFolderId) { const bool selectionAffected = SelectionTargetsItem(sourceFolderId); - if (!m_browserModel.ReparentFolder(sourceFolderId, targetParentId, movedFolderId)) { + std::string currentFolderId = std::string(AuthoritativeCurrentFolderId()); + if (!m_browserModel.ReparentFolder( + sourceFolderId, + targetParentId, + movedFolderId, + ¤tFolderId)) { return false; } - CommitProjectedCurrentFolderToStore(); - ProjectAuthoritativeCurrentFolderToBrowser(); + SetAuthoritativeCurrentFolderId(std::move(currentFolderId)); + RebuildFolderViewState(); if (selectionAffected) { ClearSelection(); } else { @@ -431,12 +474,16 @@ bool EditorProjectRuntime::MoveFolderToRoot( std::string_view sourceFolderId, std::string* movedFolderId) { const bool selectionAffected = SelectionTargetsItem(sourceFolderId); - if (!m_browserModel.MoveFolderToRoot(sourceFolderId, movedFolderId)) { + std::string currentFolderId = std::string(AuthoritativeCurrentFolderId()); + if (!m_browserModel.MoveFolderToRoot( + sourceFolderId, + movedFolderId, + ¤tFolderId)) { return false; } - CommitProjectedCurrentFolderToStore(); - ProjectAuthoritativeCurrentFolderToBrowser(); + SetAuthoritativeCurrentFolderId(std::move(currentFolderId)); + RebuildFolderViewState(); if (selectionAffected) { ClearSelection(); } else { @@ -453,6 +500,26 @@ const EditorSelectionService& EditorProjectRuntime::SelectionService() const { return *m_selectionService; } +void EditorProjectRuntime::UnbindStore() { + if (m_store != nullptr && m_storeSubscriptionToken != 0u) { + m_store->Unsubscribe(m_storeSubscriptionToken); + } + m_storeSubscriptionToken = 0u; +} + +void EditorProjectRuntime::HandleStoreStateChanged() { + if (m_store == nullptr || !BrowserModelInitialized()) { + return; + } + + if (m_store->GetState().session.currentProjectFolderId == m_folderViewState.currentFolderId) { + return; + } + + RebuildFolderViewState(); + ClearSelection(); +} + bool EditorProjectRuntime::BrowserModelInitialized() const { return !m_browserModel.GetAssetsRootPath().empty(); } @@ -462,7 +529,7 @@ std::string_view EditorProjectRuntime::AuthoritativeCurrentFolderId() const { return m_store->GetState().session.currentProjectFolderId; } - return m_browserModel.GetCurrentFolderId(); + return m_folderViewState.currentFolderId; } void EditorProjectRuntime::SetAuthoritativeCurrentFolderId(std::string folderId) { @@ -471,7 +538,7 @@ void EditorProjectRuntime::SetAuthoritativeCurrentFolderId(std::string folderId) return; } - m_browserModel.SetCurrentFolderProjection(folderId); + m_folderViewState.currentFolderId = m_browserModel.CanonicalizeFolderId(folderId); } void EditorProjectRuntime::ApplySelection( @@ -503,26 +570,16 @@ bool EditorProjectRuntime::SelectionTargetsItem(std::string_view itemId) const { IsSameOrDescendantItemId(GetSelection().itemId, itemId); } -void EditorProjectRuntime::ProjectAuthoritativeCurrentFolderToBrowser() { +void EditorProjectRuntime::RebuildFolderViewState() { if (!BrowserModelInitialized()) { return; } - const std::string_view currentFolderId = AuthoritativeCurrentFolderId(); - if (!currentFolderId.empty()) { - m_browserModel.SetCurrentFolderProjection(currentFolderId); - } -} - -void EditorProjectRuntime::CommitProjectedCurrentFolderToStore() { - if (m_store == nullptr || !BrowserModelInitialized()) { - return; - } - - const std::string& projectedCurrentFolderId = m_browserModel.GetCurrentFolderId(); - if (!projectedCurrentFolderId.empty() && - projectedCurrentFolderId != m_store->GetState().session.currentProjectFolderId) { - m_store->Dispatch(Product::EditorCommand::SetCurrentProjectFolder(projectedCurrentFolderId)); + m_folderViewState = m_browserModel.Refresh(AuthoritativeCurrentFolderId()); + if (m_store != nullptr && + m_folderViewState.currentFolderId != m_store->GetState().session.currentProjectFolderId) { + m_store->Dispatch(Product::EditorCommand::SetCurrentProjectFolder( + m_folderViewState.currentFolderId)); } } diff --git a/editor/src/Product/Services/Project/EditorProjectRuntime.h b/editor/src/Product/Services/Project/EditorProjectRuntime.h index d006646d..b8962e94 100644 --- a/editor/src/Product/Services/Project/EditorProjectRuntime.h +++ b/editor/src/Product/Services/Project/EditorProjectRuntime.h @@ -7,6 +7,7 @@ #include +#include #include #include #include @@ -23,6 +24,7 @@ namespace XCEngine::UI::Editor::App { class EditorProjectRuntime { public: EditorProjectRuntime() = default; + ~EditorProjectRuntime(); EditorProjectRuntime(const EditorProjectRuntime&) = delete; EditorProjectRuntime& operator=(const EditorProjectRuntime&) = delete; EditorProjectRuntime(EditorProjectRuntime&&) = delete; @@ -57,7 +59,10 @@ public: void Refresh(); const ProjectBrowserModel& GetBrowserModel() const; - ProjectBrowserModel& GetBrowserModel(); + std::string_view GetCurrentFolderId() const; + const std::vector& GetCurrentFolderAssets() const; + std::vector BuildCurrentFolderBreadcrumbs() const; + std::vector BuildCurrentFolderAncestorIds() const; bool HasSelection() const; const EditorSelectionState& GetSelection() const; @@ -114,17 +119,20 @@ public: private: EditorSelectionService& SelectionService(); const EditorSelectionService& SelectionService() const; + void UnbindStore(); + void HandleStoreStateChanged(); bool BrowserModelInitialized() const; std::string_view AuthoritativeCurrentFolderId() const; void SetAuthoritativeCurrentFolderId(std::string folderId); - void ProjectAuthoritativeCurrentFolderToBrowser(); - void CommitProjectedCurrentFolderToStore(); + void RebuildFolderViewState(); void ApplySelection(const ProjectBrowserModel::AssetEntry& asset); void RevalidateSelection(); bool SelectionTargetsItem(std::string_view itemId) const; ProjectBrowserModel m_browserModel = {}; + ProjectBrowserModel::FolderViewState m_folderViewState = {}; Product::EditorStore* m_store = nullptr; + std::uint64_t m_storeSubscriptionToken = 0u; EditorSelectionService m_ownedSelectionService = {}; EditorSelectionService* m_selectionService = &m_ownedSelectionService; }; diff --git a/editor/src/Product/Services/Project/ProjectBrowserModel.cpp b/editor/src/Product/Services/Project/ProjectBrowserModel.cpp index 11c71aa0..910bc6a2 100644 --- a/editor/src/Product/Services/Project/ProjectBrowserModel.cpp +++ b/editor/src/Product/Services/Project/ProjectBrowserModel.cpp @@ -542,7 +542,6 @@ void ProjectBrowserModel::Reset() { m_assetsRootPath.clear(); m_folderEntries.clear(); m_assetEntries.clear(); - m_currentFolderId.clear(); } void ProjectBrowserModel::Initialize(const std::filesystem::path& projectRoot) { @@ -564,36 +563,49 @@ void ProjectBrowserModel::Initialize(const std::filesystem::path& projectRoot) { std::to_string(m_assetEntries.size())); } -void ProjectBrowserModel::Refresh(std::string_view preferredCurrentFolderId) { +ProjectBrowserModel::FolderViewState ProjectBrowserModel::Refresh( + std::string_view preferredCurrentFolderId) { TraceProjectBrowser("ProjectBrowserModel::Refresh begin"); - RefreshBrowserState(preferredCurrentFolderId); + RefreshFolderTree(); + FolderViewState viewState = BuildFolderViewState(preferredCurrentFolderId); + m_assetEntries = viewState.assetEntries; TraceProjectBrowser( "ProjectBrowserModel::Refresh end currentFolder=" + - m_currentFolderId + + viewState.currentFolderId + " folders=" + std::to_string(m_folderEntries.size()) + " assets=" + std::to_string(m_assetEntries.size())); + return viewState; } -bool ProjectBrowserModel::SetCurrentFolderProjection(std::string_view itemId) { - if (itemId == m_currentFolderId) { - return false; - } - - m_currentFolderId = std::string(itemId); - EnsureValidCurrentFolder(); - RefreshAssetList(); - return true; +ProjectBrowserModel::FolderViewState ProjectBrowserModel::BuildFolderViewState( + std::string_view preferredCurrentFolderId) const { + FolderViewState viewState = {}; + viewState.currentFolderId = CanonicalizeFolderId(preferredCurrentFolderId); + viewState.assetEntries = BuildAssetEntriesForFolder(viewState.currentFolderId); + viewState.breadcrumbs = BuildBreadcrumbSegments(viewState.currentFolderId); + viewState.ancestorFolderIds = BuildAncestorFolderIds(viewState.currentFolderId); + return viewState; } -void ProjectBrowserModel::RefreshBrowserState(std::string_view preferredCurrentFolderId) { - RefreshFolderTree(); - if (!preferredCurrentFolderId.empty()) { - m_currentFolderId = std::string(preferredCurrentFolderId); +std::string ProjectBrowserModel::CanonicalizeFolderId( + std::string_view preferredCurrentFolderId) const { + std::string currentFolderId = + preferredCurrentFolderId.empty() + ? std::string(kAssetsRootId) + : std::string(preferredCurrentFolderId); + if (currentFolderId.empty()) { + currentFolderId = std::string(kAssetsRootId); } - EnsureValidCurrentFolder(); - RefreshAssetList(); + + if (FindFolderEntry(currentFolderId) == nullptr) { + currentFolderId = m_folderEntries.empty() + ? std::string() + : m_folderEntries.front().itemId; + } + + return currentFolderId; } bool ProjectBrowserModel::Empty() const { @@ -618,10 +630,6 @@ const std::vector& ProjectBrowserModel::GetAsse return m_assetEntries; } -const std::string& ProjectBrowserModel::GetCurrentFolderId() const { - return m_currentFolderId; -} - bool ProjectBrowserModel::IsAssetsRoot(std::string_view itemId) const { return itemId == kAssetsRootId; } @@ -699,7 +707,8 @@ bool ProjectBrowserModel::CreateFolder( return false; } - RefreshBrowserState(); + RefreshFolderTree(); + m_assetEntries.clear(); if (createdFolderId != nullptr) { *createdFolderId = BuildRelativeItemId(newFolderPath, m_assetsRootPath); } @@ -764,7 +773,8 @@ bool ProjectBrowserModel::CreateMaterial( return false; } - RefreshBrowserState(); + RefreshFolderTree(); + m_assetEntries.clear(); if (createdItemId != nullptr) { *createdItemId = BuildRelativeItemId(materialPath, m_assetsRootPath); } @@ -777,7 +787,8 @@ bool ProjectBrowserModel::CreateMaterial( bool ProjectBrowserModel::RenameItem( std::string_view itemId, std::string_view newName, - std::string* renamedItemId) { + std::string* renamedItemId, + std::string* currentFolderId) { if (renamedItemId != nullptr) { renamedItemId->clear(); } @@ -816,13 +827,18 @@ bool ProjectBrowserModel::RenameItem( } MoveMetaSidecarIfPresent(sourcePath.value(), destinationPath); - if (std::filesystem::is_directory(destinationPath)) { - m_currentFolderId = RemapMovedItemId( - m_currentFolderId, + if (std::filesystem::is_directory(destinationPath) && + currentFolderId != nullptr) { + *currentFolderId = RemapMovedItemId( + CanonicalizeFolderId(*currentFolderId), itemId, destinationItemId); } - RefreshBrowserState(); + RefreshFolderTree(); + m_assetEntries.clear(); + if (currentFolderId != nullptr) { + *currentFolderId = CanonicalizeFolderId(*currentFolderId); + } if (renamedItemId != nullptr) { *renamedItemId = destinationItemId; } @@ -832,7 +848,9 @@ bool ProjectBrowserModel::RenameItem( } } -bool ProjectBrowserModel::DeleteItem(std::string_view itemId) { +bool ProjectBrowserModel::DeleteItem( + std::string_view itemId, + std::string* currentFolderId) { if (itemId.empty() || IsAssetsRoot(itemId)) { return false; } @@ -848,15 +866,20 @@ bool ProjectBrowserModel::DeleteItem(std::string_view itemId) { return false; } - if (std::filesystem::is_directory(sourcePath.value()) && - IsSameOrDescendantFolderId(m_currentFolderId, itemId)) { - m_currentFolderId = + if (currentFolderId != nullptr && + std::filesystem::is_directory(sourcePath.value()) && + IsSameOrDescendantFolderId(CanonicalizeFolderId(*currentFolderId), itemId)) { + *currentFolderId = BuildRelativeItemId(sourcePath->parent_path(), m_assetsRootPath); } std::filesystem::remove_all(sourcePath.value()); RemoveMetaSidecarIfPresent(sourcePath.value()); - RefreshBrowserState(); + RefreshFolderTree(); + m_assetEntries.clear(); + if (currentFolderId != nullptr) { + *currentFolderId = CanonicalizeFolderId(*currentFolderId); + } return true; } catch (...) { return false; @@ -917,7 +940,8 @@ bool ProjectBrowserModel::CanMoveItemToFolder( bool ProjectBrowserModel::MoveItemToFolder( std::string_view itemId, std::string_view targetFolderId, - std::string* movedItemId) { + std::string* movedItemId, + std::string* currentFolderId) { if (movedItemId != nullptr) { movedItemId->clear(); } @@ -942,13 +966,18 @@ bool ProjectBrowserModel::MoveItemToFolder( return false; } - if (std::filesystem::is_directory(destinationPath)) { - m_currentFolderId = RemapMovedItemId( - m_currentFolderId, + if (std::filesystem::is_directory(destinationPath) && + currentFolderId != nullptr) { + *currentFolderId = RemapMovedItemId( + CanonicalizeFolderId(*currentFolderId), itemId, destinationItemId); } - RefreshBrowserState(); + RefreshFolderTree(); + m_assetEntries.clear(); + if (currentFolderId != nullptr) { + *currentFolderId = CanonicalizeFolderId(*currentFolderId); + } if (movedItemId != nullptr) { *movedItemId = destinationItemId; } @@ -1026,7 +1055,8 @@ bool ProjectBrowserModel::CanReparentFolder( bool ProjectBrowserModel::ReparentFolder( std::string_view sourceFolderId, std::string_view targetParentId, - std::string* movedFolderId) { + std::string* movedFolderId, + std::string* currentFolderId) { if (!CanReparentFolder(sourceFolderId, targetParentId)) { return false; } @@ -1046,11 +1076,17 @@ bool ProjectBrowserModel::ReparentFolder( return false; } - m_currentFolderId = RemapMovedFolderId( - m_currentFolderId, - sourceFolderId, - destinationFolderId); - RefreshBrowserState(); + if (currentFolderId != nullptr) { + *currentFolderId = RemapMovedFolderId( + CanonicalizeFolderId(*currentFolderId), + sourceFolderId, + destinationFolderId); + } + RefreshFolderTree(); + m_assetEntries.clear(); + if (currentFolderId != nullptr) { + *currentFolderId = CanonicalizeFolderId(*currentFolderId); + } if (movedFolderId != nullptr) { *movedFolderId = destinationFolderId; @@ -1060,7 +1096,8 @@ bool ProjectBrowserModel::ReparentFolder( bool ProjectBrowserModel::MoveFolderToRoot( std::string_view sourceFolderId, - std::string* movedFolderId) { + std::string* movedFolderId, + std::string* currentFolderId) { if (sourceFolderId.empty() || sourceFolderId == kAssetsRootId) { return false; } @@ -1108,11 +1145,17 @@ bool ProjectBrowserModel::MoveFolderToRoot( return false; } - m_currentFolderId = RemapMovedFolderId( - m_currentFolderId, - sourceFolderId, - destinationFolderId); - RefreshBrowserState(); + if (currentFolderId != nullptr) { + *currentFolderId = RemapMovedFolderId( + CanonicalizeFolderId(*currentFolderId), + sourceFolderId, + destinationFolderId); + } + RefreshFolderTree(); + m_assetEntries.clear(); + if (currentFolderId != nullptr) { + *currentFolderId = CanonicalizeFolderId(*currentFolderId); + } if (movedFolderId != nullptr) { *movedFolderId = destinationFolderId; @@ -1120,34 +1163,25 @@ bool ProjectBrowserModel::MoveFolderToRoot( 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 ProjectBrowserModel::BuildBreadcrumbSegments( + std::string_view currentFolderId) const { std::vector segments = {}; - if (m_currentFolderId.empty()) { + const std::string resolvedCurrentFolderId = CanonicalizeFolderId(currentFolderId); + if (resolvedCurrentFolderId.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); + while (segmentStart < resolvedCurrentFolderId.size()) { + const std::size_t separator = resolvedCurrentFolderId.find('/', segmentStart); const std::size_t segmentLength = separator == std::string_view::npos - ? m_currentFolderId.size() - segmentStart + ? resolvedCurrentFolderId.size() - segmentStart : separator - segmentStart; if (segmentLength > 0u) { - std::string label = std::string(m_currentFolderId.substr(segmentStart, segmentLength)); + std::string label = std::string(resolvedCurrentFolderId.substr(segmentStart, segmentLength)); if (cumulativeFolderId.empty()) { cumulativeFolderId = label; } else { @@ -1176,31 +1210,19 @@ std::vector ProjectBrowserModel::BuildBr return segments; } -void ProjectBrowserModel::EnsureValidCurrentFolder() { - if (m_currentFolderId.empty()) { - m_currentFolderId = std::string(kAssetsRootId); - } - - if (FindFolderEntry(m_currentFolderId) == nullptr) { - m_currentFolderId = m_folderEntries.empty() - ? std::string() - : m_folderEntries.front().itemId; - } -} - } // namespace XCEngine::UI::Editor::App namespace XCEngine::UI::Editor::App { -void ProjectBrowserModel::RefreshAssetList() { +std::vector ProjectBrowserModel::BuildAssetEntriesForFolder( + std::string_view currentFolderId) const { TraceProjectBrowser("ProjectBrowserModel::RefreshAssetList begin"); - EnsureValidCurrentFolder(); - - m_assetEntries.clear(); - const FolderEntry* currentFolder = FindFolderEntry(m_currentFolderId); + const std::string resolvedCurrentFolderId = CanonicalizeFolderId(currentFolderId); + std::vector assetEntries = {}; + const FolderEntry* currentFolder = FindFolderEntry(resolvedCurrentFolderId); if (currentFolder == nullptr) { - return; + return assetEntries; } std::vector entries = {}; @@ -1246,14 +1268,15 @@ void ProjectBrowserModel::RefreshAssetList() { assetEntry.canOpen = CanOpenItemKind(assetEntry.kind); assetEntry.canPreview = CanPreviewItem(entry.path(), assetEntry.kind, assetEntry.directory); - m_assetEntries.push_back(std::move(assetEntry)); + assetEntries.push_back(std::move(assetEntry)); } TraceProjectBrowser( "ProjectBrowserModel::RefreshAssetList end currentFolder=" + - m_currentFolderId + + resolvedCurrentFolderId + " assets=" + - std::to_string(m_assetEntries.size())); + std::to_string(assetEntries.size())); + return assetEntries; } } // namespace XCEngine::UI::Editor::App @@ -1294,14 +1317,11 @@ void ProjectBrowserModel::RefreshFolderTree() { std::to_string(m_folderEntries.size())); } -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); + const std::string resolvedItemId = CanonicalizeFolderId(itemId); + const FolderEntry* folderEntry = FindFolderEntry(resolvedItemId); if (folderEntry == nullptr) { return ancestors; } diff --git a/editor/src/Product/Services/Project/ProjectBrowserModel.h b/editor/src/Product/Services/Project/ProjectBrowserModel.h index 7d8f142c..b905a644 100644 --- a/editor/src/Product/Services/Project/ProjectBrowserModel.h +++ b/editor/src/Product/Services/Project/ProjectBrowserModel.h @@ -53,17 +53,24 @@ public: ProjectBrowserModel(ProjectBrowserModel&&) noexcept = default; ProjectBrowserModel& operator=(ProjectBrowserModel&&) noexcept = default; + struct FolderViewState { + std::string currentFolderId = {}; + std::vector assetEntries = {}; + std::vector breadcrumbs = {}; + std::vector ancestorFolderIds = {}; + }; + void Reset(); void Initialize(const std::filesystem::path& projectRoot); - void Refresh(std::string_view preferredCurrentFolderId = {}); - bool SetCurrentFolderProjection(std::string_view itemId); + FolderViewState Refresh(std::string_view preferredCurrentFolderId = {}); + FolderViewState BuildFolderViewState(std::string_view preferredCurrentFolderId) const; + std::string CanonicalizeFolderId(std::string_view preferredCurrentFolderId) const; bool Empty() const; std::filesystem::path GetProjectRootPath() const; const std::filesystem::path& GetAssetsRootPath() const; const std::vector& GetFolderEntries() const; const std::vector& GetAssetEntries() const; - const std::string& GetCurrentFolderId() const; bool IsAssetsRoot(std::string_view itemId) const; const FolderEntry* FindFolderEntry(std::string_view itemId) const; @@ -71,7 +78,6 @@ public: std::optional ResolveItemAbsolutePath( std::string_view itemId) const; std::string BuildProjectRelativePath(std::string_view itemId) const; - bool NavigateToFolder(std::string_view itemId); bool CreateFolder( std::string_view parentFolderId, std::string_view requestedName, @@ -83,38 +89,40 @@ public: bool RenameItem( std::string_view itemId, std::string_view newName, - std::string* renamedItemId = nullptr); - bool DeleteItem(std::string_view itemId); + std::string* renamedItemId = nullptr, + std::string* currentFolderId = nullptr); + bool DeleteItem( + std::string_view itemId, + std::string* currentFolderId = nullptr); bool CanMoveItemToFolder( std::string_view itemId, std::string_view targetFolderId) const; bool MoveItemToFolder( std::string_view itemId, std::string_view targetFolderId, - std::string* movedItemId = nullptr); + std::string* movedItemId = nullptr, + std::string* currentFolderId = nullptr); std::optional GetParentFolderId(std::string_view itemId) const; bool CanReparentFolder(std::string_view sourceFolderId, std::string_view targetParentId) const; bool ReparentFolder( std::string_view sourceFolderId, std::string_view targetParentId, - std::string* movedFolderId = nullptr); + std::string* movedFolderId = nullptr, + std::string* currentFolderId = nullptr); bool MoveFolderToRoot( std::string_view sourceFolderId, - std::string* movedFolderId = nullptr); - std::vector BuildBreadcrumbSegments() const; - std::vector CollectCurrentFolderAncestorIds() const; + std::string* movedFolderId = nullptr, + std::string* currentFolderId = nullptr); std::vector BuildAncestorFolderIds(std::string_view itemId) const; private: - void RefreshBrowserState(std::string_view preferredCurrentFolderId = {}); void RefreshFolderTree(); - void RefreshAssetList(); - void EnsureValidCurrentFolder(); + std::vector BuildAssetEntriesForFolder(std::string_view currentFolderId) const; + std::vector BuildBreadcrumbSegments(std::string_view currentFolderId) const; std::filesystem::path m_assetsRootPath = {}; std::vector m_folderEntries = {}; std::vector m_assetEntries = {}; - std::string m_currentFolderId = {}; }; } // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/test_editor_project_runtime.cpp b/tests/UI/Editor/unit/test_editor_project_runtime.cpp index 4420031e..7e87e1ea 100644 --- a/tests/UI/Editor/unit/test_editor_project_runtime.cpp +++ b/tests/UI/Editor/unit/test_editor_project_runtime.cpp @@ -202,7 +202,7 @@ TEST(EditorProjectRuntimeTests, BoundStorePreservesPersistedCurrentFolderDuringI ASSERT_TRUE(runtime.Initialize(repo.Root() / "project")); EXPECT_EQ(store.GetState().session.currentProjectFolderId, "Assets/Scenes"); - EXPECT_EQ(runtime.GetBrowserModel().GetCurrentFolderId(), "Assets/Scenes"); + EXPECT_EQ(runtime.GetCurrentFolderId(), "Assets/Scenes"); } TEST(EditorProjectRuntimeTests, BoundStoreCanonicalizesInvalidPersistedCurrentFolderDuringInitialize) { @@ -219,7 +219,7 @@ TEST(EditorProjectRuntimeTests, BoundStoreCanonicalizesInvalidPersistedCurrentFo ASSERT_TRUE(runtime.Initialize(repo.Root() / "project")); EXPECT_EQ(store.GetState().session.currentProjectFolderId, "Assets"); - EXPECT_EQ(runtime.GetBrowserModel().GetCurrentFolderId(), "Assets"); + EXPECT_EQ(runtime.GetCurrentFolderId(), "Assets"); } TEST(EditorProjectRuntimeTests, BoundStoreRemainsTheAuthoritativeCurrentFolderSourceAtRuntime) { @@ -236,13 +236,13 @@ TEST(EditorProjectRuntimeTests, BoundStoreRemainsTheAuthoritativeCurrentFolderSo ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scenes")); EXPECT_EQ(store.GetState().session.currentProjectFolderId, "Assets/Scenes"); - EXPECT_EQ(runtime.GetBrowserModel().GetCurrentFolderId(), "Assets/Scenes"); + EXPECT_EQ(runtime.GetCurrentFolderId(), "Assets/Scenes"); const Product::EditorStore::DispatchResult dispatchResult = store.Dispatch(Product::EditorCommand::SetCurrentProjectFolder("Assets/Scripts")); ASSERT_TRUE(dispatchResult.handled); ASSERT_TRUE(dispatchResult.stateChanged); - EXPECT_EQ(runtime.GetBrowserModel().GetCurrentFolderId(), "Assets/Scripts"); + EXPECT_EQ(runtime.GetCurrentFolderId(), "Assets/Scripts"); } } // namespace diff --git a/tests/UI/Editor/unit/test_project_browser_model.cpp b/tests/UI/Editor/unit/test_project_browser_model.cpp index e99cee0a..396d4fc1 100644 --- a/tests/UI/Editor/unit/test_project_browser_model.cpp +++ b/tests/UI/Editor/unit/test_project_browser_model.cpp @@ -58,7 +58,7 @@ private: std::filesystem::path m_root = {}; }; -TEST(ProjectBrowserModelTests, ReparentFolderMovesFolderMetaAndRemapsCurrentFolder) { +TEST(ProjectBrowserModelTests, ReparentFolderMovesFolderMetaAndRemapsExplicitCurrentFolder) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/A/Child")); ASSERT_TRUE(repo.CreateDirectory("project/Assets/B")); @@ -69,20 +69,19 @@ TEST(ProjectBrowserModelTests, ReparentFolderMovesFolderMetaAndRemapsCurrentFold ProjectBrowserModel model = {}; model.Initialize(repo.Root() / "project"); - ASSERT_TRUE(model.NavigateToFolder("Assets/A/Child")); - + std::string currentFolderId = "Assets/A/Child"; std::string movedFolderId = {}; - ASSERT_TRUE(model.ReparentFolder("Assets/A", "Assets/B", &movedFolderId)); + ASSERT_TRUE(model.ReparentFolder("Assets/A", "Assets/B", &movedFolderId, ¤tFolderId)); EXPECT_EQ(movedFolderId, "Assets/B/A"); - EXPECT_EQ(model.GetCurrentFolderId(), "Assets/B/A/Child"); + EXPECT_EQ(currentFolderId, "Assets/B/A/Child"); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/B/A")); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/B/A.meta")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/A")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/A.meta")); } -TEST(ProjectBrowserModelTests, MoveFolderToRootMovesFolderMetaAndRemapsCurrentFolder) { +TEST(ProjectBrowserModelTests, MoveFolderToRootMovesFolderMetaAndRemapsExplicitCurrentFolder) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/Parent/Nested")); ASSERT_TRUE(repo.WriteFile("project/Assets/Parent.meta")); @@ -91,13 +90,12 @@ TEST(ProjectBrowserModelTests, MoveFolderToRootMovesFolderMetaAndRemapsCurrentFo ProjectBrowserModel model = {}; model.Initialize(repo.Root() / "project"); - ASSERT_TRUE(model.NavigateToFolder("Assets/Parent/Nested")); - + std::string currentFolderId = "Assets/Parent/Nested"; std::string movedFolderId = {}; - ASSERT_TRUE(model.MoveFolderToRoot("Assets/Parent/Nested", &movedFolderId)); + ASSERT_TRUE(model.MoveFolderToRoot("Assets/Parent/Nested", &movedFolderId, ¤tFolderId)); EXPECT_EQ(movedFolderId, "Assets/Nested"); - EXPECT_EQ(model.GetCurrentFolderId(), "Assets/Nested"); + EXPECT_EQ(currentFolderId, "Assets/Nested"); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Nested")); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Nested.meta")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent/Nested")); @@ -136,10 +134,12 @@ TEST(ProjectBrowserModelTests, CreateMaterialCreatesUniqueMaterialFileAndExposes model.BuildProjectRelativePath(createdItemId), "Assets/Materials/New Material 1.mat"); - ASSERT_TRUE(model.NavigateToFolder("Assets/Materials")); + const ProjectBrowserModel::FolderViewState viewState = + model.Refresh("Assets/Materials"); const ProjectBrowserModel::AssetEntry* createdEntry = model.FindAssetEntry(createdItemId); ASSERT_NE(createdEntry, nullptr); + EXPECT_EQ(viewState.currentFolderId, "Assets/Materials"); EXPECT_EQ(createdEntry->kind, ProjectBrowserModel::ItemKind::Material); EXPECT_EQ(createdEntry->displayName, "New Material 1"); EXPECT_EQ(createdEntry->nameWithExtension, "New Material 1.mat"); @@ -157,7 +157,7 @@ TEST(ProjectBrowserModelTests, CanMoveItemToFolderRejectsDescendantFolderTargets EXPECT_TRUE(model.CanMoveItemToFolder("Assets/FolderA", "Assets/FolderB")); } -TEST(ProjectBrowserModelTests, MoveItemToFolderMovesFileMetaAndRefreshesCurrentListing) { +TEST(ProjectBrowserModelTests, MoveItemToFolderMovesFileMetaAndPreservesExplicitCurrentFolder) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts")); ASSERT_TRUE(repo.CreateDirectory("project/Assets/Archive")); @@ -166,44 +166,53 @@ TEST(ProjectBrowserModelTests, MoveItemToFolderMovesFileMetaAndRefreshesCurrentL ProjectBrowserModel model = {}; model.Initialize(repo.Root() / "project"); - ASSERT_TRUE(model.NavigateToFolder("Assets/Scripts")); + std::string currentFolderId = "Assets/Scripts"; std::string movedItemId = {}; ASSERT_TRUE(model.MoveItemToFolder( "Assets/Scripts/Player.cs", "Assets/Archive", - &movedItemId)); + &movedItemId, + ¤tFolderId)); EXPECT_EQ(movedItemId, "Assets/Archive/Player.cs"); - EXPECT_EQ(model.GetCurrentFolderId(), "Assets/Scripts"); + EXPECT_EQ(currentFolderId, "Assets/Scripts"); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scripts/Player.cs")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scripts/Player.cs.meta")); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Archive/Player.cs")); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Archive/Player.cs.meta")); + + model.Refresh("Assets/Scripts"); EXPECT_EQ(model.FindAssetEntry("Assets/Scripts/Player.cs"), nullptr); - ASSERT_TRUE(model.NavigateToFolder("Assets/Archive")); + model.Refresh("Assets/Archive"); EXPECT_NE(model.FindAssetEntry("Assets/Archive/Player.cs"), nullptr); } -TEST(ProjectBrowserModelTests, RenameFilePreservesExtensionAndUpdatesCurrentListing) { +TEST(ProjectBrowserModelTests, RenameFilePreservesExtensionAndUpdatesExplicitCurrentFolderListing) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc")); ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc.meta")); ProjectBrowserModel model = {}; model.Initialize(repo.Root() / "project"); - ASSERT_TRUE(model.NavigateToFolder("Assets/Scenes")); + std::string currentFolderId = "Assets/Scenes"; std::string renamedItemId = {}; - ASSERT_TRUE(model.RenameItem("Assets/Scenes/Main.xc", "Gameplay", &renamedItemId)); + ASSERT_TRUE(model.RenameItem( + "Assets/Scenes/Main.xc", + "Gameplay", + &renamedItemId, + ¤tFolderId)); EXPECT_EQ(renamedItemId, "Assets/Scenes/Gameplay.xc"); + EXPECT_EQ(currentFolderId, "Assets/Scenes"); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Gameplay.xc")); EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Gameplay.xc.meta")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Main.xc")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Main.xc.meta")); + model.Refresh(currentFolderId); const ProjectBrowserModel::AssetEntry* renamedEntry = model.FindAssetEntry("Assets/Scenes/Gameplay.xc"); ASSERT_NE(renamedEntry, nullptr); @@ -211,7 +220,7 @@ TEST(ProjectBrowserModelTests, RenameFilePreservesExtensionAndUpdatesCurrentList EXPECT_EQ(renamedEntry->nameWithExtension, "Gameplay.xc"); } -TEST(ProjectBrowserModelTests, DeleteFolderRemovesMetaAndFallsBackCurrentFolder) { +TEST(ProjectBrowserModelTests, DeleteFolderRemapsExplicitCurrentFolderToParent) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/Parent/Nested")); ASSERT_TRUE(repo.WriteFile("project/Assets/Parent.meta")); @@ -219,16 +228,16 @@ TEST(ProjectBrowserModelTests, DeleteFolderRemovesMetaAndFallsBackCurrentFolder) ProjectBrowserModel model = {}; model.Initialize(repo.Root() / "project"); - ASSERT_TRUE(model.NavigateToFolder("Assets/Parent/Nested")); - ASSERT_TRUE(model.DeleteItem("Assets/Parent")); + std::string currentFolderId = "Assets/Parent/Nested"; + ASSERT_TRUE(model.DeleteItem("Assets/Parent", ¤tFolderId)); - EXPECT_EQ(model.GetCurrentFolderId(), "Assets"); + EXPECT_EQ(currentFolderId, "Assets"); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent")); EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent.meta")); } -TEST(ProjectBrowserModelTests, AssetEntriesExposeKindAndOpenCapability) { +TEST(ProjectBrowserModelTests, AssetEntriesExposeKindAndOpenCapabilityForExplicitView) { TemporaryRepo repo = {}; ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes")); ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc")); @@ -238,11 +247,14 @@ TEST(ProjectBrowserModelTests, AssetEntriesExposeKindAndOpenCapability) { ProjectBrowserModel model = {}; model.Initialize(repo.Root() / "project"); - const ProjectBrowserModel::AssetEntry* sceneEntry = + const ProjectBrowserModel::FolderViewState rootView = model.Refresh("Assets"); + ASSERT_EQ(rootView.currentFolderId, "Assets"); + + const ProjectBrowserModel::AssetEntry* modelEntry = model.FindAssetEntry("Assets/mesh.fbx"); - ASSERT_NE(sceneEntry, nullptr); - EXPECT_EQ(sceneEntry->kind, ProjectBrowserModel::ItemKind::Model); - EXPECT_FALSE(sceneEntry->canOpen); + ASSERT_NE(modelEntry, nullptr); + EXPECT_EQ(modelEntry->kind, ProjectBrowserModel::ItemKind::Model); + EXPECT_FALSE(modelEntry->canOpen); const ProjectBrowserModel::AssetEntry* fileEntry = model.FindAssetEntry("Assets/readme.txt"); @@ -250,7 +262,9 @@ TEST(ProjectBrowserModelTests, AssetEntriesExposeKindAndOpenCapability) { EXPECT_EQ(fileEntry->kind, ProjectBrowserModel::ItemKind::File); EXPECT_FALSE(fileEntry->canOpen); - ASSERT_TRUE(model.NavigateToFolder("Assets/Scenes")); + const ProjectBrowserModel::FolderViewState sceneView = + model.Refresh("Assets/Scenes"); + ASSERT_EQ(sceneView.currentFolderId, "Assets/Scenes"); const ProjectBrowserModel::AssetEntry* openedSceneEntry = model.FindAssetEntry("Assets/Scenes/Main.xc"); ASSERT_NE(openedSceneEntry, nullptr);