From 0ff02150c03e58f89f68a37f4434011dd9077ea9 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 11 Apr 2026 22:31:14 +0800 Subject: [PATCH] Refine editor tree alignment and project panel events --- new_editor/app/Application.cpp | 64 ++++++++ new_editor/app/Panels/ProductProjectPanel.cpp | 141 ++++++++++++++---- new_editor/app/Panels/ProductProjectPanel.h | 36 ++++- new_editor/app/Panels/ProductTreeViewStyle.h | 1 + .../XCEditor/Collections/UIEditorTreeView.h | 1 + .../src/Collections/UIEditorTreeView.cpp | 11 +- 6 files changed, 219 insertions(+), 35 deletions(-) diff --git a/new_editor/app/Application.cpp b/new_editor/app/Application.cpp index d27f2c84..c28917f0 100644 --- a/new_editor/app/Application.cpp +++ b/new_editor/app/Application.cpp @@ -328,6 +328,65 @@ std::string DescribeInputEventType(const UIInputEvent& event) { } } +std::string DescribeProjectPanelEvent(const App::ProductProjectPanel::Event& event) { + std::ostringstream stream = {}; + switch (event.kind) { + case App::ProductProjectPanel::EventKind::AssetSelected: + stream << "AssetSelected"; + break; + case App::ProductProjectPanel::EventKind::AssetSelectionCleared: + stream << "AssetSelectionCleared"; + break; + case App::ProductProjectPanel::EventKind::FolderNavigated: + stream << "FolderNavigated"; + break; + case App::ProductProjectPanel::EventKind::AssetOpened: + stream << "AssetOpened"; + break; + case App::ProductProjectPanel::EventKind::ContextMenuRequested: + stream << "ContextMenuRequested"; + break; + case App::ProductProjectPanel::EventKind::None: + default: + stream << "None"; + break; + } + + stream << " source="; + switch (event.source) { + case App::ProductProjectPanel::EventSource::Tree: + stream << "Tree"; + break; + case App::ProductProjectPanel::EventSource::Breadcrumb: + stream << "Breadcrumb"; + break; + case App::ProductProjectPanel::EventSource::GridPrimary: + stream << "GridPrimary"; + break; + case App::ProductProjectPanel::EventSource::GridDoubleClick: + stream << "GridDoubleClick"; + break; + case App::ProductProjectPanel::EventSource::GridSecondary: + stream << "GridSecondary"; + break; + case App::ProductProjectPanel::EventSource::Background: + stream << "Background"; + break; + case App::ProductProjectPanel::EventSource::None: + default: + stream << "None"; + break; + } + + if (!event.itemId.empty()) { + stream << " item=" << event.itemId; + } + if (!event.displayName.empty()) { + stream << " label=" << event.displayName; + } + return stream.str(); +} + std::vector FilterShellInputEventsForHostedContentCapture( const std::vector& inputEvents) { std::vector filteredEvents = {}; @@ -568,6 +627,11 @@ void Application::RenderFrame() { hostedContentEvents, !m_shellFrame.result.workspaceInputSuppressed, m_workspaceController.GetWorkspace().activePanelId == "project"); + for (const App::ProductProjectPanel::Event& event : m_projectPanel.GetFrameEvents()) { + LogRuntimeTrace("project", DescribeProjectPanelEvent(event)); + m_lastStatus = "Project"; + m_lastMessage = DescribeProjectPanelEvent(event); + } ApplyHostedContentCaptureRequests(); ApplyCurrentCursor(); const UIEditorShellComposeModel shellComposeModel = diff --git a/new_editor/app/Panels/ProductProjectPanel.cpp b/new_editor/app/Panels/ProductProjectPanel.cpp index 6bb40ea8..dbd37a9b 100644 --- a/new_editor/app/Panels/ProductProjectPanel.cpp +++ b/new_editor/app/Panels/ProductProjectPanel.cpp @@ -405,6 +405,10 @@ bool ProductProjectPanel::HasActivePointerCapture() const { return m_splitterDragging; } +const std::vector& ProductProjectPanel::GetFrameEvents() const { + return m_frameEvents; +} + const ProductProjectPanel::FolderEntry* ProductProjectPanel::FindFolderEntry( std::string_view itemId) const { for (const FolderEntry& entry : m_folderEntries) { @@ -416,6 +420,17 @@ const ProductProjectPanel::FolderEntry* ProductProjectPanel::FindFolderEntry( return nullptr; } +const ProductProjectPanel::AssetEntry* ProductProjectPanel::FindAssetEntry( + std::string_view itemId) const { + for (const AssetEntry& entry : m_assetEntries) { + if (entry.itemId == itemId) { + return &entry; + } + } + + return nullptr; +} + const UIEditorPanelContentHostPanelState* ProductProjectPanel::FindMountedProjectPanel( const UIEditorPanelContentHostFrame& contentHostFrame) const { for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) { @@ -656,9 +671,9 @@ void ProductProjectPanel::SyncCurrentFolderSelection() { m_folderSelection.SetSelection(m_currentFolderId); } -void ProductProjectPanel::NavigateToFolder(std::string_view itemId) { +bool ProductProjectPanel::NavigateToFolder(std::string_view itemId, EventSource source) { if (itemId.empty() || FindFolderEntry(itemId) == nullptr || itemId == m_currentFolderId) { - return; + return false; } m_currentFolderId = std::string(itemId); @@ -667,6 +682,53 @@ void ProductProjectPanel::NavigateToFolder(std::string_view itemId) { m_hoveredAssetItemId.clear(); m_lastPrimaryClickedAssetId.clear(); RefreshAssetList(); + EmitEvent(EventKind::FolderNavigated, source, FindFolderEntry(m_currentFolderId)); + return true; +} + +void ProductProjectPanel::EmitEvent( + EventKind kind, + EventSource source, + const FolderEntry* folder) { + if (kind == EventKind::None || folder == nullptr) { + return; + } + + Event event = {}; + event.kind = kind; + event.source = source; + event.itemId = folder->itemId; + event.absolutePath = folder->absolutePath; + event.displayName = PathToUtf8String(folder->absolutePath.filename()); + event.directory = true; + m_frameEvents.push_back(std::move(event)); +} + +void ProductProjectPanel::EmitEvent( + EventKind kind, + EventSource source, + const AssetEntry* asset) { + if (kind == EventKind::None) { + return; + } + + Event event = {}; + event.kind = kind; + event.source = source; + if (asset != nullptr) { + event.itemId = asset->itemId; + event.absolutePath = asset->absolutePath; + event.displayName = asset->displayName; + event.directory = asset->directory; + } + m_frameEvents.push_back(std::move(event)); +} + +void ProductProjectPanel::EmitSelectionClearedEvent(EventSource source) { + Event event = {}; + event.kind = EventKind::AssetSelectionCleared; + event.source = source; + m_frameEvents.push_back(std::move(event)); } void ProductProjectPanel::RefreshAssetList() { @@ -720,6 +782,7 @@ void ProductProjectPanel::RefreshAssetList() { void ProductProjectPanel::ResetTransientFrames() { m_treeFrame = {}; + m_frameEvents.clear(); m_layout = {}; m_hoveredAssetItemId.clear(); m_hoveredBreadcrumbIndex = kInvalidLayoutIndex; @@ -735,6 +798,7 @@ void ProductProjectPanel::Update( bool panelActive) { m_requestPointerCapture = false; m_requestPointerRelease = false; + m_frameEvents.clear(); const UIEditorPanelContentHostPanelState* panelState = FindMountedProjectPanel(contentHostFrame); @@ -779,7 +843,7 @@ void ProductProjectPanel::Update( if (m_treeFrame.result.selectionChanged && !m_treeFrame.result.selectedItemId.empty() && m_treeFrame.result.selectedItemId != m_currentFolderId) { - NavigateToFolder(m_treeFrame.result.selectedItemId); + NavigateToFolder(m_treeFrame.result.selectedItemId, EventSource::Tree); m_layout = BuildLayout(panelState->bounds); } @@ -823,39 +887,35 @@ void ProductProjectPanel::Update( break; case UIInputEventType::PointerButtonDown: - if (event.pointerButton != ::XCEngine::UI::UIPointerButton::Left) { - break; - } + if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Left) { + if (ContainsPoint(m_layout.dividerRect, event.position)) { + m_splitterDragging = true; + m_splitterHovered = true; + m_pressedBreadcrumbIndex = kInvalidLayoutIndex; + m_requestPointerCapture = true; + break; + } - if (ContainsPoint(m_layout.dividerRect, event.position)) { - m_splitterDragging = true; - m_splitterHovered = true; - m_pressedBreadcrumbIndex = kInvalidLayoutIndex; - m_requestPointerCapture = true; - break; - } + m_pressedBreadcrumbIndex = HitTestBreadcrumbItem(event.position); - m_pressedBreadcrumbIndex = HitTestBreadcrumbItem(event.position); + if (!ContainsPoint(m_layout.gridRect, event.position)) { + break; + } - if (!ContainsPoint(m_layout.gridRect, event.position)) { - break; - } - - { const std::size_t hitIndex = HitTestAssetTile(event.position); if (hitIndex >= m_assetEntries.size()) { - m_assetSelection.ClearSelection(); + if (m_assetSelection.HasSelection()) { + m_assetSelection.ClearSelection(); + EmitSelectionClearedEvent(EventSource::Background); + } break; } const AssetEntry& assetEntry = m_assetEntries[hitIndex]; const bool alreadySelected = m_assetSelection.IsSelected(assetEntry.itemId); - m_assetSelection.SetSelection(assetEntry.itemId); - - if (!assetEntry.directory) { - m_lastPrimaryClickedAssetId = assetEntry.itemId; - m_lastPrimaryClickTimeMs = GetTickCount64(); - break; + const bool selectionChanged = m_assetSelection.SetSelection(assetEntry.itemId); + if (selectionChanged) { + EmitEvent(EventKind::AssetSelected, EventSource::GridPrimary, &assetEntry); } const std::uint64_t nowMs = GetTickCount64(); @@ -873,9 +933,30 @@ void ProductProjectPanel::Update( break; } - NavigateToFolder(assetEntry.itemId); - m_layout = BuildLayout(panelState->bounds); - m_hoveredAssetItemId.clear(); + if (assetEntry.directory) { + NavigateToFolder(assetEntry.itemId, EventSource::GridDoubleClick); + m_layout = BuildLayout(panelState->bounds); + m_hoveredAssetItemId.clear(); + } else { + EmitEvent(EventKind::AssetOpened, EventSource::GridDoubleClick, &assetEntry); + } + break; + } + + if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Right && + ContainsPoint(m_layout.gridRect, event.position)) { + const std::size_t hitIndex = HitTestAssetTile(event.position); + if (hitIndex >= m_assetEntries.size()) { + EmitEvent(EventKind::ContextMenuRequested, EventSource::Background, static_cast(nullptr)); + break; + } + + const AssetEntry& assetEntry = m_assetEntries[hitIndex]; + if (!m_assetSelection.IsSelected(assetEntry.itemId)) { + m_assetSelection.SetSelection(assetEntry.itemId); + EmitEvent(EventKind::AssetSelected, EventSource::GridSecondary, &assetEntry); + } + EmitEvent(EventKind::ContextMenuRequested, EventSource::GridSecondary, &assetEntry); } break; @@ -900,7 +981,7 @@ void ProductProjectPanel::Update( const BreadcrumbItemLayout& item = m_layout.breadcrumbItems[releasedBreadcrumbIndex]; if (item.clickable) { - NavigateToFolder(item.targetFolderId); + NavigateToFolder(item.targetFolderId, EventSource::Breadcrumb); m_layout = BuildLayout(panelState->bounds); } } diff --git a/new_editor/app/Panels/ProductProjectPanel.h b/new_editor/app/Panels/ProductProjectPanel.h index 6e70a3bc..bc25963e 100644 --- a/new_editor/app/Panels/ProductProjectPanel.h +++ b/new_editor/app/Panels/ProductProjectPanel.h @@ -25,6 +25,34 @@ public: ResizeEW }; + enum class EventKind : std::uint8_t { + None = 0, + AssetSelected, + AssetSelectionCleared, + FolderNavigated, + AssetOpened, + ContextMenuRequested + }; + + enum class EventSource : std::uint8_t { + None = 0, + Tree, + Breadcrumb, + GridPrimary, + GridDoubleClick, + GridSecondary, + Background + }; + + struct Event { + EventKind kind = EventKind::None; + EventSource source = EventSource::None; + std::string itemId = {}; + std::filesystem::path absolutePath = {}; + std::string displayName = {}; + bool directory = false; + }; + void Initialize(const std::filesystem::path& repoRoot); void SetBuiltInIcons(const ProductBuiltInIcons* icons); void SetTextMeasurer(const ::XCEngine::UI::Editor::UIEditorTextMeasurer* textMeasurer); @@ -39,6 +67,7 @@ public: bool WantsHostPointerCapture() const; bool WantsHostPointerRelease() const; bool HasActivePointerCapture() const; + const std::vector& GetFrameEvents() const; private: struct FolderEntry { @@ -83,6 +112,7 @@ private: }; const FolderEntry* FindFolderEntry(std::string_view itemId) const; + const AssetEntry* FindAssetEntry(std::string_view itemId) const; const UIEditorPanelContentHostPanelState* FindMountedProjectPanel( const UIEditorPanelContentHostFrame& contentHostFrame) const; Layout BuildLayout(const ::XCEngine::UI::UIRect& bounds) const; @@ -93,7 +123,10 @@ private: void EnsureValidCurrentFolder(); void ExpandFolderAncestors(std::string_view itemId); void SyncCurrentFolderSelection(); - void NavigateToFolder(std::string_view itemId); + bool NavigateToFolder(std::string_view itemId, EventSource source = EventSource::None); + void EmitEvent(EventKind kind, EventSource source, const FolderEntry* folder); + void EmitEvent(EventKind kind, EventSource source, const AssetEntry* asset); + void EmitSelectionClearedEvent(EventSource source); void ResetTransientFrames(); std::filesystem::path m_assetsRootPath = {}; @@ -107,6 +140,7 @@ private: ::XCEngine::UI::Widgets::UISelectionModel m_assetSelection = {}; UIEditorTreeViewInteractionState m_treeInteractionState = {}; UIEditorTreeViewInteractionFrame m_treeFrame = {}; + std::vector m_frameEvents = {}; Layout m_layout = {}; std::string m_currentFolderId = {}; std::string m_hoveredAssetItemId = {}; diff --git a/new_editor/app/Panels/ProductTreeViewStyle.h b/new_editor/app/Panels/ProductTreeViewStyle.h index beb31c91..0e2a6d20 100644 --- a/new_editor/app/Panels/ProductTreeViewStyle.h +++ b/new_editor/app/Panels/ProductTreeViewStyle.h @@ -20,6 +20,7 @@ inline Widgets::UIEditorTreeViewMetrics BuildProductTreeViewMetrics() { metrics.disclosureLabelGap = 2.0f; metrics.iconExtent = 18.0f; metrics.iconLabelGap = 2.0f; + metrics.iconInsetY = -1.0f; metrics.labelInsetY = 0.0f; metrics.cornerRounding = 0.0f; metrics.borderThickness = 0.0f; diff --git a/new_editor/include/XCEditor/Collections/UIEditorTreeView.h b/new_editor/include/XCEditor/Collections/UIEditorTreeView.h index c7345497..79ebae47 100644 --- a/new_editor/include/XCEditor/Collections/UIEditorTreeView.h +++ b/new_editor/include/XCEditor/Collections/UIEditorTreeView.h @@ -43,6 +43,7 @@ struct UIEditorTreeViewMetrics { float disclosureLabelGap = 6.0f; float iconExtent = 18.0f; float iconLabelGap = 2.0f; + float iconInsetY = 0.0f; float labelInsetY = 6.0f; float cornerRounding = 6.0f; float borderThickness = 1.0f; diff --git a/new_editor/src/Collections/UIEditorTreeView.cpp b/new_editor/src/Collections/UIEditorTreeView.cpp index fd9afb2d..54307277 100644 --- a/new_editor/src/Collections/UIEditorTreeView.cpp +++ b/new_editor/src/Collections/UIEditorTreeView.cpp @@ -27,9 +27,12 @@ float ResolveTreeViewRowHeight( return item.desiredHeight > 0.0f ? item.desiredHeight : metrics.rowHeight; } -float ResolveTextTop(const ::XCEngine::UI::UIRect& rect, float fontSize) { +float ResolveTextTop( + const ::XCEngine::UI::UIRect& rect, + float fontSize, + float insetY) { const float lineHeight = fontSize * 1.6f; - return rect.y + std::floor((rect.height - lineHeight) * 0.5f); + return rect.y + std::floor((rect.height - lineHeight) * 0.5f) + insetY; } void AppendDisclosureArrow( @@ -217,7 +220,7 @@ UIEditorTreeViewLayout BuildUIEditorTreeViewLayout( const float contentStartX = disclosureRect.x + metrics.disclosureExtent + metrics.disclosureLabelGap; const ::XCEngine::UI::UIRect iconRect( hasLeadingIcon ? contentStartX : 0.0f, - rowRect.y + (rowRect.height - iconExtent) * 0.5f, + rowRect.y + (rowRect.height - iconExtent) * 0.5f + metrics.iconInsetY, hasLeadingIcon ? iconExtent : 0.0f, hasLeadingIcon ? iconExtent : 0.0f); const float labelStartX = @@ -322,7 +325,7 @@ void AppendUIEditorTreeViewForeground( drawList.AddText( ::XCEngine::UI::UIPoint( layout.labelRects[visibleOffset].x, - ResolveTextTop(layout.labelRects[visibleOffset], kTreeFontSize)), + ResolveTextTop(layout.labelRects[visibleOffset], kTreeFontSize, metrics.labelInsetY)), item.label, palette.textColor, kTreeFontSize);