Refine editor action shell and add regression tests

This commit is contained in:
2026-03-27 12:06:24 +08:00
parent c33404767e
commit 4b9a63098e
21 changed files with 838 additions and 308 deletions

View File

@@ -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

View File

@@ -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);
});
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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");

View File

@@ -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();
@@ -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<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() {

View File

@@ -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;

View File

@@ -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<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);
}
}

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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;
};

View File

@@ -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();

View File

@@ -18,6 +18,7 @@ private:
char m_searchBuffer[256] = "";
UI::TextInputPopupState<256> m_createFolderDialog;
UI::DeferredPopupState m_emptyContextMenu;
UI::TargetedPopupState<AssetItemPtr> m_itemContextMenu;
};