Refine editor action shell and add regression tests
This commit is contained in:
@@ -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 保持
|
||||
|
||||
## 下一阶段建议执行顺序
|
||||
|
||||
### 第一阶段
|
||||
|
||||
@@ -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<int>(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
|
||||
|
||||
@@ -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, [&]() {
|
||||
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());
|
||||
});
|
||||
HandleShortcut(MakeRenameEntityAction(target.selectedGameObject), shortcutContext, [&]() {
|
||||
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() });
|
||||
});
|
||||
HandleShortcut(MakeCopyEntityAction(target.selectedGameObject), shortcutContext, [&]() {
|
||||
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());
|
||||
});
|
||||
HandleShortcut(MakePasteEntityAction(context), shortcutContext, [&]() {
|
||||
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);
|
||||
});
|
||||
HandleShortcut(MakeDuplicateEntityAction(target.selectedGameObject), shortcutContext, [&]() {
|
||||
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(MakePasteSelectionAction(context, target), [&]() {
|
||||
ExecutePasteSelection(context, target);
|
||||
});
|
||||
DrawMenuAction(MakeDuplicateEntityAction(const_cast<::XCEngine::Components::GameObject*>(activeObject)), [&]() {
|
||||
Commands::DuplicateEntity(context, target.selectedGameObject->GetID());
|
||||
DrawMenuAction(MakeDuplicateSelectionAction(target), [&]() {
|
||||
ExecuteDuplicateSelection(context, target);
|
||||
});
|
||||
DrawMenuAction(MakeDeleteEntityAction(const_cast<::XCEngine::Components::GameObject*>(activeObject)), [&]() {
|
||||
Commands::DeleteEntity(context, target.selectedGameObject->GetID());
|
||||
DrawMenuAction(MakeDeleteSelectionAction(target), [&]() {
|
||||
ExecuteDeleteSelection(context, target);
|
||||
});
|
||||
DrawMenuAction(MakeRenameEntityAction(const_cast<::XCEngine::Components::GameObject*>(activeObject)), [&]() {
|
||||
context.GetEventBus().Publish(EntityRenameRequestedEvent{ target.selectedGameObject->GetID() });
|
||||
DrawMenuAction(MakeRenameSelectionAction(target), [&]() {
|
||||
ExecuteRenameSelection(context, target);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <size_t BufferCapacity>
|
||||
inline void HandleHierarchyBackgroundPrimaryClick(
|
||||
IEditorContext& context,
|
||||
const UI::InlineTextEditState<uint64_t, BufferCapacity>& renameState) {
|
||||
if (!ImGui::IsWindowHovered() || !ImGui::IsMouseDown(0) || ImGui::IsAnyItemHovered() || renameState.IsActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.GetSelectionManager().ClearSelection();
|
||||
}
|
||||
|
||||
inline void RequestHierarchyOptionsPopup(UI::DeferredPopupState& optionsPopup) {
|
||||
optionsPopup.RequestOpen();
|
||||
}
|
||||
|
||||
template <typename SortMode, typename SetSortModeFn>
|
||||
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
|
||||
|
||||
@@ -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 <string>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<AssetItemPtr>& itemContextMenu) {
|
||||
projectManager.SetSelectedIndex(index);
|
||||
itemContextMenu.RequestOpen(item);
|
||||
}
|
||||
|
||||
inline void DrawProjectItemContextPopup(IEditorContext& context, UI::TargetedPopupState<AssetItemPtr>& 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<BufferCapacit
|
||||
});
|
||||
}
|
||||
|
||||
template <size_t BufferCapacity>
|
||||
inline void DrawProjectEmptyContextPopup(
|
||||
UI::DeferredPopupState& emptyContextMenu,
|
||||
UI::TextInputPopupState<BufferCapacity>& createFolderDialog) {
|
||||
emptyContextMenu.ConsumeOpenRequest("EmptyContextMenu");
|
||||
|
||||
if (!UI::BeginPopup("EmptyContextMenu")) {
|
||||
return;
|
||||
}
|
||||
|
||||
DrawProjectEmptyContextActions(createFolderDialog);
|
||||
UI::EndPopup();
|
||||
}
|
||||
|
||||
template <size_t BufferCapacity>
|
||||
inline void DrawProjectCreateFolderDialog(IEditorContext& context, UI::TextInputPopupState<BufferCapacity>& createFolderDialog) {
|
||||
createFolderDialog.ConsumeOpenRequest("Create Folder");
|
||||
|
||||
@@ -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<EditorContext>();
|
||||
m_editorContext->SetProjectPath(projectPath);
|
||||
m_exitRequestedHandlerId = m_editorContext->GetEventBus().Subscribe<EditorExitRequestedEvent>(
|
||||
[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<Core::Layer>(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<EditorExitRequestedEvent>(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();
|
||||
@@ -26,54 +82,26 @@ bool Application::Initialize(HWND hwnd) {
|
||||
|
||||
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<EditorContext>();
|
||||
m_editorContext->SetProjectPath(exeDir);
|
||||
m_exitRequestedHandlerId = m_editorContext->GetEventBus().Subscribe<EditorExitRequestedEvent>(
|
||||
[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<Core::Layer>(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<EditorExitRequestedEvent>(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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -33,24 +33,7 @@ void ConsolePanel::Render() {
|
||||
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<int>(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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -88,20 +88,10 @@ void HierarchyPanel::Render() {
|
||||
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) {
|
||||
@@ -194,10 +169,7 @@ 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ private:
|
||||
|
||||
char m_searchBuffer[256] = "";
|
||||
UI::InlineTextEditState<uint64_t, 256> m_renameState;
|
||||
UI::DeferredPopupState m_optionsPopup;
|
||||
SortMode m_sortMode = SortMode::Name;
|
||||
uint64_t m_selectionHandlerId = 0;
|
||||
uint64_t m_renameRequestHandlerId = 0;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "Actions/MainMenuActionRouter.h"
|
||||
#include "MenuBar.h"
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "UI/UI.h"
|
||||
#include <imgui.h>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -18,6 +18,7 @@ private:
|
||||
|
||||
char m_searchBuffer[256] = "";
|
||||
UI::TextInputPopupState<256> m_createFolderDialog;
|
||||
UI::DeferredPopupState m_emptyContextMenu;
|
||||
UI::TargetedPopupState<AssetItemPtr> m_itemContextMenu;
|
||||
};
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ add_subdirectory(Rendering)
|
||||
add_subdirectory(rhi)
|
||||
add_subdirectory(resources)
|
||||
add_subdirectory(input)
|
||||
add_subdirectory(editor)
|
||||
|
||||
# ============================================================
|
||||
# Test Summary
|
||||
|
||||
36
tests/editor/CMakeLists.txt
Normal file
36
tests/editor/CMakeLists.txt
Normal file
@@ -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)
|
||||
224
tests/editor/test_action_routing.cpp
Normal file
224
tests/editor/test_action_routing.cpp
Normal file
@@ -0,0 +1,224 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Actions/EditActionRouter.h"
|
||||
#include "Commands/EntityCommands.h"
|
||||
#include "Commands/SceneCommands.h"
|
||||
#include "Core/EditorContext.h"
|
||||
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
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<int>(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<EntityRenameRequestedEvent>(
|
||||
[&](const EntityRenameRequestedEvent& event) {
|
||||
renameRequestedId = event.entityId;
|
||||
});
|
||||
EXPECT_TRUE(Actions::ExecuteRenameSelection(m_context, target));
|
||||
EXPECT_EQ(renameRequestedId, entity->GetID());
|
||||
m_context.GetEventBus().Unsubscribe<EntityRenameRequestedEvent>(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
|
||||
Reference in New Issue
Block a user