diff --git a/docs/plan/Editor重构3.26.md b/docs/plan/Editor重构3.26.md index 1005983b..d2a8c024 100644 --- a/docs/plan/Editor重构3.26.md +++ b/docs/plan/Editor重构3.26.md @@ -109,6 +109,8 @@ - `Inspector` 的 component section header 菜单已开始改成 callback/router 驱动,而不是在 widget 层硬编码动作 - `MenuBar` 的 File / View / Help / global shortcut 也开始继续下沉到 shared main-menu router - `Hierarchy / Project` 的 drag-drop payload、拖拽接收与部分选择语义也开始继续下沉到 shared action router +- `Hierarchy / Project / Inspector / Console` 的空白点击、popup 请求、日志绘制与 interactive-change 边界也开始继续从 panel 下沉到 shared router/state +- `MainMenu / Edit` 的菜单绘制、shortcut 触发与动作执行路径也开始继续合并成统一 router 入口 ### 5. Dock / Layout 层 @@ -158,6 +160,7 @@ - DX12 swapchain / render target / present / resize 已抽成 `Platform/D3D12WindowRenderer.h` - scene title 拼装已抽成 `Core/EditorWindowTitle.h` - crash filter / stderr redirect / logging sink 初始化已继续从 `Application.cpp` 抽离 +- editor context 初始化、ImGui backend/session 初始化、layer attach/detach 与 frame render 壳方法也已继续从 `Application::Initialize/Shutdown/Render` 主流程中拆出 ## 主要面板状态 @@ -176,6 +179,7 @@ - `Edit` 菜单与上下文快捷键开始共享同一套 edit action router - `File / View / Help / global shortcut` 已开始继续从 panel 下沉到 shared main-menu router - scene status 与 about dialog 已开始继续从 panel 下沉到共享 UI widget/dialog +- `MenuBar` 已进一步退化为 router 驱动的薄壳,File/View/Help/Edit 的拼装继续从 panel 移出 仍待完成: @@ -192,6 +196,9 @@ - 重命名交互已从 panel 局部字段收口到 shared inline edit state - `Rename` 请求已能从 `MenuBar -> EventBus -> Hierarchy inline edit` 触发 - entity drag payload / 目标接收 / root drop / selection click 语义已开始继续从 panel 下沉到 shared hierarchy router +- hierarchy 空白点击清空选择与排序 popup 请求/菜单也已开始继续从 panel 下沉 +- hierarchy 背景 / entity context popup 与 root drop target 入口也已开始继续从 panel 下沉 +- rename commit 也已开始继续从 panel 直连 command 改成 shared hierarchy router 入口 仍待完成: @@ -208,6 +215,8 @@ - 创建文件夹弹窗已改成 shared popup state 驱动 - `Back / Open / Delete` 已接 panel-focused keyboard action - asset drag payload / folder drop / 拖拽预览高亮已开始继续从 panel 下沉到 shared project router +- item 选择 / item context 请求 / 空白区 context popup 请求也已开始继续从 panel 下沉 +- asset open 请求也已开始继续从 panel 直连 command 改成 shared project router 入口 仍待完成: @@ -225,6 +234,7 @@ - Add Component popup 已接 shared popup state - Add Component popup 菜单项组装已开始从 panel 下沉到 shared inspector action router - 组件 section header 的移除动作已开始从 widget 层硬编码迁回 inspector action router +- selection 切换时的 interactive undo finalize、Add Component 按钮与 popup 绘制也已开始继续从 panel 下沉 仍待完成: @@ -240,6 +250,7 @@ - `Clear / Filter` 已接 action 层 - console filter 状态已从 panel 裸布尔字段收成独立 state object - console toolbar action 与日志文本格式化已继续从 panel 下沉到共享层 +- log row 列表绘制与复制语义也已开始继续从 panel 下沉到 console router 仍待完成: @@ -289,6 +300,15 @@ 7. 根据条件补最小回归测试或最小验证脚本 重点验证 shortcut、undo、scene dirty/save switch、drag-drop/reparent。 +当前新增验证: + +- 已新增 `tests/editor/test_action_routing.cpp` +- 已新增 `tests/editor/CMakeLists.txt` 与 `editor_tests` target +- 已覆盖 `Hierarchy Edit route` 的 copy / paste / duplicate / delete / rename request +- 已覆盖 `Project Edit route` 的 open / back / delete +- 已覆盖 `scene dirty save + load` 后的 selection / undo reset +- 已覆盖 `reparent` 的 parent 切换、cycle 拦截与 world position / scale 保持 + ## 下一阶段建议执行顺序 ### 第一阶段 diff --git a/editor/src/Actions/ConsoleActionRouter.h b/editor/src/Actions/ConsoleActionRouter.h index af511f10..d73bfcfd 100644 --- a/editor/src/Actions/ConsoleActionRouter.h +++ b/editor/src/Actions/ConsoleActionRouter.h @@ -24,6 +24,26 @@ inline void DrawConsoleToolbarActions(Debug::EditorConsoleSink& sink, UI::Consol DrawToolbarToggleAction(MakeConsoleErrorFilterAction(filterState.ShowError()), filterState.ShowError()); } +inline void DrawConsoleLogRows(const Debug::EditorConsoleSink& sink, const UI::ConsoleFilterState& filterState) { + const auto logs = sink.GetLogs(); + size_t logIndex = 0; + for (const auto& log : logs) { + if (!filterState.Allows(log.level)) { + continue; + } + + ImGui::PushID(static_cast(logIndex)); + + const std::string fullMessage = UI::BuildConsoleLogText(log); + if (UI::DrawConsoleLogRow(fullMessage.c_str())) { + ImGui::SetClipboardText(fullMessage.c_str()); + } + + ImGui::PopID(); + ++logIndex; + } +} + } // namespace Actions } // namespace Editor } // namespace XCEngine diff --git a/editor/src/Actions/EditActionRouter.h b/editor/src/Actions/EditActionRouter.h index d3719c24..12e1908d 100644 --- a/editor/src/Actions/EditActionRouter.h +++ b/editor/src/Actions/EditActionRouter.h @@ -31,96 +31,173 @@ inline ActionBinding MakeDisabledPasteAction() { return MakeAction("Paste", "Ctrl+V", false, false); } -inline void HandleProjectEditShortcuts( - IEditorContext& context, - const EditActionTarget& target, - const ShortcutContext& shortcutContext) { - auto& projectManager = context.GetProjectManager(); - - HandleShortcut(MakeNavigateBackAction(projectManager.CanNavigateBack()), shortcutContext, [&]() { - projectManager.NavigateBack(); - }); - HandleShortcut(MakeOpenAssetAction(Commands::CanOpenAsset(target.selectedAssetItem)), shortcutContext, [&]() { - Commands::OpenAsset(context, target.selectedAssetItem); - }); - HandleShortcut(MakeDeleteAssetAction(target.selectedAssetItem != nullptr), shortcutContext, [&]() { - Commands::DeleteAsset(projectManager, projectManager.GetSelectedIndex()); - }); +inline ActionBinding MakeOpenSelectionAction(const EditActionTarget& target) { + return MakeOpenAssetAction(target.route == EditorActionRoute::Project && Commands::CanOpenAsset(target.selectedAssetItem)); } -inline void HandleHierarchyEditShortcuts( - IEditorContext& context, - const EditActionTarget& target, - const ShortcutContext& shortcutContext) { - HandleShortcut(MakeDeleteEntityAction(target.selectedGameObject), shortcutContext, [&]() { - Commands::DeleteEntity(context, target.selectedGameObject->GetID()); - }); - HandleShortcut(MakeRenameEntityAction(target.selectedGameObject), shortcutContext, [&]() { - context.GetEventBus().Publish(EntityRenameRequestedEvent{ target.selectedGameObject->GetID() }); - }); - HandleShortcut(MakeCopyEntityAction(target.selectedGameObject), shortcutContext, [&]() { - Commands::CopyEntity(context, target.selectedGameObject->GetID()); - }); - HandleShortcut(MakePasteEntityAction(context), shortcutContext, [&]() { - Commands::PasteEntity(context, target.selectedGameObject ? target.selectedGameObject->GetID() : 0); - }); - HandleShortcut(MakeDuplicateEntityAction(target.selectedGameObject), shortcutContext, [&]() { - Commands::DuplicateEntity(context, target.selectedGameObject->GetID()); - }); +inline ActionBinding MakeDeleteSelectionAction(const EditActionTarget& target) { + if (target.route == EditorActionRoute::Project) { + return MakeDeleteAssetAction(target.selectedAssetItem != nullptr); + } + + return MakeDeleteEntityAction(target.route == EditorActionRoute::Hierarchy ? target.selectedGameObject : nullptr); +} + +inline ActionBinding MakeRenameSelectionAction(const EditActionTarget& target) { + return MakeRenameEntityAction(target.route == EditorActionRoute::Hierarchy ? target.selectedGameObject : nullptr); +} + +inline ActionBinding MakeCopySelectionAction(const EditActionTarget& target) { + return MakeCopyEntityAction(target.route == EditorActionRoute::Hierarchy ? target.selectedGameObject : nullptr); +} + +inline ActionBinding MakePasteSelectionAction(IEditorContext& context, const EditActionTarget& target) { + return target.route == EditorActionRoute::Hierarchy ? MakePasteEntityAction(context) : MakeDisabledPasteAction(); +} + +inline ActionBinding MakeDuplicateSelectionAction(const EditActionTarget& target) { + return MakeDuplicateEntityAction(target.route == EditorActionRoute::Hierarchy ? target.selectedGameObject : nullptr); +} + +inline ActionBinding MakeNavigateBackSelectionAction(IEditorContext& context, const EditActionTarget& target) { + const bool enabled = + target.route == EditorActionRoute::Project && context.GetProjectManager().CanNavigateBack(); + return MakeNavigateBackAction(enabled); +} + +inline bool ExecuteOpenSelection(IEditorContext& context, const EditActionTarget& target) { + if (target.route != EditorActionRoute::Project || !Commands::CanOpenAsset(target.selectedAssetItem)) { + return false; + } + + Commands::OpenAsset(context, target.selectedAssetItem); + return true; +} + +inline bool ExecuteDeleteSelection(IEditorContext& context, const EditActionTarget& target) { + if (target.route == EditorActionRoute::Project) { + auto& projectManager = context.GetProjectManager(); + if (!target.selectedAssetItem || projectManager.GetSelectedIndex() < 0) { + return false; + } + + Commands::DeleteAsset(projectManager, projectManager.GetSelectedIndex()); + return true; + } + + if (target.route != EditorActionRoute::Hierarchy || !target.selectedGameObject) { + return false; + } + + Commands::DeleteEntity(context, target.selectedGameObject->GetID()); + return true; +} + +inline bool ExecuteNavigateBackSelection(IEditorContext& context, const EditActionTarget& target) { + if (target.route != EditorActionRoute::Project) { + return false; + } + + auto& projectManager = context.GetProjectManager(); + if (!projectManager.CanNavigateBack()) { + return false; + } + + projectManager.NavigateBack(); + return true; +} + +inline bool ExecuteRenameSelection(IEditorContext& context, const EditActionTarget& target) { + if (target.route != EditorActionRoute::Hierarchy || !target.selectedGameObject) { + return false; + } + + context.GetEventBus().Publish(EntityRenameRequestedEvent{ target.selectedGameObject->GetID() }); + return true; +} + +inline bool ExecuteCopySelection(IEditorContext& context, const EditActionTarget& target) { + if (target.route != EditorActionRoute::Hierarchy || !target.selectedGameObject) { + return false; + } + + Commands::CopyEntity(context, target.selectedGameObject->GetID()); + return true; +} + +inline bool ExecutePasteSelection(IEditorContext& context, const EditActionTarget& target) { + if (target.route != EditorActionRoute::Hierarchy || !context.GetSceneManager().HasClipboardData()) { + return false; + } + + Commands::PasteEntity(context, target.selectedGameObject ? target.selectedGameObject->GetID() : 0); + return true; +} + +inline bool ExecuteDuplicateSelection(IEditorContext& context, const EditActionTarget& target) { + if (target.route != EditorActionRoute::Hierarchy || !target.selectedGameObject) { + return false; + } + + Commands::DuplicateEntity(context, target.selectedGameObject->GetID()); + return true; } inline void HandleEditShortcuts(IEditorContext& context, const ShortcutContext& shortcutContext) { const EditActionTarget target = ResolveEditActionTarget(context); - switch (target.route) { - case EditorActionRoute::Project: - HandleProjectEditShortcuts(context, target, shortcutContext); - return; - case EditorActionRoute::Hierarchy: - HandleHierarchyEditShortcuts(context, target, shortcutContext); - return; - case EditorActionRoute::None: - return; - } + HandleShortcut(MakeNavigateBackSelectionAction(context, target), shortcutContext, [&]() { + ExecuteNavigateBackSelection(context, target); + }); + HandleShortcut(MakeOpenSelectionAction(target), shortcutContext, [&]() { + ExecuteOpenSelection(context, target); + }); + HandleShortcut(MakeDeleteSelectionAction(target), shortcutContext, [&]() { + ExecuteDeleteSelection(context, target); + }); + HandleShortcut(MakeRenameSelectionAction(target), shortcutContext, [&]() { + ExecuteRenameSelection(context, target); + }); + HandleShortcut(MakeCopySelectionAction(target), shortcutContext, [&]() { + ExecuteCopySelection(context, target); + }); + HandleShortcut(MakePasteSelectionAction(context, target), shortcutContext, [&]() { + ExecutePasteSelection(context, target); + }); + HandleShortcut(MakeDuplicateSelectionAction(target), shortcutContext, [&]() { + ExecuteDuplicateSelection(context, target); + }); } inline void DrawProjectEditActions(IEditorContext& context, const EditActionTarget& target) { - auto& projectManager = context.GetProjectManager(); - - DrawMenuAction(MakeOpenAssetAction(Commands::CanOpenAsset(target.selectedAssetItem)), [&]() { - Commands::OpenAsset(context, target.selectedAssetItem); + DrawMenuAction(MakeOpenSelectionAction(target), [&]() { + ExecuteOpenSelection(context, target); }); - DrawMenuAction(MakeDeleteAssetAction(target.selectedAssetItem != nullptr), [&]() { - Commands::DeleteAsset(projectManager, projectManager.GetSelectedIndex()); + DrawMenuAction(MakeDeleteSelectionAction(target), [&]() { + ExecuteDeleteSelection(context, target); }); DrawMenuSeparator(); - DrawMenuAction(MakeNavigateBackAction(projectManager.CanNavigateBack()), [&]() { - projectManager.NavigateBack(); + DrawMenuAction(MakeNavigateBackSelectionAction(context, target), [&]() { + ExecuteNavigateBackSelection(context, target); }); } inline void DrawHierarchyEditActions(IEditorContext& context, const EditActionTarget& target) { - const bool hierarchyRouteActive = target.route == EditorActionRoute::Hierarchy; - const ::XCEngine::Components::GameObject* activeObject = - hierarchyRouteActive ? target.selectedGameObject : nullptr; - DrawMenuAction(MakeCutAction(false), []() {}); - DrawMenuAction(MakeCopyEntityAction(const_cast<::XCEngine::Components::GameObject*>(activeObject)), [&]() { - Commands::CopyEntity(context, target.selectedGameObject->GetID()); + DrawMenuAction(MakeCopySelectionAction(target), [&]() { + ExecuteCopySelection(context, target); }); - DrawMenuAction( - hierarchyRouteActive ? MakePasteEntityAction(context) : MakeDisabledPasteAction(), - [&]() { - Commands::PasteEntity(context, target.selectedGameObject ? target.selectedGameObject->GetID() : 0); - }); - DrawMenuAction(MakeDuplicateEntityAction(const_cast<::XCEngine::Components::GameObject*>(activeObject)), [&]() { - Commands::DuplicateEntity(context, target.selectedGameObject->GetID()); + DrawMenuAction(MakePasteSelectionAction(context, target), [&]() { + ExecutePasteSelection(context, target); }); - DrawMenuAction(MakeDeleteEntityAction(const_cast<::XCEngine::Components::GameObject*>(activeObject)), [&]() { - Commands::DeleteEntity(context, target.selectedGameObject->GetID()); + DrawMenuAction(MakeDuplicateSelectionAction(target), [&]() { + ExecuteDuplicateSelection(context, target); }); - DrawMenuAction(MakeRenameEntityAction(const_cast<::XCEngine::Components::GameObject*>(activeObject)), [&]() { - context.GetEventBus().Publish(EntityRenameRequestedEvent{ target.selectedGameObject->GetID() }); + DrawMenuAction(MakeDeleteSelectionAction(target), [&]() { + ExecuteDeleteSelection(context, target); + }); + DrawMenuAction(MakeRenameSelectionAction(target), [&]() { + ExecuteRenameSelection(context, target); }); } diff --git a/editor/src/Actions/HierarchyActionRouter.h b/editor/src/Actions/HierarchyActionRouter.h index 55a3740a..5fe40151 100644 --- a/editor/src/Actions/HierarchyActionRouter.h +++ b/editor/src/Actions/HierarchyActionRouter.h @@ -5,6 +5,7 @@ #include "Core/EditorEvents.h" #include "Core/EventBus.h" #include "Core/IEditorContext.h" +#include "UI/PopupState.h" namespace XCEngine { namespace Editor { @@ -22,6 +23,15 @@ inline void RequestEntityRename(IEditorContext& context, const ::XCEngine::Compo context.GetEventBus().Publish(EntityRenameRequestedEvent{ gameObject->GetID() }); } +inline bool CommitEntityRename(IEditorContext& context, uint64_t entityId, const char* newName) { + if (entityId == 0 || !newName || newName[0] == '\0' || !context.GetSceneManager().GetEntity(entityId)) { + return false; + } + + Commands::RenameEntity(context, entityId, newName); + return true; +} + inline void HandleHierarchySelectionClick(IEditorContext& context, uint64_t entityId, bool additive) { auto& selectionManager = context.GetSelectionManager(); if (additive) { @@ -34,6 +44,59 @@ inline void HandleHierarchySelectionClick(IEditorContext& context, uint64_t enti selectionManager.SetSelectedEntity(entityId); } +template +inline void HandleHierarchyBackgroundPrimaryClick( + IEditorContext& context, + const UI::InlineTextEditState& renameState) { + if (!ImGui::IsWindowHovered() || !ImGui::IsMouseDown(0) || ImGui::IsAnyItemHovered() || renameState.IsActive()) { + return; + } + + context.GetSelectionManager().ClearSelection(); +} + +inline void RequestHierarchyOptionsPopup(UI::DeferredPopupState& optionsPopup) { + optionsPopup.RequestOpen(); +} + +template +inline void DrawHierarchySortOptionsPopup( + UI::DeferredPopupState& optionsPopup, + SortMode currentMode, + SortMode nameMode, + SortMode componentCountMode, + SortMode transformFirstMode, + SetSortModeFn&& setSortMode) { + optionsPopup.ConsumeOpenRequest("HierarchyOptions"); + + if (!UI::BeginPopup("HierarchyOptions")) { + return; + } + + const UI::MenuCommand commands[] = { + UI::MenuCommand::Action("Sort By Name", nullptr, currentMode == nameMode), + UI::MenuCommand::Action("Sort By Component Count", nullptr, currentMode == componentCountMode), + UI::MenuCommand::Action("Transform First", nullptr, currentMode == transformFirstMode) + }; + + UI::DrawMenuCommands(commands, [&](size_t index) { + switch (index) { + case 0: + setSortMode(nameMode); + break; + case 1: + setSortMode(componentCountMode); + break; + case 2: + setSortMode(transformFirstMode); + break; + default: + break; + } + }); + UI::EndPopup(); +} + inline bool BeginHierarchyEntityDrag(::XCEngine::Components::GameObject* gameObject) { if (!gameObject || !ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { return false; @@ -79,6 +142,11 @@ inline bool AcceptHierarchyEntityDropToRoot(IEditorContext& context) { return accepted; } +inline void DrawHierarchyRootDropTarget(IEditorContext& context) { + ImGui::InvisibleButton("##DragTarget", ImVec2(-1, -1)); + AcceptHierarchyEntityDropToRoot(context); +} + inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Components::GameObject* parent) { DrawMenuAction(MakeCreateEmptyEntityAction(), [&]() { Commands::CreateEmptyEntity(context, parent, "Create Entity", "GameObject"); @@ -102,6 +170,15 @@ inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Comp }); } +inline void DrawHierarchyBackgroundContextPopup(IEditorContext& context) { + if (!UI::BeginPopupContextWindow("HierarchyContextMenu", ImGuiPopupFlags_MouseButtonRight)) { + return; + } + + DrawHierarchyCreateActions(context, nullptr); + UI::EndPopup(); +} + inline void DrawHierarchyContextActions(IEditorContext& context, ::XCEngine::Components::GameObject* gameObject) { if (UI::DrawMenuScope("Create", [&]() { DrawHierarchyCreateActions(context, gameObject); @@ -133,6 +210,15 @@ inline void DrawHierarchyContextActions(IEditorContext& context, ::XCEngine::Com }); } +inline void DrawHierarchyEntityContextPopup(IEditorContext& context, ::XCEngine::Components::GameObject* gameObject) { + if (!UI::BeginPopupContextItem("EntityContextMenu")) { + return; + } + + DrawHierarchyContextActions(context, gameObject); + UI::EndPopup(); +} + } // namespace Actions } // namespace Editor } // namespace XCEngine diff --git a/editor/src/Actions/InspectorActionRouter.h b/editor/src/Actions/InspectorActionRouter.h index 966d7c6d..d755b8a4 100644 --- a/editor/src/Actions/InspectorActionRouter.h +++ b/editor/src/Actions/InspectorActionRouter.h @@ -3,7 +3,9 @@ #include "EditorActions.h" #include "Commands/ComponentCommands.h" #include "ComponentEditors/ComponentEditorRegistry.h" +#include "Core/EditorEvents.h" #include "Core/IEditorContext.h" +#include "UI/PopupState.h" #include "UI/UI.h" #include @@ -28,6 +30,28 @@ inline std::string BuildAddComponentMenuLabel(const IComponentEditor& editor, :: return label; } +inline void HandleInspectorSelectionChanged( + IEditorContext& context, + const SelectionChangedEvent& event, + uint64_t& selectedEntityId, + UI::DeferredPopupState& addComponentPopup) { + if (context.GetUndoManager().HasPendingInteractiveChange()) { + context.GetUndoManager().FinalizeInteractiveChange(); + } + + selectedEntityId = event.primarySelection; + addComponentPopup.Clear(); +} + +inline bool DrawInspectorAddComponentButton(UI::DeferredPopupState& addComponentPopup, bool enabled = true) { + if (!DrawInspectorAction(MakeAddComponentButtonAction(enabled))) { + return false; + } + + addComponentPopup.RequestOpen(); + return true; +} + inline bool DrawInspectorAddComponentMenu(IEditorContext& context, ::XCEngine::Components::GameObject* gameObject) { bool drewAnyEntry = false; @@ -49,6 +73,23 @@ inline bool DrawInspectorAddComponentMenu(IEditorContext& context, ::XCEngine::C return drewAnyEntry; } +inline void DrawInspectorAddComponentPopup( + IEditorContext& context, + UI::DeferredPopupState& addComponentPopup, + ::XCEngine::Components::GameObject* gameObject) { + addComponentPopup.ConsumeOpenRequest("AddComponent"); + + if (!UI::BeginTitledPopup("AddComponent", "Components")) { + return; + } + + if (!DrawInspectorAddComponentMenu(context, gameObject)) { + UI::DrawHintText("No registered component editors"); + } + + UI::EndTitledPopup(); +} + inline bool DrawInspectorComponentMenu( IEditorContext& context, ::XCEngine::Components::Component* component, @@ -60,6 +101,12 @@ inline bool DrawInspectorComponentMenu( }); } +inline void FinalizeInspectorInteractiveChangeIfIdle(IEditorContext& context) { + if (context.GetUndoManager().HasPendingInteractiveChange() && !ImGui::IsAnyItemActive()) { + context.GetUndoManager().FinalizeInteractiveChange(); + } +} + } // namespace Actions } // namespace Editor } // namespace XCEngine diff --git a/editor/src/Actions/MainMenuActionRouter.h b/editor/src/Actions/MainMenuActionRouter.h index bc147cc5..9c9f9383 100644 --- a/editor/src/Actions/MainMenuActionRouter.h +++ b/editor/src/Actions/MainMenuActionRouter.h @@ -7,42 +7,102 @@ #include "Core/EventBus.h" #include "Core/IEditorContext.h" #include "UI/PopupState.h" +#include "UI/UI.h" namespace XCEngine { namespace Editor { namespace Actions { +inline void ExecuteNewScene(IEditorContext& context) { + Commands::NewScene(context); +} + +inline void ExecuteOpenScene(IEditorContext& context) { + Commands::OpenSceneWithDialog(context); +} + +inline void ExecuteSaveScene(IEditorContext& context) { + Commands::SaveCurrentScene(context); +} + +inline void ExecuteSaveSceneAs(IEditorContext& context) { + Commands::SaveSceneAsWithDialog(context); +} + +inline void ExecuteUndo(IEditorContext& context) { + context.GetUndoManager().Undo(); +} + +inline void ExecuteRedo(IEditorContext& context) { + context.GetUndoManager().Redo(); +} + +inline void RequestEditorExit(IEditorContext& context) { + context.GetEventBus().Publish(EditorExitRequestedEvent{}); +} + +inline void RequestDockLayoutReset(IEditorContext& context) { + context.GetEventBus().Publish(DockLayoutResetRequestedEvent{}); +} + +inline void RequestAboutPopup(UI::DeferredPopupState& aboutPopup) { + aboutPopup.RequestOpen(); +} + inline void HandleMainMenuShortcuts(IEditorContext& context, const ShortcutContext& shortcutContext) { - HandleShortcut(MakeNewSceneAction(), shortcutContext, [&]() { Commands::NewScene(context); }); - HandleShortcut(MakeOpenSceneAction(), shortcutContext, [&]() { Commands::OpenSceneWithDialog(context); }); - HandleShortcut(MakeSaveSceneAction(), shortcutContext, [&]() { Commands::SaveCurrentScene(context); }); - HandleShortcut(MakeSaveSceneAsAction(), shortcutContext, [&]() { Commands::SaveSceneAsWithDialog(context); }); - HandleShortcut(MakeUndoAction(context), shortcutContext, [&]() { context.GetUndoManager().Undo(); }); - HandleShortcut(MakeRedoAction(context), shortcutContext, [&]() { context.GetUndoManager().Redo(); }); + HandleShortcut(MakeNewSceneAction(), shortcutContext, [&]() { ExecuteNewScene(context); }); + HandleShortcut(MakeOpenSceneAction(), shortcutContext, [&]() { ExecuteOpenScene(context); }); + HandleShortcut(MakeSaveSceneAction(), shortcutContext, [&]() { ExecuteSaveScene(context); }); + HandleShortcut(MakeSaveSceneAsAction(), shortcutContext, [&]() { ExecuteSaveSceneAs(context); }); + HandleShortcut(MakeUndoAction(context), shortcutContext, [&]() { ExecuteUndo(context); }); + HandleShortcut(MakeRedoAction(context), shortcutContext, [&]() { ExecuteRedo(context); }); HandleEditShortcuts(context, shortcutContext); } inline void DrawFileMenuActions(IEditorContext& context) { - DrawMenuAction(MakeNewSceneAction(), [&]() { Commands::NewScene(context); }); - DrawMenuAction(MakeOpenSceneAction(), [&]() { Commands::OpenSceneWithDialog(context); }); - DrawMenuAction(MakeSaveSceneAction(), [&]() { Commands::SaveCurrentScene(context); }); - DrawMenuAction(MakeSaveSceneAsAction(), [&]() { Commands::SaveSceneAsWithDialog(context); }); + DrawMenuAction(MakeNewSceneAction(), [&]() { ExecuteNewScene(context); }); + DrawMenuAction(MakeOpenSceneAction(), [&]() { ExecuteOpenScene(context); }); + DrawMenuAction(MakeSaveSceneAction(), [&]() { ExecuteSaveScene(context); }); + DrawMenuAction(MakeSaveSceneAsAction(), [&]() { ExecuteSaveSceneAs(context); }); DrawMenuSeparator(); - DrawMenuAction(MakeExitAction(), [&]() { - context.GetEventBus().Publish(EditorExitRequestedEvent{}); - }); + DrawMenuAction(MakeExitAction(), [&]() { RequestEditorExit(context); }); } inline void DrawViewMenuActions(IEditorContext& context) { - DrawMenuAction(MakeResetLayoutAction(), [&]() { - context.GetEventBus().Publish(DockLayoutResetRequestedEvent{}); - }); + DrawMenuAction(MakeResetLayoutAction(), [&]() { RequestDockLayoutReset(context); }); } inline void DrawHelpMenuActions(UI::DeferredPopupState& aboutPopup) { - DrawMenuAction(MakeAboutAction(), [&]() { - aboutPopup.RequestOpen(); + DrawMenuAction(MakeAboutAction(), [&]() { RequestAboutPopup(aboutPopup); }); +} + +inline void HandleMenuBarShortcuts(IEditorContext& context) { + HandleMainMenuShortcuts(context, GlobalShortcutContext()); +} + +inline void DrawMainMenuBar(IEditorContext& context, UI::DeferredPopupState& aboutPopup) { + if (!ImGui::BeginMainMenuBar()) { + return; + } + + UI::DrawMenuScope("File", [&]() { + DrawFileMenuActions(context); }); + UI::DrawMenuScope("Edit", [&]() { + DrawEditActions(context); + }); + UI::DrawMenuScope("View", [&]() { + DrawViewMenuActions(context); + }); + UI::DrawMenuScope("Help", [&]() { + DrawHelpMenuActions(aboutPopup); + }); + UI::DrawSceneStatusWidget(context); + ImGui::EndMainMenuBar(); +} + +inline void DrawMainMenuOverlays(IEditorContext* context, UI::DeferredPopupState& aboutPopup) { + UI::DrawEditorAboutDialog(context, aboutPopup); } } // namespace Actions diff --git a/editor/src/Actions/ProjectActionRouter.h b/editor/src/Actions/ProjectActionRouter.h index f855ea8b..984e9722 100644 --- a/editor/src/Actions/ProjectActionRouter.h +++ b/editor/src/Actions/ProjectActionRouter.h @@ -14,6 +14,8 @@ inline constexpr const char* ProjectAssetPayloadType() { return "ASSET_ITEM"; } +inline void DrawProjectAssetContextActions(IEditorContext& context, const AssetItemPtr& item); + inline int FindProjectItemIndex(IProjectManager& projectManager, const AssetItemPtr& item) { if (!item) { return -1; @@ -77,6 +79,64 @@ inline bool BeginProjectAssetDrag(const AssetItemPtr& item, UI::AssetIconKind ic return true; } +inline bool OpenProjectAsset(IEditorContext& context, const AssetItemPtr& item) { + if (!Commands::CanOpenAsset(item)) { + return false; + } + + Commands::OpenAsset(context, item); + return true; +} + +inline bool DrawProjectNavigateBackAction(IProjectManager& projectManager) { + const bool canGoBack = projectManager.CanNavigateBack(); + if (!DrawToolbarAction(MakeNavigateBackAction(canGoBack), UI::ProjectBackButtonSize())) { + return false; + } + + projectManager.NavigateBack(); + return true; +} + +inline void HandleProjectBackgroundPrimaryClick(IProjectManager& projectManager) { + if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(0) && !ImGui::IsAnyItemHovered()) { + projectManager.SetSelectedIndex(-1); + } +} + +inline void RequestProjectEmptyContextPopup(UI::DeferredPopupState& emptyContextMenu) { + if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(1) && !ImGui::IsAnyItemHovered()) { + emptyContextMenu.RequestOpen(); + } +} + +inline void HandleProjectItemSelection(IProjectManager& projectManager, int index) { + projectManager.SetSelectedIndex(index); +} + +inline void HandleProjectItemContextRequest( + IProjectManager& projectManager, + int index, + const AssetItemPtr& item, + UI::TargetedPopupState& itemContextMenu) { + projectManager.SetSelectedIndex(index); + itemContextMenu.RequestOpen(item); +} + +inline void DrawProjectItemContextPopup(IEditorContext& context, UI::TargetedPopupState& itemContextMenu) { + itemContextMenu.ConsumeOpenRequest("ItemContextMenu"); + if (UI::BeginPopup("ItemContextMenu")) { + if (itemContextMenu.HasTarget()) { + DrawProjectAssetContextActions(context, itemContextMenu.TargetValue()); + } + UI::EndPopup(); + } + + if (!ImGui::IsPopupOpen("ItemContextMenu") && !itemContextMenu.HasPendingOpenRequest()) { + itemContextMenu.Clear(); + } +} + inline void DrawProjectAssetContextActions(IEditorContext& context, const AssetItemPtr& item) { auto& projectManager = context.GetProjectManager(); const int itemIndex = FindProjectItemIndex(projectManager, item); @@ -97,6 +157,20 @@ inline void DrawProjectEmptyContextActions(UI::TextInputPopupState +inline void DrawProjectEmptyContextPopup( + UI::DeferredPopupState& emptyContextMenu, + UI::TextInputPopupState& createFolderDialog) { + emptyContextMenu.ConsumeOpenRequest("EmptyContextMenu"); + + if (!UI::BeginPopup("EmptyContextMenu")) { + return; + } + + DrawProjectEmptyContextActions(createFolderDialog); + UI::EndPopup(); +} + template inline void DrawProjectCreateFolderDialog(IEditorContext& context, UI::TextInputPopupState& createFolderDialog) { createFolderDialog.ConsumeOpenRequest("Create Folder"); diff --git a/editor/src/Application.cpp b/editor/src/Application.cpp index 29a38486..c5513f82 100644 --- a/editor/src/Application.cpp +++ b/editor/src/Application.cpp @@ -17,6 +17,62 @@ Application& Application::Get() { return instance; } +bool Application::InitializeWindowRenderer(HWND hwnd) { + if (m_windowRenderer.Initialize(hwnd, 1280, 720)) { + return true; + } + + MessageBoxW(hwnd, L"Failed to create D3D12 device", L"Error", MB_OK | MB_ICONERROR); + return false; +} + +void Application::InitializeEditorContext(const std::string& projectPath) { + m_editorContext = std::make_shared(); + m_editorContext->SetProjectPath(projectPath); + m_exitRequestedHandlerId = m_editorContext->GetEventBus().Subscribe( + [this](const EditorExitRequestedEvent&) { + if (m_hwnd) { + PostMessageW(m_hwnd, WM_CLOSE, 0, 0); + } + }); +} + +void Application::InitializeImGui(HWND hwnd) { + m_imguiSession.Initialize(m_editorContext->GetProjectPath()); + m_imguiBackend.Initialize(hwnd, m_windowRenderer.GetDevice(), m_windowRenderer.GetSrvHeap()); +} + +void Application::AttachEditorLayer() { + m_editorLayer = new EditorLayer(); + m_editorLayer->SetContext(m_editorContext); + m_layerStack.pushLayer(std::unique_ptr(m_editorLayer)); + m_layerStack.onAttach(); +} + +void Application::DetachEditorLayer() { + m_layerStack.onDetach(); + m_editorLayer = nullptr; +} + +void Application::ShutdownEditorContext() { + if (m_editorContext && m_exitRequestedHandlerId) { + m_editorContext->GetEventBus().Unsubscribe(m_exitRequestedHandlerId); + m_exitRequestedHandlerId = 0; + } + + m_editorContext.reset(); +} + +void Application::RenderEditorFrame() { + static constexpr float kClearColor[4] = { 0.22f, 0.22f, 0.22f, 1.0f }; + + m_imguiBackend.BeginFrame(); + m_layerStack.onImGuiRender(); + UpdateWindowTitle(); + ImGui::Render(); + m_windowRenderer.Render(m_imguiBackend, kClearColor); +} + bool Application::Initialize(HWND hwnd) { Platform::InstallCrashExceptionFilter(); Platform::RedirectStderrToExecutableLog(); @@ -25,55 +81,27 @@ bool Application::Initialize(HWND hwnd) { ConfigureEditorLogging(exeDir); m_hwnd = hwnd; - - if (!m_windowRenderer.Initialize(hwnd, 1280, 720)) { - MessageBoxW(hwnd, L"Failed to create D3D12 device", L"Error", MB_OK | MB_ICONERROR); + + if (!InitializeWindowRenderer(hwnd)) { return false; } - - m_editorContext = std::make_shared(); - m_editorContext->SetProjectPath(exeDir); - m_exitRequestedHandlerId = m_editorContext->GetEventBus().Subscribe( - [this](const EditorExitRequestedEvent&) { - if (m_hwnd) { - PostMessageW(m_hwnd, WM_CLOSE, 0, 0); - } - }); - m_imguiSession.Initialize(m_editorContext->GetProjectPath()); - m_imguiBackend.Initialize(hwnd, m_windowRenderer.GetDevice(), m_windowRenderer.GetSrvHeap()); - - m_editorLayer = new EditorLayer(); - m_editorLayer->SetContext(m_editorContext); - m_layerStack.pushLayer(std::unique_ptr(m_editorLayer)); - m_layerStack.onAttach(); - + InitializeEditorContext(exeDir); + InitializeImGui(hwnd); + AttachEditorLayer(); return true; } void Application::Shutdown() { - m_layerStack.onDetach(); - - if (m_editorContext && m_exitRequestedHandlerId) { - m_editorContext->GetEventBus().Unsubscribe(m_exitRequestedHandlerId); - m_exitRequestedHandlerId = 0; - } - + DetachEditorLayer(); m_imguiBackend.Shutdown(); m_imguiSession.Shutdown(); + ShutdownEditorContext(); m_windowRenderer.Shutdown(); } void Application::Render() { - m_imguiBackend.BeginFrame(); - - m_layerStack.onImGuiRender(); - UpdateWindowTitle(); - - ImGui::Render(); - - float clearColor[4] = { 0.22f, 0.22f, 0.22f, 1.0f }; - m_windowRenderer.Render(m_imguiBackend, clearColor); + RenderEditorFrame(); } void Application::UpdateWindowTitle() { diff --git a/editor/src/Application.h b/editor/src/Application.h index 2674562f..ce5e47d7 100644 --- a/editor/src/Application.h +++ b/editor/src/Application.h @@ -31,6 +31,13 @@ public: private: Application() = default; ~Application() = default; + bool InitializeWindowRenderer(HWND hwnd); + void InitializeEditorContext(const std::string& projectPath); + void InitializeImGui(HWND hwnd); + void AttachEditorLayer(); + void DetachEditorLayer(); + void ShutdownEditorContext(); + void RenderEditorFrame(); void UpdateWindowTitle(); HWND m_hwnd = nullptr; diff --git a/editor/src/panels/ConsolePanel.cpp b/editor/src/panels/ConsolePanel.cpp index 5902ee36..b08e8c69 100644 --- a/editor/src/panels/ConsolePanel.cpp +++ b/editor/src/panels/ConsolePanel.cpp @@ -32,25 +32,8 @@ void ConsolePanel::Render() { if (!content.IsOpen()) { return; } - - const auto logs = sink->GetLogs(); - size_t logIndex = 0; - for (const auto& log : logs) { - if (!m_filterState.Allows(log.level)) { - continue; - } - ImGui::PushID(static_cast(logIndex)); - - const std::string fullMessage = UI::BuildConsoleLogText(log); - if (UI::DrawConsoleLogRow(fullMessage.c_str())) { - ImGui::SetClipboardText(fullMessage.c_str()); - } - - ImGui::PopID(); - logIndex++; - } - + Actions::DrawConsoleLogRows(*sink, m_filterState); } } diff --git a/editor/src/panels/HierarchyPanel.cpp b/editor/src/panels/HierarchyPanel.cpp index 613e8881..b5a4f6dc 100644 --- a/editor/src/panels/HierarchyPanel.cpp +++ b/editor/src/panels/HierarchyPanel.cpp @@ -87,21 +87,11 @@ void HierarchyPanel::Render() { for (auto* gameObject : rootEntities) { RenderEntity(gameObject, filter); } - - if (ImGui::IsWindowHovered() && ImGui::IsMouseDown(0) && !ImGui::IsAnyItemHovered()) { - if (!m_renameState.IsActive()) { - m_context->GetSelectionManager().ClearSelection(); - } - } - - if (UI::BeginPopupContextWindow("HierarchyContextMenu", ImGuiPopupFlags_MouseButtonRight)) { - Actions::DrawHierarchyCreateActions(*m_context, nullptr); - UI::EndPopup(); - } - - ImGui::InvisibleButton("##DragTarget", ImVec2(-1, -1)); - Actions::AcceptHierarchyEntityDropToRoot(*m_context); - + + Actions::HandleHierarchyBackgroundPrimaryClick(*m_context, m_renameState); + + Actions::DrawHierarchyBackgroundContextPopup(*m_context); + Actions::DrawHierarchyRootDropTarget(*m_context); } void HierarchyPanel::RenderSearchBar() { @@ -119,33 +109,18 @@ void HierarchyPanel::RenderSearchBar() { buttonWidth + UI::ToolbarSearchTrailingSpacing()); ImGui::SameLine(); if (UI::ToolbarButton("...", false, ImVec2(buttonWidth, 0.0f))) { - ImGui::OpenPopup("HierarchyOptions"); + Actions::RequestHierarchyOptionsPopup(m_optionsPopup); } - if (UI::BeginPopup("HierarchyOptions")) { - const UI::MenuCommand commands[] = { - UI::MenuCommand::Action("Sort By Name", nullptr, m_sortMode == SortMode::Name), - UI::MenuCommand::Action("Sort By Component Count", nullptr, m_sortMode == SortMode::ComponentCount), - UI::MenuCommand::Action("Transform First", nullptr, m_sortMode == SortMode::TransformFirst) - }; - - UI::DrawMenuCommands(commands, [&](size_t index) { - switch (index) { - case 0: - m_sortMode = SortMode::Name; - break; - case 1: - m_sortMode = SortMode::ComponentCount; - break; - case 2: - m_sortMode = SortMode::TransformFirst; - break; - default: - break; - } + Actions::DrawHierarchySortOptionsPopup( + m_optionsPopup, + m_sortMode, + SortMode::Name, + SortMode::ComponentCount, + SortMode::TransformFirst, + [this](SortMode mode) { + m_sortMode = mode; }); - UI::EndPopup(); - } } void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject, const std::string& filter) { @@ -193,11 +168,8 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject Actions::BeginHierarchyEntityDrag(gameObject); Actions::AcceptHierarchyEntityDrop(*m_context, gameObject); - - if (UI::BeginPopupContextItem("EntityContextMenu")) { - Actions::DrawHierarchyContextActions(*m_context, gameObject); - UI::EndPopup(); - } + + Actions::DrawHierarchyEntityContextPopup(*m_context, gameObject); if (node.open) { for (size_t i = 0; i < gameObject->GetChildCount(); i++) { @@ -225,10 +197,7 @@ void HierarchyPanel::CommitRename() { } const uint64_t entityId = m_renameState.Item(); - if (!m_renameState.Empty() && m_context->GetSceneManager().GetEntity(entityId)) { - Commands::RenameEntity(*m_context, entityId, m_renameState.Buffer()); - } - + Actions::CommitEntityRename(*m_context, entityId, m_renameState.Buffer()); CancelRename(); } diff --git a/editor/src/panels/HierarchyPanel.h b/editor/src/panels/HierarchyPanel.h index 3aa1553b..c46db982 100644 --- a/editor/src/panels/HierarchyPanel.h +++ b/editor/src/panels/HierarchyPanel.h @@ -31,6 +31,7 @@ private: char m_searchBuffer[256] = ""; UI::InlineTextEditState m_renameState; + UI::DeferredPopupState m_optionsPopup; SortMode m_sortMode = SortMode::Name; uint64_t m_selectionHandlerId = 0; uint64_t m_renameRequestHandlerId = 0; diff --git a/editor/src/panels/InspectorPanel.cpp b/editor/src/panels/InspectorPanel.cpp index 07b7c600..bd38b34c 100644 --- a/editor/src/panels/InspectorPanel.cpp +++ b/editor/src/panels/InspectorPanel.cpp @@ -44,11 +44,7 @@ void InspectorPanel::OnDetach() { } void InspectorPanel::OnSelectionChanged(const SelectionChangedEvent& event) { - if (m_context && m_context->GetUndoManager().HasPendingInteractiveChange()) { - m_context->GetUndoManager().FinalizeInteractiveChange(); - } - m_selectedEntityId = event.primarySelection; - m_addComponentPopup.Clear(); + Actions::HandleInspectorSelectionChanged(*m_context, event, m_selectedEntityId, m_addComponentPopup); } void InspectorPanel::Render() { @@ -90,14 +86,10 @@ void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameOb RenderComponent(component, gameObject); } - if (Actions::DrawInspectorAction(Actions::MakeAddComponentButtonAction(gameObject != nullptr))) { - m_addComponentPopup.RequestOpen(); - } - RenderAddComponentPopup(gameObject); + Actions::DrawInspectorAddComponentButton(m_addComponentPopup, gameObject != nullptr); + Actions::DrawInspectorAddComponentPopup(*m_context, m_addComponentPopup, gameObject); - if (m_context->GetUndoManager().HasPendingInteractiveChange() && !ImGui::IsAnyItemActive()) { - m_context->GetUndoManager().FinalizeInteractiveChange(); - } + Actions::FinalizeInspectorInteractiveChangeIfIdle(*m_context); } void InspectorPanel::RenderEmptyState(const char* title, const char* subtitle) { @@ -109,20 +101,6 @@ void InspectorPanel::RenderEmptyState(const char* title, const char* subtitle) { UI::DrawEmptyState(title, subtitle); } -void InspectorPanel::RenderAddComponentPopup(::XCEngine::Components::GameObject* gameObject) { - m_addComponentPopup.ConsumeOpenRequest("AddComponent"); - - if (!UI::BeginTitledPopup("AddComponent", "Components")) { - return; - } - - if (!Actions::DrawInspectorAddComponentMenu(*m_context, gameObject)) { - UI::DrawHintText("No registered component editors"); - } - - UI::EndTitledPopup(); -} - void InspectorPanel::RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject) { if (!component) return; diff --git a/editor/src/panels/InspectorPanel.h b/editor/src/panels/InspectorPanel.h index cd51be94..fb51e3b4 100644 --- a/editor/src/panels/InspectorPanel.h +++ b/editor/src/panels/InspectorPanel.h @@ -24,7 +24,6 @@ public: private: void RenderGameObject(::XCEngine::Components::GameObject* gameObject); - void RenderAddComponentPopup(::XCEngine::Components::GameObject* gameObject); void RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject); void RenderEmptyState(const char* title, const char* subtitle = nullptr); void OnSelectionChanged(const struct SelectionChangedEvent& event); diff --git a/editor/src/panels/MenuBar.cpp b/editor/src/panels/MenuBar.cpp index c06fb8e6..91db2ca5 100644 --- a/editor/src/panels/MenuBar.cpp +++ b/editor/src/panels/MenuBar.cpp @@ -3,7 +3,6 @@ #include "Actions/MainMenuActionRouter.h" #include "MenuBar.h" #include "Core/IEditorContext.h" -#include "UI/UI.h" #include namespace XCEngine { @@ -12,61 +11,13 @@ namespace Editor { MenuBar::MenuBar() : Panel("MenuBar") {} void MenuBar::Render() { - HandleShortcuts(); - - if (ImGui::BeginMainMenuBar()) { - ShowFileMenu(); - ShowEditMenu(); - ShowViewMenu(); - ShowHelpMenu(); - RenderSceneStatus(); - ImGui::EndMainMenuBar(); - } - - RenderAboutPopup(); -} - -void MenuBar::HandleShortcuts() { if (!m_context) { return; } - Actions::HandleMainMenuShortcuts(*m_context, Actions::GlobalShortcutContext()); -} - -void MenuBar::ShowFileMenu() { - UI::DrawMenuScope("File", [&]() { - Actions::DrawFileMenuActions(*m_context); - }); -} - -void MenuBar::ShowEditMenu() { - UI::DrawMenuScope("Edit", [&]() { - Actions::DrawEditActions(*m_context); - }); -} - -void MenuBar::ShowViewMenu() { - UI::DrawMenuScope("View", [&]() { - Actions::DrawViewMenuActions(*m_context); - }); -} - -void MenuBar::ShowHelpMenu() { - UI::DrawMenuScope("Help", [&]() { - Actions::DrawHelpMenuActions(m_aboutPopup); - }); -} - -void MenuBar::RenderAboutPopup() { - UI::DrawEditorAboutDialog(m_context, m_aboutPopup); -} - -void MenuBar::RenderSceneStatus() { - if (!m_context) { - return; - } - UI::DrawSceneStatusWidget(*m_context); + Actions::HandleMenuBarShortcuts(*m_context); + Actions::DrawMainMenuBar(*m_context, m_aboutPopup); + Actions::DrawMainMenuOverlays(m_context, m_aboutPopup); } } diff --git a/editor/src/panels/MenuBar.h b/editor/src/panels/MenuBar.h index 99225f23..d8acc8e0 100644 --- a/editor/src/panels/MenuBar.h +++ b/editor/src/panels/MenuBar.h @@ -12,14 +12,6 @@ public: void Render() override; private: - void HandleShortcuts(); - void RenderSceneStatus(); - void RenderAboutPopup(); - void ShowFileMenu(); - void ShowEditMenu(); - void ShowViewMenu(); - void ShowHelpMenu(); - UI::DeferredPopupState m_aboutPopup; }; diff --git a/editor/src/panels/ProjectPanel.cpp b/editor/src/panels/ProjectPanel.cpp index 9d2d925a..36833615 100644 --- a/editor/src/panels/ProjectPanel.cpp +++ b/editor/src/panels/ProjectPanel.cpp @@ -30,11 +30,7 @@ void ProjectPanel::Render() { UI::PanelToolbarScope toolbar("ProjectToolbar", UI::ProjectPanelToolbarHeight()); if (toolbar.IsOpen()) { - - bool canGoBack = manager.CanNavigateBack(); - if (Actions::DrawToolbarAction(Actions::MakeNavigateBackAction(canGoBack), UI::ProjectBackButtonSize())) { - manager.NavigateBack(); - } + Actions::DrawProjectNavigateBackAction(manager); ImGui::SameLine(); size_t pathDepth = manager.GetPathDepth(); @@ -88,29 +84,10 @@ void ProjectPanel::Render() { searchStr.empty() ? "Current folder is empty" : "No assets match the current search"); } - if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(0) && !ImGui::IsAnyItemHovered()) { - manager.SetSelectedIndex(-1); - } - - m_itemContextMenu.ConsumeOpenRequest("ItemContextMenu"); - if (UI::BeginPopup("ItemContextMenu")) { - if (m_itemContextMenu.HasTarget()) { - Actions::DrawProjectAssetContextActions(*m_context, m_itemContextMenu.TargetValue()); - } - UI::EndPopup(); - } - if (!ImGui::IsPopupOpen("ItemContextMenu") && !m_itemContextMenu.HasPendingOpenRequest()) { - m_itemContextMenu.Clear(); - } - - if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(1) && !ImGui::IsAnyItemHovered()) { - ImGui::OpenPopup("EmptyContextMenu"); - } - - if (UI::BeginPopup("EmptyContextMenu")) { - Actions::DrawProjectEmptyContextActions(m_createFolderDialog); - UI::EndPopup(); - } + Actions::HandleProjectBackgroundPrimaryClick(manager); + Actions::DrawProjectItemContextPopup(*m_context, m_itemContextMenu); + Actions::RequestProjectEmptyContextPopup(m_emptyContextMenu); + Actions::DrawProjectEmptyContextPopup(m_emptyContextMenu, m_createFolderDialog); Actions::DrawProjectCreateFolderDialog(*m_context, m_createFolderDialog); } @@ -132,19 +109,18 @@ void ProjectPanel::RenderAssetItem(const AssetItemPtr& item, int index) { }); if (tile.clicked) { - manager.SetSelectedIndex(index); + Actions::HandleProjectItemSelection(manager, index); } if (tile.contextRequested) { - manager.SetSelectedIndex(index); - m_itemContextMenu.RequestOpen(item); + Actions::HandleProjectItemContextRequest(manager, index, item, m_itemContextMenu); } Actions::AcceptProjectAssetDrop(manager, item); Actions::BeginProjectAssetDrag(item, iconKind); if (tile.openRequested) { - Commands::OpenAsset(*m_context, item); + Actions::OpenProjectAsset(*m_context, item); } ImGui::PopID(); diff --git a/editor/src/panels/ProjectPanel.h b/editor/src/panels/ProjectPanel.h index e63845c5..cf309dd8 100644 --- a/editor/src/panels/ProjectPanel.h +++ b/editor/src/panels/ProjectPanel.h @@ -18,6 +18,7 @@ private: char m_searchBuffer[256] = ""; UI::TextInputPopupState<256> m_createFolderDialog; + UI::DeferredPopupState m_emptyContextMenu; UI::TargetedPopupState m_itemContextMenu; }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9603d16f..9828313d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -44,6 +44,7 @@ add_subdirectory(Rendering) add_subdirectory(rhi) add_subdirectory(resources) add_subdirectory(input) +add_subdirectory(editor) # ============================================================ # Test Summary diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt new file mode 100644 index 00000000..0cb2f55d --- /dev/null +++ b/tests/editor/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.15) + +project(XCEngine_EditorTests) + +set(EDITOR_TEST_SOURCES + test_action_routing.cpp + ${CMAKE_SOURCE_DIR}/editor/src/Core/UndoManager.cpp + ${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp + ${CMAKE_SOURCE_DIR}/editor/src/Managers/ProjectManager.cpp +) + +add_executable(editor_tests ${EDITOR_TEST_SOURCES}) + +if(MSVC) + set_target_properties(editor_tests PROPERTIES + LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib" + ) +endif() + +target_link_libraries(editor_tests PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + user32 + comdlg32 +) + +target_include_directories(editor_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/editor/src + ${CMAKE_BINARY_DIR}/_deps/imgui-src + ${CMAKE_BINARY_DIR}/_deps/imgui-src/backends +) + +include(GoogleTest) +gtest_discover_tests(editor_tests) diff --git a/tests/editor/test_action_routing.cpp b/tests/editor/test_action_routing.cpp new file mode 100644 index 00000000..3d93fe68 --- /dev/null +++ b/tests/editor/test_action_routing.cpp @@ -0,0 +1,224 @@ +#include + +#include "Actions/EditActionRouter.h" +#include "Commands/EntityCommands.h" +#include "Commands/SceneCommands.h" +#include "Core/EditorContext.h" + +#include +#include + +#include +#include +#include +#include + +namespace fs = std::filesystem; + +namespace XCEngine::Editor { +namespace { + +class EditorActionRoutingTest : public ::testing::Test { +protected: + void SetUp() override { + const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); + m_projectRoot = fs::temp_directory_path() / ("xc_editor_tests_" + std::to_string(stamp)); + fs::create_directories(m_projectRoot); + + m_context.SetProjectPath(m_projectRoot.string()); + m_context.GetProjectManager().Initialize(m_projectRoot.string()); + m_context.GetSceneManager().NewScene("Editor Test Scene"); + } + + void TearDown() override { + std::error_code ec; + fs::remove_all(m_projectRoot, ec); + } + + AssetItemPtr FindCurrentItemByName(const std::string& name) { + for (auto& item : m_context.GetProjectManager().GetCurrentItems()) { + if (item && item->name == name) { + return item; + } + } + return nullptr; + } + + int FindCurrentItemIndexByName(const std::string& name) { + auto& items = m_context.GetProjectManager().GetCurrentItems(); + for (int i = 0; i < static_cast(items.size()); ++i) { + if (items[i] && items[i]->name == name) { + return i; + } + } + return -1; + } + + static void ExpectNear(const Math::Vector3& actual, const Math::Vector3& expected, float epsilon = 1e-4f) { + EXPECT_NEAR(actual.x, expected.x, epsilon); + EXPECT_NEAR(actual.y, expected.y, epsilon); + EXPECT_NEAR(actual.z, expected.z, epsilon); + } + + static size_t CountHierarchyEntities(const ISceneManager& sceneManager) { + auto countChildren = [](const ::XCEngine::Components::GameObject* gameObject, const auto& self) -> size_t { + if (!gameObject) { + return 0; + } + + size_t count = 1; + for (size_t i = 0; i < gameObject->GetChildCount(); ++i) { + count += self(gameObject->GetChild(i), self); + } + return count; + }; + + size_t total = 0; + for (auto* root : sceneManager.GetRootEntities()) { + total += countChildren(root, countChildren); + } + return total; + } + + EditorContext m_context; + fs::path m_projectRoot; +}; + +TEST_F(EditorActionRoutingTest, HierarchyRouteExecutesCopyPasteDuplicateDeleteAndRename) { + auto* entity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Entity", "Original"); + ASSERT_NE(entity, nullptr); + + m_context.SetActiveActionRoute(EditorActionRoute::Hierarchy); + m_context.GetSelectionManager().SetSelectedEntity(entity->GetID()); + + const Actions::EditActionTarget target = Actions::ResolveEditActionTarget(m_context); + ASSERT_EQ(target.route, EditorActionRoute::Hierarchy); + ASSERT_EQ(target.selectedGameObject, entity); + + uint64_t renameRequestedId = 0; + const uint64_t renameSubscription = m_context.GetEventBus().Subscribe( + [&](const EntityRenameRequestedEvent& event) { + renameRequestedId = event.entityId; + }); + EXPECT_TRUE(Actions::ExecuteRenameSelection(m_context, target)); + EXPECT_EQ(renameRequestedId, entity->GetID()); + m_context.GetEventBus().Unsubscribe(renameSubscription); + + const size_t entityCountBeforePaste = CountHierarchyEntities(m_context.GetSceneManager()); + EXPECT_TRUE(Actions::ExecuteCopySelection(m_context, target)); + EXPECT_TRUE(m_context.GetSceneManager().HasClipboardData()); + EXPECT_TRUE(Actions::ExecutePasteSelection(m_context, target)); + EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforePaste + 1); + + const uint64_t pastedEntityId = m_context.GetSelectionManager().GetSelectedEntity(); + EXPECT_NE(pastedEntityId, 0u); + EXPECT_NE(pastedEntityId, entity->GetID()); + ASSERT_NE(m_context.GetSceneManager().GetEntity(pastedEntityId), nullptr); + EXPECT_EQ(m_context.GetSceneManager().GetEntity(pastedEntityId)->GetParent(), entity); + + const Actions::EditActionTarget pastedTarget = Actions::ResolveEditActionTarget(m_context); + ASSERT_NE(pastedTarget.selectedGameObject, nullptr); + + const size_t entityCountBeforeDuplicate = CountHierarchyEntities(m_context.GetSceneManager()); + EXPECT_TRUE(Actions::ExecuteDuplicateSelection(m_context, pastedTarget)); + EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforeDuplicate + 1); + + const uint64_t duplicatedEntityId = m_context.GetSelectionManager().GetSelectedEntity(); + const Actions::EditActionTarget duplicatedTarget = Actions::ResolveEditActionTarget(m_context); + ASSERT_NE(duplicatedTarget.selectedGameObject, nullptr); + EXPECT_EQ(duplicatedTarget.selectedGameObject->GetParent(), entity); + EXPECT_TRUE(Actions::ExecuteDeleteSelection(m_context, duplicatedTarget)); + EXPECT_EQ(m_context.GetSceneManager().GetEntity(duplicatedEntityId), nullptr); + EXPECT_FALSE(m_context.GetSelectionManager().IsSelected(duplicatedEntityId)); +} + +TEST_F(EditorActionRoutingTest, ProjectRouteExecutesOpenBackAndDelete) { + const fs::path assetsDir = m_projectRoot / "Assets"; + const fs::path folderPath = assetsDir / "RouteFolder"; + const fs::path filePath = assetsDir / "DeleteMe.txt"; + + fs::create_directories(folderPath); + std::ofstream(filePath.string()) << "temporary"; + m_context.GetProjectManager().RefreshCurrentFolder(); + + const int folderIndex = FindCurrentItemIndexByName("RouteFolder"); + ASSERT_GE(folderIndex, 0); + + m_context.SetActiveActionRoute(EditorActionRoute::Project); + m_context.GetProjectManager().SetSelectedIndex(folderIndex); + + const Actions::EditActionTarget folderTarget = Actions::ResolveEditActionTarget(m_context); + ASSERT_EQ(folderTarget.route, EditorActionRoute::Project); + ASSERT_NE(folderTarget.selectedAssetItem, nullptr); + EXPECT_EQ(folderTarget.selectedAssetItem->name, "RouteFolder"); + + EXPECT_TRUE(Actions::ExecuteOpenSelection(m_context, folderTarget)); + EXPECT_EQ(m_context.GetProjectManager().GetCurrentPath(), "Assets/RouteFolder"); + + const Actions::EditActionTarget backTarget = Actions::ResolveEditActionTarget(m_context); + EXPECT_TRUE(Actions::ExecuteNavigateBackSelection(m_context, backTarget)); + EXPECT_EQ(m_context.GetProjectManager().GetCurrentPath(), "Assets"); + + m_context.GetProjectManager().RefreshCurrentFolder(); + const int fileIndex = FindCurrentItemIndexByName("DeleteMe.txt"); + ASSERT_GE(fileIndex, 0); + m_context.GetProjectManager().SetSelectedIndex(fileIndex); + + const Actions::EditActionTarget deleteTarget = Actions::ResolveEditActionTarget(m_context); + ASSERT_NE(deleteTarget.selectedAssetItem, nullptr); + EXPECT_TRUE(Actions::ExecuteDeleteSelection(m_context, deleteTarget)); + EXPECT_FALSE(fs::exists(filePath)); +} + +TEST_F(EditorActionRoutingTest, LoadSceneResetsSelectionAndUndoAfterFallbackSave) { + auto* savedEntity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Saved", "SavedEntity"); + ASSERT_NE(savedEntity, nullptr); + ASSERT_TRUE(m_context.GetSelectionManager().HasSelection()); + ASSERT_TRUE(m_context.GetUndoManager().CanUndo()); + + const fs::path savedScenePath = m_projectRoot / "Assets" / "Scenes" / "RegressionScene.xc"; + EXPECT_TRUE(Commands::SaveDirtySceneWithFallback(m_context, savedScenePath.string())); + EXPECT_TRUE(fs::exists(savedScenePath)); + EXPECT_FALSE(m_context.GetSceneManager().IsSceneDirty()); + + auto* transientEntity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Transient", "TransientEntity"); + ASSERT_NE(transientEntity, nullptr); + ASSERT_TRUE(m_context.GetSelectionManager().HasSelection()); + ASSERT_TRUE(m_context.GetUndoManager().CanUndo()); + + EXPECT_TRUE(Commands::LoadScene(m_context, savedScenePath.string(), false)); + EXPECT_FALSE(m_context.GetSelectionManager().HasSelection()); + EXPECT_FALSE(m_context.GetUndoManager().CanUndo()); + ASSERT_EQ(m_context.GetSceneManager().GetRootEntities().size(), 1u); + EXPECT_EQ(m_context.GetSceneManager().GetRootEntities()[0]->GetName(), "SavedEntity"); +} + +TEST_F(EditorActionRoutingTest, ReparentPreserveWorldTransformKeepsWorldPose) { + auto* parentA = Commands::CreateEmptyEntity(m_context, nullptr, "Create Parent A", "ParentA"); + auto* child = Commands::CreateEmptyEntity(m_context, parentA, "Create Child", "Child"); + auto* parentB = Commands::CreateEmptyEntity(m_context, nullptr, "Create Parent B", "ParentB"); + + ASSERT_NE(parentA, nullptr); + ASSERT_NE(child, nullptr); + ASSERT_NE(parentB, nullptr); + + parentA->GetTransform()->SetPosition(Math::Vector3(5.0f, 1.0f, -2.0f)); + parentB->GetTransform()->SetPosition(Math::Vector3(-4.0f, 3.0f, 8.0f)); + child->GetTransform()->SetLocalPosition(Math::Vector3(2.0f, 3.0f, 4.0f)); + child->GetTransform()->SetLocalRotation(Math::Quaternion::FromEulerAngles(Math::Vector3(0.25f, 0.5f, 0.75f))); + child->GetTransform()->SetLocalScale(Math::Vector3(1.5f, 2.0f, 0.5f)); + + const Math::Vector3 worldPositionBefore = child->GetTransform()->GetPosition(); + const Math::Vector3 worldScaleBefore = child->GetTransform()->GetScale(); + + EXPECT_TRUE(Commands::CanReparentEntity(child, parentB)); + EXPECT_FALSE(Commands::CanReparentEntity(parentA, child)); + EXPECT_TRUE(Commands::ReparentEntityPreserveWorldTransform(m_context, child, parentB->GetID())); + + EXPECT_EQ(child->GetParent(), parentB); + ExpectNear(child->GetTransform()->GetPosition(), worldPositionBefore); + ExpectNear(child->GetTransform()->GetScale(), worldScaleBefore); +} + +} // namespace +} // namespace XCEngine::Editor