#include "Actions/EditorActions.h" #include "Commands/EntityCommands.h" #include "HierarchyPanel.h" #include "Core/IEditorContext.h" #include "Core/ISceneManager.h" #include "Core/ISelectionManager.h" #include "Core/IUndoManager.h" #include "Core/EditorEvents.h" #include "Core/EventBus.h" #include "UI/UI.h" #include #include namespace XCEngine { namespace Editor { HierarchyPanel::HierarchyPanel() : Panel("Hierarchy") { } HierarchyPanel::~HierarchyPanel() { if (m_context) { m_context->GetEventBus().Unsubscribe(m_selectionHandlerId); } } void HierarchyPanel::OnAttach() { m_selectionHandlerId = m_context->GetEventBus().Subscribe( [this](const SelectionChangedEvent& event) { OnSelectionChanged(event); } ); } void HierarchyPanel::OnSelectionChanged(const SelectionChangedEvent& event) { if (m_renameState.IsActive() && event.primarySelection != m_renameState.Item()) { CancelRename(); } } void HierarchyPanel::Render() { UI::PanelWindowScope panel(m_name.c_str()); if (!panel.IsOpen()) { return; } RenderSearchBar(); HandleKeyboardShortcuts(); std::string filter = m_searchBuffer; UI::PanelContentScope content("EntityList"); if (!content.IsOpen()) { return; } auto& sceneManager = m_context->GetSceneManager(); auto rootEntities = sceneManager.GetRootEntities(); SortEntities(const_cast&>(rootEntities)); 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)) { RenderCreateMenu(nullptr); UI::EndPopup(); } ImGui::InvisibleButton("##DragTarget", ImVec2(-1, -1)); if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ENTITY_PTR")) { ::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data; if (sourceGameObject && sourceGameObject->GetParent() != nullptr) { Commands::ReparentEntityPreserveWorldTransform(*m_context, sourceGameObject, 0); } } ImGui::EndDragDropTarget(); } } void HierarchyPanel::RenderSearchBar() { UI::PanelToolbarScope toolbar("HierarchyToolbar", UI::StandardPanelToolbarHeight()); if (!toolbar.IsOpen()) { return; } const float buttonWidth = UI::HierarchyOverflowButtonWidth(); UI::ToolbarSearchField( "##Search", "Search hierarchy", m_searchBuffer, sizeof(m_searchBuffer), buttonWidth + UI::ToolbarSearchTrailingSpacing()); ImGui::SameLine(); if (UI::ToolbarButton("...", false, ImVec2(buttonWidth, 0.0f))) { ImGui::OpenPopup("HierarchyOptions"); } 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; } }); UI::EndPopup(); } } void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject, const std::string& filter) { if (!gameObject) return; if (!filter.empty() && !PassesFilter(gameObject, filter)) { return; } ImGui::PushID(static_cast(gameObject->GetID())); if (m_renameState.IsEditing(gameObject->GetID())) { if (m_renameState.ConsumeFocusRequest()) { ImGui::SetKeyboardFocusHere(); } ImGui::SetNextItemWidth(-1); if (ImGui::InputText( "##Rename", m_renameState.Buffer(), m_renameState.BufferSize(), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) { CommitRename(); } if (ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape)) { CancelRename(); } else if (!ImGui::IsItemActive() && ImGui::IsMouseClicked(0)) { CommitRename(); } } else { const UI::HierarchyNodeResult node = UI::DrawHierarchyNode( (void*)gameObject->GetUUID(), gameObject->GetName().c_str(), m_context->GetSelectionManager().IsSelected(gameObject->GetID()), gameObject->GetChildCount() == 0); if (node.clicked) { ImGuiIO& io = ImGui::GetIO(); if (io.KeyCtrl) { if (!m_context->GetSelectionManager().IsSelected(gameObject->GetID())) { m_context->GetSelectionManager().AddToSelection(gameObject->GetID()); } } else { m_context->GetSelectionManager().SetSelectedEntity(gameObject->GetID()); } } if (node.doubleClicked) { BeginRename(gameObject); } HandleDragDrop(gameObject); if (UI::BeginPopupContextItem("EntityContextMenu")) { RenderContextMenu(gameObject); UI::EndPopup(); } if (node.open) { for (size_t i = 0; i < gameObject->GetChildCount(); i++) { RenderEntity(gameObject->GetChild(i), filter); } UI::EndHierarchyNode(); } } ImGui::PopID(); } void HierarchyPanel::RenderContextMenu(::XCEngine::Components::GameObject* gameObject) { if (UI::DrawMenuScope("Create", [&]() { RenderCreateMenu(gameObject); })) { } Actions::DrawMenuAction(Actions::MakeCreateChildEntityAction(gameObject), [&]() { Commands::CreateEmptyEntity(*m_context, gameObject, "Create Child", "GameObject"); }); Actions::DrawMenuSeparator(); Actions::DrawMenuAction(Actions::MakeDetachEntityAction(gameObject), [&]() { Commands::DetachEntity(*m_context, gameObject); }); Actions::DrawMenuAction(Actions::MakeRenameEntityAction(gameObject), [&]() { BeginRename(gameObject); }); Actions::DrawMenuAction(Actions::MakeDeleteEntityAction(gameObject), [&]() { Commands::DeleteEntity(*m_context, gameObject->GetID()); }); Actions::DrawMenuSeparator(); Actions::DrawMenuAction(Actions::MakeCopyEntityAction(gameObject), [&]() { Commands::CopyEntity(*m_context, gameObject->GetID()); }); Actions::DrawMenuAction(Actions::MakePasteEntityAction(*m_context), [&]() { Commands::PasteEntity(*m_context, gameObject->GetID()); }); Actions::DrawMenuAction(Actions::MakeDuplicateEntityAction(gameObject), [&]() { Commands::DuplicateEntity(*m_context, gameObject->GetID()); }); } void HierarchyPanel::RenderCreateMenu(::XCEngine::Components::GameObject* parent) { Actions::DrawMenuAction(Actions::MakeCreateEmptyEntityAction(), [&]() { Commands::CreateEmptyEntity(*m_context, parent, "Create Entity", "GameObject"); }); Actions::DrawMenuSeparator(); Actions::DrawMenuAction(Actions::MakeCreateCameraEntityAction(), [&]() { Commands::CreateCameraEntity(*m_context, parent); }); Actions::DrawMenuAction(Actions::MakeCreateLightEntityAction(), [&]() { Commands::CreateLightEntity(*m_context, parent); }); Actions::DrawMenuSeparator(); Actions::DrawMenuAction(Actions::MakeCreateCubeEntityAction(), [&]() { Commands::CreateEmptyEntity(*m_context, parent, "Create Cube", "Cube"); }); Actions::DrawMenuAction(Actions::MakeCreateSphereEntityAction(), [&]() { Commands::CreateEmptyEntity(*m_context, parent, "Create Sphere", "Sphere"); }); Actions::DrawMenuAction(Actions::MakeCreatePlaneEntityAction(), [&]() { Commands::CreateEmptyEntity(*m_context, parent, "Create Plane", "Plane"); }); } void HierarchyPanel::BeginRename(::XCEngine::Components::GameObject* gameObject) { if (!gameObject) { CancelRename(); return; } m_renameState.Begin(gameObject->GetID(), gameObject->GetName().c_str()); } void HierarchyPanel::CommitRename() { if (!m_renameState.IsActive()) { return; } const uint64_t entityId = m_renameState.Item(); if (!m_renameState.Empty() && m_context->GetSceneManager().GetEntity(entityId)) { Commands::RenameEntity(*m_context, entityId, m_renameState.Buffer()); } CancelRename(); } void HierarchyPanel::CancelRename() { m_renameState.Cancel(); } void HierarchyPanel::HandleDragDrop(::XCEngine::Components::GameObject* gameObject) { if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { ImGui::SetDragDropPayload("ENTITY_PTR", &gameObject, sizeof(::XCEngine::Components::GameObject*)); ImGui::Text("%s", gameObject->GetName().c_str()); ImGui::EndDragDropSource(); } if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ENTITY_PTR")) { ::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data; if (sourceGameObject != gameObject && Commands::CanReparentEntity(sourceGameObject, gameObject)) { Commands::ReparentEntityPreserveWorldTransform(*m_context, sourceGameObject, gameObject->GetID()); } } ImGui::EndDragDropTarget(); } } void HierarchyPanel::HandleKeyboardShortcuts() { auto& sceneManager = m_context->GetSceneManager(); auto& selectionManager = m_context->GetSelectionManager(); ::XCEngine::Components::GameObject* selectedGameObject = sceneManager.GetEntity(selectionManager.GetSelectedEntity()); if (ImGui::IsWindowFocused()) { Actions::HandleShortcut(Actions::MakeDeleteEntityAction(selectedGameObject), [&]() { Commands::DeleteEntity(*m_context, selectedGameObject->GetID()); }); Actions::HandleShortcut(Actions::MakeRenameEntityAction(selectedGameObject), [&]() { BeginRename(selectedGameObject); }); Actions::HandleShortcut(Actions::MakeCopyEntityAction(selectedGameObject), [&]() { Commands::CopyEntity(*m_context, selectedGameObject->GetID()); }); Actions::HandleShortcut(Actions::MakePasteEntityAction(*m_context), [&]() { Commands::PasteEntity(*m_context, selectedGameObject ? selectedGameObject->GetID() : 0); }); Actions::HandleShortcut(Actions::MakeDuplicateEntityAction(selectedGameObject), [&]() { Commands::DuplicateEntity(*m_context, selectedGameObject->GetID()); }); } } bool HierarchyPanel::PassesFilter(::XCEngine::Components::GameObject* gameObject, const std::string& filter) { if (!gameObject) return false; if (gameObject->GetName().find(filter) != std::string::npos) { return true; } for (size_t i = 0; i < gameObject->GetChildCount(); i++) { if (PassesFilter(gameObject->GetChild(i), filter)) { return true; } } return false; } void HierarchyPanel::SortEntities(std::vector<::XCEngine::Components::GameObject*>& entities) { switch (m_sortMode) { case SortMode::Name: std::sort(entities.begin(), entities.end(), [](::XCEngine::Components::GameObject* a, ::XCEngine::Components::GameObject* b) { return a->GetName() < b->GetName(); }); break; case SortMode::ComponentCount: std::sort(entities.begin(), entities.end(), [](::XCEngine::Components::GameObject* a, ::XCEngine::Components::GameObject* b) { return a->GetComponents<::XCEngine::Components::Component>().size() > b->GetComponents<::XCEngine::Components::Component>().size(); }); break; case SortMode::TransformFirst: std::sort(entities.begin(), entities.end(), [](::XCEngine::Components::GameObject* a, ::XCEngine::Components::GameObject* b) { bool aHasTransform = a->GetComponent<::XCEngine::Components::TransformComponent>() != nullptr; bool bHasTransform = b->GetComponent<::XCEngine::Components::TransformComponent>() != nullptr; if (aHasTransform != bHasTransform) { return aHasTransform; } return a->GetName() < b->GetName(); }); break; } } } }