完善HierarchyPanel功能:右键菜单、拖拽排序、搜索过滤、重命名、快捷键

右键菜单:
- Create: Empty Object, Camera, Light, Cube, Sphere, Plane
- Rename (F2), Delete (Del)
- Copy (Ctrl+C), Paste (Ctrl+V), Duplicate (Ctrl+D)

拖拽排序:
- 支持拖拽实体到另一个实体下成为子节点
- 自动防止循环父子关系

搜索过滤:
- 顶部搜索框实时过滤实体

双击重命名:
- 双击实体名称进入编辑模式

键盘快捷键:
- Delete: 删除选中实体
- F2: 重命名
- Ctrl+C/V/D: 复制/粘贴/复制
This commit is contained in:
2026-03-12 17:54:59 +08:00
parent c1473d2d39
commit 4bcd1055dd
4 changed files with 511 additions and 78 deletions

View File

@@ -1,5 +1,185 @@
#include "SceneManager.h"
#include <algorithm>
namespace UI {
EntityID SceneManager::CreateEntity(const std::string& name, EntityID parent) {
EntityID id = m_nextEntityId++;
Entity entity;
entity.id = id;
entity.name = name;
entity.parent = parent;
m_entities[id] = std::move(entity);
if (parent != INVALID_ENTITY) {
m_entities[parent].children.push_back(id);
} else {
m_rootEntities.push_back(id);
}
OnEntityCreated.Invoke(id);
return id;
}
void SceneManager::DeleteEntity(EntityID id) {
auto it = m_entities.find(id);
if (it == m_entities.end()) return;
Entity& entity = it->second;
std::vector<EntityID> childrenToDelete = entity.children;
for (EntityID childId : childrenToDelete) {
DeleteEntity(childId);
}
if (entity.parent != INVALID_ENTITY) {
auto* parent = GetEntity(entity.parent);
if (parent) {
auto& siblings = parent->children;
siblings.erase(std::remove(siblings.begin(), siblings.end(), id), siblings.end());
}
} else {
m_rootEntities.erase(std::remove(m_rootEntities.begin(), m_rootEntities.end(), id), m_rootEntities.end());
}
if (SelectionManager::Get().GetSelectedEntity() == id) {
SelectionManager::Get().ClearSelection();
}
m_entities.erase(it);
OnEntityDeleted.Invoke(id);
}
ClipboardData SceneManager::CopyEntityRecursive(const Entity* entity) {
ClipboardData data;
data.name = entity->name;
for (const auto& comp : entity->components) {
if (auto* transform = dynamic_cast<const TransformComponent*>(comp.get())) {
auto newComp = std::make_unique<TransformComponent>();
memcpy(newComp->position, transform->position, sizeof(transform->position));
memcpy(newComp->rotation, transform->rotation, sizeof(transform->rotation));
memcpy(newComp->scale, transform->scale, sizeof(transform->scale));
data.components.push_back(std::move(newComp));
}
else if (auto* meshRenderer = dynamic_cast<const MeshRendererComponent*>(comp.get())) {
auto newComp = std::make_unique<MeshRendererComponent>();
newComp->materialName = meshRenderer->materialName;
newComp->meshName = meshRenderer->meshName;
data.components.push_back(std::move(newComp));
}
}
for (EntityID childId : entity->children) {
const Entity* child = GetEntity(childId);
if (child) {
data.children.push_back(CopyEntityRecursive(child));
}
}
return data;
}
void SceneManager::CopyEntity(EntityID id) {
const Entity* entity = GetEntity(id);
if (!entity) return;
m_clipboard = CopyEntityRecursive(entity);
}
EntityID SceneManager::PasteEntityRecursive(const ClipboardData& data, EntityID parent) {
EntityID newId = CreateEntity(data.name, parent);
Entity* newEntity = GetEntity(newId);
if (newEntity) {
newEntity->components.clear();
for (const auto& comp : data.components) {
if (auto* transform = dynamic_cast<const TransformComponent*>(comp.get())) {
auto newComp = std::make_unique<TransformComponent>();
memcpy(newComp->position, transform->position, sizeof(transform->position));
memcpy(newComp->rotation, transform->rotation, sizeof(transform->rotation));
memcpy(newComp->scale, transform->scale, sizeof(transform->scale));
newEntity->components.push_back(std::move(newComp));
}
else if (auto* meshRenderer = dynamic_cast<const MeshRendererComponent*>(comp.get())) {
auto newComp = std::make_unique<MeshRendererComponent>();
newComp->materialName = meshRenderer->materialName;
newComp->meshName = meshRenderer->meshName;
newEntity->components.push_back(std::move(newComp));
}
}
}
for (const auto& childData : data.children) {
PasteEntityRecursive(childData, newId);
}
return newId;
}
EntityID SceneManager::PasteEntity(EntityID parent) {
if (!m_clipboard) return INVALID_ENTITY;
return PasteEntityRecursive(*m_clipboard, parent);
}
EntityID SceneManager::DuplicateEntity(EntityID id) {
CopyEntity(id);
const Entity* entity = GetEntity(id);
if (!entity) return INVALID_ENTITY;
return PasteEntity(entity->parent);
}
void SceneManager::MoveEntity(EntityID id, EntityID newParent) {
Entity* entity = GetEntity(id);
if (!entity || id == newParent) return;
if (entity->parent != INVALID_ENTITY) {
Entity* oldParent = GetEntity(entity->parent);
if (oldParent) {
auto& siblings = oldParent->children;
siblings.erase(std::remove(siblings.begin(), siblings.end(), id), siblings.end());
}
} else {
m_rootEntities.erase(std::remove(m_rootEntities.begin(), m_rootEntities.end(), id), m_rootEntities.end());
}
entity->parent = newParent;
if (newParent != INVALID_ENTITY) {
Entity* newParentEntity = GetEntity(newParent);
if (newParentEntity) {
newParentEntity->children.push_back(id);
}
} else {
m_rootEntities.push_back(id);
}
OnEntityChanged.Invoke(id);
}
void SceneManager::CreateDemoScene() {
m_entities.clear();
m_rootEntities.clear();
m_nextEntityId = 1;
m_clipboard.reset();
EntityID camera = CreateEntity("Main Camera");
GetEntity(camera)->AddComponent<TransformComponent>();
EntityID light = CreateEntity("Directional Light");
EntityID cube = CreateEntity("Cube");
GetEntity(cube)->AddComponent<TransformComponent>();
GetEntity(cube)->AddComponent<MeshRendererComponent>()->meshName = "Cube Mesh";
EntityID sphere = CreateEntity("Sphere");
GetEntity(sphere)->AddComponent<TransformComponent>();
GetEntity(sphere)->AddComponent<MeshRendererComponent>()->meshName = "Sphere Mesh";
EntityID player = CreateEntity("Player");
EntityID weapon = CreateEntity("Weapon", player);
OnSceneChanged.Invoke();
}
}

