2026-03-26 23:52:05 +08:00
|
|
|
#include "Actions/HierarchyActionRouter.h"
|
2026-03-26 22:10:43 +08:00
|
|
|
#include "Actions/ActionRouting.h"
|
2026-03-26 21:18:33 +08:00
|
|
|
#include "Commands/EntityCommands.h"
|
2026-03-20 17:08:06 +08:00
|
|
|
#include "HierarchyPanel.h"
|
2026-03-25 15:51:27 +08:00
|
|
|
#include "Core/IEditorContext.h"
|
2026-03-25 16:39:15 +08:00
|
|
|
#include "Core/ISceneManager.h"
|
2026-03-25 16:41:21 +08:00
|
|
|
#include "Core/ISelectionManager.h"
|
|
|
|
|
#include "Core/EditorEvents.h"
|
|
|
|
|
#include "Core/EventBus.h"
|
2026-03-26 21:18:33 +08:00
|
|
|
#include "UI/UI.h"
|
2026-03-20 17:08:06 +08:00
|
|
|
#include <imgui.h>
|
|
|
|
|
|
2026-03-24 20:02:38 +08:00
|
|
|
namespace XCEngine {
|
|
|
|
|
namespace Editor {
|
2026-03-20 17:08:06 +08:00
|
|
|
|
|
|
|
|
HierarchyPanel::HierarchyPanel() : Panel("Hierarchy") {
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 00:08:46 +08:00
|
|
|
void HierarchyPanel::OnAttach() {
|
|
|
|
|
if (!m_context || m_selectionHandlerId || m_renameRequestHandlerId) {
|
|
|
|
|
return;
|
2026-03-25 16:41:21 +08:00
|
|
|
}
|
2026-03-25 15:51:27 +08:00
|
|
|
|
2026-03-25 16:41:21 +08:00
|
|
|
m_selectionHandlerId = m_context->GetEventBus().Subscribe<SelectionChangedEvent>(
|
|
|
|
|
[this](const SelectionChangedEvent& event) {
|
|
|
|
|
OnSelectionChanged(event);
|
|
|
|
|
}
|
|
|
|
|
);
|
2026-03-26 22:10:43 +08:00
|
|
|
m_renameRequestHandlerId = m_context->GetEventBus().Subscribe<EntityRenameRequestedEvent>(
|
|
|
|
|
[this](const EntityRenameRequestedEvent& event) {
|
|
|
|
|
OnRenameRequested(event);
|
|
|
|
|
}
|
|
|
|
|
);
|
2026-03-25 16:41:21 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 00:08:46 +08:00
|
|
|
void HierarchyPanel::OnDetach() {
|
|
|
|
|
if (!m_context) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_selectionHandlerId) {
|
|
|
|
|
m_context->GetEventBus().Unsubscribe<SelectionChangedEvent>(m_selectionHandlerId);
|
|
|
|
|
m_selectionHandlerId = 0;
|
|
|
|
|
}
|
|
|
|
|
if (m_renameRequestHandlerId) {
|
|
|
|
|
m_context->GetEventBus().Unsubscribe<EntityRenameRequestedEvent>(m_renameRequestHandlerId);
|
|
|
|
|
m_renameRequestHandlerId = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 16:41:21 +08:00
|
|
|
void HierarchyPanel::OnSelectionChanged(const SelectionChangedEvent& event) {
|
2026-03-26 21:30:46 +08:00
|
|
|
if (m_renameState.IsActive() && event.primarySelection != m_renameState.Item()) {
|
|
|
|
|
CancelRename();
|
|
|
|
|
}
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 22:10:43 +08:00
|
|
|
void HierarchyPanel::OnRenameRequested(const EntityRenameRequestedEvent& event) {
|
|
|
|
|
if (!m_context || event.entityId == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (auto* gameObject = m_context->GetSceneManager().GetEntity(event.entityId)) {
|
|
|
|
|
BeginRename(gameObject);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 17:08:06 +08:00
|
|
|
void HierarchyPanel::Render() {
|
2026-03-26 16:43:06 +08:00
|
|
|
UI::PanelWindowScope panel(m_name.c_str());
|
|
|
|
|
if (!panel.IsOpen()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 22:10:43 +08:00
|
|
|
Actions::ObserveFocusedActionRoute(*m_context, EditorActionRoute::Hierarchy);
|
|
|
|
|
|
2026-03-20 17:08:06 +08:00
|
|
|
RenderSearchBar();
|
|
|
|
|
std::string filter = m_searchBuffer;
|
2026-03-26 16:43:06 +08:00
|
|
|
|
|
|
|
|
UI::PanelContentScope content("EntityList");
|
|
|
|
|
if (!content.IsOpen()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 16:39:15 +08:00
|
|
|
auto& sceneManager = m_context->GetSceneManager();
|
2026-03-25 15:51:27 +08:00
|
|
|
auto rootEntities = sceneManager.GetRootEntities();
|
2026-03-25 12:30:05 +08:00
|
|
|
SortEntities(const_cast<std::vector<::XCEngine::Components::GameObject*>&>(rootEntities));
|
|
|
|
|
|
|
|
|
|
for (auto* gameObject : rootEntities) {
|
2026-03-25 01:23:08 +08:00
|
|
|
RenderEntity(gameObject, filter);
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseDown(0) && !ImGui::IsAnyItemHovered()) {
|
2026-03-26 21:30:46 +08:00
|
|
|
if (!m_renameState.IsActive()) {
|
2026-03-25 15:51:27 +08:00
|
|
|
m_context->GetSelectionManager().ClearSelection();
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 21:18:33 +08:00
|
|
|
if (UI::BeginPopupContextWindow("HierarchyContextMenu", ImGuiPopupFlags_MouseButtonRight)) {
|
2026-03-26 23:52:05 +08:00
|
|
|
Actions::DrawHierarchyCreateActions(*m_context, nullptr);
|
2026-03-26 21:18:33 +08:00
|
|
|
UI::EndPopup();
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::InvisibleButton("##DragTarget", ImVec2(-1, -1));
|
2026-03-27 00:30:11 +08:00
|
|
|
Actions::AcceptHierarchyEntityDropToRoot(*m_context);
|
2026-03-20 17:08:06 +08:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void HierarchyPanel::RenderSearchBar() {
|
2026-03-26 21:18:33 +08:00
|
|
|
UI::PanelToolbarScope toolbar("HierarchyToolbar", UI::StandardPanelToolbarHeight());
|
2026-03-26 16:43:06 +08:00
|
|
|
if (!toolbar.IsOpen()) {
|
|
|
|
|
return;
|
2026-03-25 12:30:05 +08:00
|
|
|
}
|
2026-03-26 16:43:06 +08:00
|
|
|
|
2026-03-26 21:18:33 +08:00
|
|
|
const float buttonWidth = UI::HierarchyOverflowButtonWidth();
|
|
|
|
|
UI::ToolbarSearchField(
|
|
|
|
|
"##Search",
|
|
|
|
|
"Search hierarchy",
|
|
|
|
|
m_searchBuffer,
|
|
|
|
|
sizeof(m_searchBuffer),
|
|
|
|
|
buttonWidth + UI::ToolbarSearchTrailingSpacing());
|
2026-03-25 12:30:05 +08:00
|
|
|
ImGui::SameLine();
|
2026-03-26 16:43:06 +08:00
|
|
|
if (UI::ToolbarButton("...", false, ImVec2(buttonWidth, 0.0f))) {
|
|
|
|
|
ImGui::OpenPopup("HierarchyOptions");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 21:18:33 +08:00
|
|
|
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)
|
|
|
|
|
};
|
2026-03-26 16:43:06 +08:00
|
|
|
|
2026-03-26 21:18:33 +08:00
|
|
|
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();
|
|
|
|
|
}
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-25 01:23:08 +08:00
|
|
|
void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject, const std::string& filter) {
|
|
|
|
|
if (!gameObject) return;
|
2026-03-20 17:08:06 +08:00
|
|
|
|
2026-03-25 01:23:08 +08:00
|
|
|
if (!filter.empty() && !PassesFilter(gameObject, filter)) {
|
2026-03-20 17:08:06 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 01:23:08 +08:00
|
|
|
ImGui::PushID(static_cast<int>(gameObject->GetID()));
|
2026-03-26 21:18:33 +08:00
|
|
|
|
2026-03-26 21:30:46 +08:00
|
|
|
if (m_renameState.IsEditing(gameObject->GetID())) {
|
|
|
|
|
if (m_renameState.ConsumeFocusRequest()) {
|
2026-03-20 17:08:06 +08:00
|
|
|
ImGui::SetKeyboardFocusHere();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextItemWidth(-1);
|
2026-03-26 21:30:46 +08:00
|
|
|
if (ImGui::InputText(
|
|
|
|
|
"##Rename",
|
|
|
|
|
m_renameState.Buffer(),
|
|
|
|
|
m_renameState.BufferSize(),
|
|
|
|
|
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) {
|
2026-03-26 21:18:33 +08:00
|
|
|
CommitRename();
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
2026-03-26 21:18:33 +08:00
|
|
|
|
|
|
|
|
if (ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
|
|
|
|
CancelRename();
|
|
|
|
|
} else if (!ImGui::IsItemActive() && ImGui::IsMouseClicked(0)) {
|
|
|
|
|
CommitRename();
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-03-26 21:18:33 +08:00
|
|
|
const UI::HierarchyNodeResult node = UI::DrawHierarchyNode(
|
|
|
|
|
(void*)gameObject->GetUUID(),
|
|
|
|
|
gameObject->GetName().c_str(),
|
|
|
|
|
m_context->GetSelectionManager().IsSelected(gameObject->GetID()),
|
|
|
|
|
gameObject->GetChildCount() == 0);
|
2026-03-20 17:08:06 +08:00
|
|
|
|
2026-03-26 21:18:33 +08:00
|
|
|
if (node.clicked) {
|
2026-03-27 00:30:11 +08:00
|
|
|
Actions::HandleHierarchySelectionClick(*m_context, gameObject->GetID(), ImGui::GetIO().KeyCtrl);
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 21:18:33 +08:00
|
|
|
if (node.doubleClicked) {
|
|
|
|
|
BeginRename(gameObject);
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 00:30:11 +08:00
|
|
|
Actions::BeginHierarchyEntityDrag(gameObject);
|
|
|
|
|
Actions::AcceptHierarchyEntityDrop(*m_context, gameObject);
|
2026-03-20 17:08:06 +08:00
|
|
|
|
2026-03-26 21:18:33 +08:00
|
|
|
if (UI::BeginPopupContextItem("EntityContextMenu")) {
|
2026-03-26 23:52:05 +08:00
|
|
|
Actions::DrawHierarchyContextActions(*m_context, gameObject);
|
2026-03-26 21:18:33 +08:00
|
|
|
UI::EndPopup();
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 21:18:33 +08:00
|
|
|
if (node.open) {
|
2026-03-25 01:23:08 +08:00
|
|
|
for (size_t i = 0; i < gameObject->GetChildCount(); i++) {
|
|
|
|
|
RenderEntity(gameObject->GetChild(i), filter);
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
2026-03-26 21:18:33 +08:00
|
|
|
UI::EndHierarchyNode();
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 21:18:33 +08:00
|
|
|
void HierarchyPanel::BeginRename(::XCEngine::Components::GameObject* gameObject) {
|
|
|
|
|
if (!gameObject) {
|
|
|
|
|
CancelRename();
|
|
|
|
|
return;
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
2026-03-26 21:18:33 +08:00
|
|
|
|
2026-03-26 21:30:46 +08:00
|
|
|
m_renameState.Begin(gameObject->GetID(), gameObject->GetName().c_str());
|
2026-03-26 21:18:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void HierarchyPanel::CommitRename() {
|
2026-03-26 21:30:46 +08:00
|
|
|
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());
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
2026-03-26 21:18:33 +08:00
|
|
|
|
|
|
|
|
CancelRename();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void HierarchyPanel::CancelRename() {
|
2026-03-26 21:30:46 +08:00
|
|
|
m_renameState.Cancel();
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-25 01:23:08 +08:00
|
|
|
bool HierarchyPanel::PassesFilter(::XCEngine::Components::GameObject* gameObject, const std::string& filter) {
|
|
|
|
|
if (!gameObject) return false;
|
2026-03-20 17:08:06 +08:00
|
|
|
|
2026-03-25 01:23:08 +08:00
|
|
|
if (gameObject->GetName().find(filter) != std::string::npos) {
|
2026-03-20 17:08:06 +08:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 01:23:08 +08:00
|
|
|
for (size_t i = 0; i < gameObject->GetChildCount(); i++) {
|
|
|
|
|
if (PassesFilter(gameObject->GetChild(i), filter)) {
|
2026-03-20 17:08:06 +08:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 12:30:05 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 18:38:01 +08:00
|
|
|
}
|
2026-03-24 20:02:38 +08:00
|
|
|
}
|