Files
XCEngine/editor/src/panels/HierarchyPanel.cpp

367 lines
13 KiB
C++

#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 <imgui.h>
#include <cstring>
namespace XCEngine {
namespace Editor {
HierarchyPanel::HierarchyPanel() : Panel("Hierarchy") {
}
HierarchyPanel::~HierarchyPanel() {
if (m_context) {
m_context->GetEventBus().Unsubscribe<SelectionChangedEvent>(m_selectionHandlerId);
}
}
void HierarchyPanel::OnAttach() {
m_selectionHandlerId = m_context->GetEventBus().Subscribe<SelectionChangedEvent>(
[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<std::vector<::XCEngine::Components::GameObject*>&>(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<int>(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;
}
}
}
}