View File

@@ -4,9 +4,17 @@
#include "SelectionManager.h"
#include <unordered_map>
#include <vector>
#include <memory>
#include <optional>
namespace UI {
struct ClipboardData {
std::string name;
std::vector<std::unique_ptr<Component>> components;
std::vector<ClipboardData> children;
};
class SceneManager {
public:
static SceneManager& Get() {
@@ -14,23 +22,7 @@ public:
return instance;
}
EntityID CreateEntity(const std::string& name, EntityID parent = INVALID_ENTITY) {
EntityID id = m_nextEntityId++;
Entity entity;
entity.id = id;
entity.name = name;
entity.parent = parent;
m_entities[id] = std::move(entity);
if (parent != INVALID_ENTITY) {
m_entities[parent].children.push_back(id);
} else {
m_rootEntities.push_back(id);
}
OnEntityCreated.Invoke(id);
return id;
}
EntityID CreateEntity(const std::string& name, EntityID parent = INVALID_ENTITY);
Entity* GetEntity(EntityID id) {
auto it = m_entities.find(id);
@@ -52,68 +44,43 @@ public:
return m_rootEntities;
}
void DeleteEntity(EntityID id) {
auto it = m_entities.find(id);
if (it == m_entities.end()) return;
Entity& entity = it->second;
for (EntityID childId : entity.children) {
DeleteEntity(childId);
void DeleteEntity(EntityID id);
void RenameEntity(EntityID id, const std::string& newName) {
auto* entity = GetEntity(id);
if (entity) {
entity->name = newName;
OnEntityChanged.Invoke(id);
}
if (entity.parent != INVALID_ENTITY) {
auto* parent = GetEntity(entity.parent);
if (parent) {
auto& siblings = parent->children;
siblings.erase(std::remove(siblings.begin(), siblings.end(), id), siblings.end());
}
} else {
m_rootEntities.erase(std::remove(m_rootEntities.begin(), m_rootEntities.end(), id), m_rootEntities.end());
}
if (SelectionManager::Get().GetSelectedEntity() == id) {
SelectionManager::Get().ClearSelection();
}
m_entities.erase(it);
OnEntityDeleted.Invoke(id);
}
void CreateDemoScene() {
m_entities.clear();
m_rootEntities.clear();
m_nextEntityId = 1;
EntityID camera = CreateEntity("Main Camera");
GetEntity(camera)->AddComponent<TransformComponent>();
EntityID light = CreateEntity("Directional Light");
EntityID cube = CreateEntity("Cube");
GetEntity(cube)->AddComponent<TransformComponent>();
GetEntity(cube)->AddComponent<MeshRendererComponent>()->meshName = "Cube Mesh";
EntityID sphere = CreateEntity("Sphere");
GetEntity(sphere)->AddComponent<TransformComponent>();
GetEntity(sphere)->AddComponent<MeshRendererComponent>()->meshName = "Sphere Mesh";
EntityID player = CreateEntity("Player");
EntityID weapon = CreateEntity("Weapon", player);
OnSceneChanged.Invoke();
}
void CopyEntity(EntityID id);
EntityID PasteEntity(EntityID parent = INVALID_ENTITY);
EntityID DuplicateEntity(EntityID id);
void MoveEntity(EntityID id, EntityID newParent);
void CreateDemoScene();
bool HasClipboardData() const { return m_clipboard.has_value(); }
Event<EntityID> OnEntityCreated;
Event<EntityID> OnEntityDeleted;
Event<EntityID> OnEntityChanged;
Event<> OnSceneChanged;
private:
SceneManager() = default;
ClipboardData CopyEntityRecursive(const Entity* entity);
EntityID PasteEntityRecursive(const ClipboardData& data, EntityID parent);
EntityID m_nextEntityId = 1;
std::unordered_map<EntityID, Entity> m_entities;
std::vector<EntityID> m_rootEntities;
std::optional<ClipboardData> m_clipboard;
};
}

View File

@@ -2,6 +2,7 @@
#include "Managers/SceneManager.h"
#include "Managers/SelectionManager.h"
#include <imgui.h>
#include <cstring>
namespace UI {
@@ -19,18 +20,52 @@ HierarchyPanel::~HierarchyPanel() {
void HierarchyPanel::Render() {
ImGui::Begin(m_name.c_str(), &m_isOpen, ImGuiWindowFlags_None);
for (EntityID id : SceneManager::Get().GetRootEntities()) {
RenderEntity(id);
RenderSearchBar();
ImGui::Separator();
HandleKeyboardShortcuts();
std::string filter = m_searchBuffer;
if (ImGui::BeginPopupContextWindow("HierarchyContextMenu", ImGuiPopupFlags_MouseButtonRight | ImGuiPopupFlags_NoOpenOverItems)) {
RenderCreateMenu(INVALID_ENTITY);
ImGui::EndPopup();
}
ImGui::BeginChild("EntityList");
for (EntityID id : SceneManager::Get().GetRootEntities()) {
RenderEntity(id, filter);
}
if (ImGui::IsWindowHovered() && ImGui::IsMouseDown(0) && !ImGui::IsAnyItemHovered()) {
if (!m_renaming) {
SelectionManager::Get().ClearSelection();
}
}
ImGui::EndChild();
ImGui::End();
}
void HierarchyPanel::RenderEntity(EntityID id) {
void HierarchyPanel::RenderSearchBar() {
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##Search", "Search...", m_searchBuffer, sizeof(m_searchBuffer));
}
void HierarchyPanel::RenderEntity(EntityID id, const std::string& filter) {
auto& sceneManager = SceneManager::Get();
const Entity* entity = sceneManager.GetEntity(id);
Entity* entity = sceneManager.GetEntity(id);
if (!entity) return;
if (!filter.empty() && !PassesFilter(id, filter)) {
return;
}
ImGui::PushID(static_cast<int>(id));
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth;
if (entity->children.empty()) {
@@ -41,18 +76,256 @@ void HierarchyPanel::RenderEntity(EntityID id) {
flags |= ImGuiTreeNodeFlags_Selected;
}
bool isOpen = ImGui::TreeNodeEx(entity->name.c_str(), flags);
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
SelectionManager::Get().SetSelectedEntity(id);
if (m_renaming && m_renamingEntity == id) {
if (m_renameJustStarted) {
ImGui::SetKeyboardFocusHere();
m_renameJustStarted = false;
}
ImGui::SetNextItemWidth(-1);
if (ImGui::InputText("##Rename", m_renameBuffer, sizeof(m_renameBuffer), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) {
if (strlen(m_renameBuffer) > 0) {
sceneManager.RenameEntity(id, m_renameBuffer);
}
m_renaming = false;
m_renamingEntity = INVALID_ENTITY;
}
if (!ImGui::IsItemActive() && ImGui::IsMouseClicked(0)) {
if (strlen(m_renameBuffer) > 0) {
sceneManager.RenameEntity(id, m_renameBuffer);
}
m_renaming = false;
m_renamingEntity = INVALID_ENTITY;
}
} else {
bool isOpen = ImGui::TreeNodeEx(entity->name.c_str(), flags);
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
SelectionManager::Get().SetSelectedEntity(id);
}
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
m_renaming = true;
m_renamingEntity = id;
strcpy_s(m_renameBuffer, entity->name.c_str());
m_renameJustStarted = true;
}
HandleDragDrop(id);
if (ImGui::BeginPopupContextItem("EntityContextMenu")) {
RenderContextMenu(id);
ImGui::EndPopup();
}
if (isOpen) {
for (EntityID childId : entity->children) {
RenderEntity(childId, filter);
}
ImGui::TreePop();
}
}
if (isOpen) {
for (EntityID childId : entity->children) {
RenderEntity(childId);
ImGui::PopID();
}
void HierarchyPanel::RenderContextMenu(EntityID id) {
auto& sceneManager = SceneManager::Get();
auto& selectionManager = SelectionManager::Get();
if (ImGui::BeginMenu("Create")) {
RenderCreateMenu(id);
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::MenuItem("Rename", "F2")) {
const Entity* entity = sceneManager.GetEntity(id);
if (entity) {
m_renaming = true;
m_renamingEntity = id;
strcpy_s(m_renameBuffer, entity->name.c_str());
m_renameJustStarted = true;
}
}
if (ImGui::MenuItem("Delete", "Delete")) {
sceneManager.DeleteEntity(id);
}
ImGui::Separator();
if (ImGui::MenuItem("Copy", "Ctrl+C")) {
sceneManager.CopyEntity(id);
}
if (ImGui::MenuItem("Paste", "Ctrl+V", false, sceneManager.HasClipboardData())) {
sceneManager.PasteEntity(id);
}
if (ImGui::MenuItem("Duplicate", "Ctrl+D")) {
EntityID newId = sceneManager.DuplicateEntity(id);
if (newId != INVALID_ENTITY) {
selectionManager.SetSelectedEntity(newId);
}
ImGui::TreePop();
}
}
void HierarchyPanel::RenderCreateMenu(EntityID parent) {
auto& sceneManager = SceneManager::Get();
auto& selectionManager = SelectionManager::Get();
if (ImGui::MenuItem("Empty Object")) {
EntityID newId = sceneManager.CreateEntity("GameObject", parent);
selectionManager.SetSelectedEntity(newId);
}
ImGui::Separator();
if (ImGui::MenuItem("Camera")) {
EntityID newId = sceneManager.CreateEntity("Camera", parent);
sceneManager.GetEntity(newId)->AddComponent<TransformComponent>();
selectionManager.SetSelectedEntity(newId);
}
if (ImGui::MenuItem("Light")) {
EntityID newId = sceneManager.CreateEntity("Light", parent);
selectionManager.SetSelectedEntity(newId);
}
ImGui::Separator();
if (ImGui::MenuItem("Cube")) {
EntityID newId = sceneManager.CreateEntity("Cube", parent);
sceneManager.GetEntity(newId)->AddComponent<TransformComponent>();
sceneManager.GetEntity(newId)->AddComponent<MeshRendererComponent>()->meshName = "Cube";
selectionManager.SetSelectedEntity(newId);
}
if (ImGui::MenuItem("Sphere")) {
EntityID newId = sceneManager.CreateEntity("Sphere", parent);
sceneManager.GetEntity(newId)->AddComponent<TransformComponent>();
sceneManager.GetEntity(newId)->AddComponent<MeshRendererComponent>()->meshName = "Sphere";
selectionManager.SetSelectedEntity(newId);
}
if (ImGui::MenuItem("Plane")) {
EntityID newId = sceneManager.CreateEntity("Plane", parent);
sceneManager.GetEntity(newId)->AddComponent<TransformComponent>();
sceneManager.GetEntity(newId)->AddComponent<MeshRendererComponent>()->meshName = "Plane";
selectionManager.SetSelectedEntity(newId);
}
}
void HierarchyPanel::HandleDragDrop(EntityID id) {
auto& sceneManager = SceneManager::Get();
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
m_dragSource = id;
ImGui::SetDragDropPayload("ENTITY_ID", &id, sizeof(EntityID));
const Entity* entity = sceneManager.GetEntity(id);
if (entity) {
ImGui::Text("%s", entity->name.c_str());
}
ImGui::EndDragDropSource();
}
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ENTITY_ID")) {
EntityID sourceId = *(const EntityID*)payload->Data;
if (sourceId != id && sourceId != INVALID_ENTITY) {
const Entity* targetEntity = sceneManager.GetEntity(id);
const Entity* sourceEntity = sceneManager.GetEntity(sourceId);
bool isValidMove = true;
EntityID checkParent = targetEntity ? targetEntity->parent : INVALID_ENTITY;
while (checkParent != INVALID_ENTITY) {
if (checkParent == sourceId) {
isValidMove = false;
break;
}
const Entity* parentEntity = sceneManager.GetEntity(checkParent);
checkParent = parentEntity ? parentEntity->parent : INVALID_ENTITY;
}
if (isValidMove && sourceEntity && sourceEntity->parent != id) {
sceneManager.MoveEntity(sourceId, id);
}
}
}
ImGui::EndDragDropTarget();
}
}
void HierarchyPanel::HandleKeyboardShortcuts() {
auto& sceneManager = SceneManager::Get();
auto& selectionManager = SelectionManager::Get();
EntityID selectedId = selectionManager.GetSelectedEntity();
if (ImGui::IsWindowFocused()) {
if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
if (selectedId != INVALID_ENTITY) {
sceneManager.DeleteEntity(selectedId);
}
}
if (ImGui::IsKeyPressed(ImGuiKey_F2)) {
if (selectedId != INVALID_ENTITY) {
const Entity* entity = sceneManager.GetEntity(selectedId);
if (entity) {
m_renaming = true;
m_renamingEntity = selectedId;
strcpy_s(m_renameBuffer, entity->name.c_str());
m_renameJustStarted = true;
}
}
}
ImGuiIO& io = ImGui::GetIO();
if (io.KeyCtrl) {
if (ImGui::IsKeyPressed(ImGuiKey_C)) {
if (selectedId != INVALID_ENTITY) {
sceneManager.CopyEntity(selectedId);
}
}
if (ImGui::IsKeyPressed(ImGuiKey_V)) {
if (sceneManager.HasClipboardData()) {
sceneManager.PasteEntity(selectedId);
}
}
if (ImGui::IsKeyPressed(ImGuiKey_D)) {
if (selectedId != INVALID_ENTITY) {
EntityID newId = sceneManager.DuplicateEntity(selectedId);
if (newId != INVALID_ENTITY) {
selectionManager.SetSelectedEntity(newId);
}
}
}
}
}
}
bool HierarchyPanel::PassesFilter(EntityID id, const std::string& filter) {
auto& sceneManager = SceneManager::Get();
const Entity* entity = sceneManager.GetEntity(id);
if (!entity) return false;
if (entity->name.find(filter) != std::string::npos) {
return true;
}
for (EntityID childId : entity->children) {
if (PassesFilter(childId, filter)) {
return true;
}
}
return false;
}
}

View File

@@ -14,9 +14,22 @@ public:
void Render() override;
private:
void RenderEntity(EntityID id);
void RenderSearchBar();
void RenderEntity(EntityID id, const std::string& filter);
void RenderContextMenu(EntityID id);
void RenderCreateMenu(EntityID parent);
void HandleDragDrop(EntityID id);
void HandleKeyboardShortcuts();
bool PassesFilter(EntityID id, const std::string& filter);
Event<EntityID>::HandlerID m_selectionHandlerId = 0;
char m_searchBuffer[256] = "";
bool m_renaming = false;
EntityID m_renamingEntity = INVALID_ENTITY;
char m_renameBuffer[256] = "";
bool m_renameJustStarted = false;
EntityID m_dragSource = INVALID_ENTITY;
};
}