diff --git a/include/Engine/Editor/EditorGUI.h b/include/Engine/Editor/EditorGUI.h index 77218e9d..20946e36 100644 --- a/include/Engine/Editor/EditorGUI.h +++ b/include/Engine/Editor/EditorGUI.h @@ -29,12 +29,14 @@ public: typedef std::function OnEntitySelectedCallback_t; void SetEntitySelectedCallback(OnEntitySelectedCallback_t f) { m_OnEntitySelected = f; } // Called when the user means to import an entity file. - // Expects an EntityWrapper of the newly created entity in return. - typedef std::function OnEntityImport_t; + // @param EntityWrapper The entity to parent the imported entity to. The entity will be imported into the world of this entity. + // @param boost::filesystem::path The path to the entity to import + // @return EntityWrapper The newly created entity + typedef std::function OnEntityImport_t; void SetEntityImportCallback(OnEntityImport_t f) { m_OnEntityImport = f; } // Called when the user means to save an entity to file. - // Expects a bool indicating whether the save was successful or not in return. - typedef std::function OnEntitySave_t; + // Permitted to throw exceptions on save failure. + typedef std::function OnEntitySave_t; void SetEntitySaveCallback(OnEntitySave_t f) { m_OnEntitySave = f; } // Called when the user means to create a new entity. // @param EntityWrapper The parent of the entity to be created @@ -47,13 +49,20 @@ public: // Called when the user means to attach a new component to an entity. typedef std::function OnComponentAttach_t; void SetComponentAttachCallback(OnComponentAttach_t f) { m_OnComponentAttach = f; } - + // Called when the user means to delete a component off an entity. + typedef std::function OnComponentDelete_t; + void SetComponentDeleteCallback(OnComponentDelete_t f) { m_OnComponentDelete = f; } private: EventBroker* m_EventBroker; + // Config variables + const boost::filesystem::path m_DefaultEntityPath = boost::filesystem::path("Schema") / boost::filesystem::path("Entities"); + // State variables EntityWrapper m_CurrentSelection = EntityWrapper::Invalid; + std::unordered_map m_EntityFiles; + std::string m_LastErrorMessage; // Callbacks OnEntitySelectedCallback_t m_OnEntitySelected = nullptr; @@ -62,7 +71,17 @@ private: OnEntityCreate_t m_OnEntityCreate = nullptr; OnEntityDelete_t m_OnEntityDelete = nullptr; OnComponentAttach_t m_OnComponentAttach = nullptr; + OnComponentDelete_t m_OnComponentDelete = nullptr; + + // Utility functions + boost::filesystem::path fileOpenDialog(); + boost::filesystem::path fileSaveDialog(); + + // Entity file handling methods + void entityImport(World* world); + void entitySave(EntityWrapper entity); + // UI drawing methods void drawMenu(); void drawEntities(World* world); void drawEntitiesRecursive(World* world, EntityID parent); @@ -78,6 +97,10 @@ private: void drawComponentField_double(ComponentWrapper &c, const ComponentInfo::Field_t &field); void drawComponentField_bool(ComponentWrapper &c, const ComponentInfo::Field_t &field); void drawComponentField_string(ComponentWrapper &c, const ComponentInfo::Field_t &field); + void drawModals(); + + // Custom UI elements + bool createDeleteButton(const std::string& componentType); }; #endif \ No newline at end of file diff --git a/include/Engine/Editor/EditorSystem.h b/include/Engine/Editor/EditorSystem.h index ae3502c3..7998a26d 100644 --- a/include/Engine/Editor/EditorSystem.h +++ b/include/Engine/Editor/EditorSystem.h @@ -8,6 +8,7 @@ #include "../Core/ResourceManager.h" #include "../Core/EntityFilePreprocessor.h" #include "../Core/EntityFileParser.h" +#include "../Core/EntityFileWriter.h" #include "EditorGUI.h" #include "EditorStats.h" @@ -31,5 +32,12 @@ private: EditorGUI* m_EditorGUI; EditorStats* m_EditorStats; + // Utility functions + EntityWrapper importEntity(EntityWrapper parent, boost::filesystem::path filePath); + + // GUI callbacks void OnEntitySelected(EntityWrapper entity); + void OnEntitySave(EntityWrapper entity, boost::filesystem::path filePath); + void OnComponentAttach(EntityWrapper entity, const std::string& componentType); + void OnComponentDelete(EntityWrapper entity, const std::string& componentType); }; \ No newline at end of file diff --git a/src/Engine/Editor/EditorGUI.cpp b/src/Engine/Editor/EditorGUI.cpp index 388016a6..00c8b07c 100644 --- a/src/Engine/Editor/EditorGUI.cpp +++ b/src/Engine/Editor/EditorGUI.cpp @@ -28,9 +28,16 @@ void EditorGUI::drawEntities(World* world) } float buttonWidth = (ImGui::GetContentRegionAvailWidth() - 10.f) / 3.f ; - ImGui::Button("Create", ImVec2(buttonWidth, 0)); + if (ImGui::Button("Create", ImVec2(buttonWidth, 0))) { + if (m_OnEntityCreate != nullptr) { + EntityWrapper newEntity = m_OnEntityCreate(EntityWrapper(world, EntityID_Invalid)); + SelectEntity(newEntity); + } + } ImGui::SameLine(0.f, 5.f); - ImGui::Button("Import", ImVec2(buttonWidth, 0)); + if (ImGui::Button("Import", ImVec2(buttonWidth, 0))) { + entityImport(world); + } ImGui::SameLine(0.f, 5.f); ImGui::Button("Reference", ImVec2(buttonWidth, 0)); @@ -38,6 +45,8 @@ void EditorGUI::drawEntities(World* world) drawEntitiesRecursive(world, EntityID_Invalid); + // Draw any potential modals before ending this scope + drawModals(); ImGui::End(); } @@ -83,28 +92,29 @@ bool EditorGUI::drawEntityNode(EntityWrapper entity) // } //} - ImGui::SetNextTreeNodeOpened(true, ImGuiSetCond_Once); - std::string nodeTitle; + // Compose title + std::stringstream nodeTitle; const std::string& entityName = entity.World->GetName(entity); if (!entityName.empty()) { - nodeTitle = entityName; + nodeTitle << entityName; } else { - nodeTitle = std::string("#") + std::to_string(entity.ID); + nodeTitle << "#" << entity.ID; } - if (ImGui::TreeNode(nodeTitle.c_str())) { + if (m_EntityFiles.count(entity) == 1) { + nodeTitle << " (" << m_EntityFiles.at(entity).filename().string() << ")"; + } + + ImGui::SetNextTreeNodeOpened(true, ImGuiSetCond_Once); + if (ImGui::TreeNode(nodeTitle.str().c_str())) { //if (m_UIDraggingEntity != EntityID_Invalid && ImGui::IsItemHoveredRect() && ImGui::IsMouseReleased(0)) { // LOG_DEBUG("Changed parent of %i to %i", m_UIDraggingEntity, entity); // changeParent(m_UIDraggingEntity, entity); // m_UIDraggingEntity = EntityID_Invalid; //} - if (ImGui::BeginPopupContextItem("item context menu")) { - if (ImGui::Button("Add")) { - if (m_OnEntityCreate != nullptr) { - EntityWrapper newEntity = m_OnEntityCreate(EntityWrapper(entity.World, EntityID_Invalid)); - ImGui::CloseCurrentPopup(); - SelectEntity(newEntity); - } + if (ImGui::BeginPopupContextItem("entity context menu")) { + if (ImGui::Button("Save")) { + entitySave(entity); } ImGui::SameLine(); if (ImGui::Button("Delete")) { @@ -116,6 +126,7 @@ bool EditorGUI::drawEntityNode(EntityWrapper entity) SelectEntity(EntityWrapper::Invalid); } } + drawModals(); ImGui::EndPopup(); } return true; @@ -170,14 +181,21 @@ void EditorGUI::drawComponents(EntityWrapper entity) if (!entity.HasComponent(componentType)) { continue; } - // TODO: Add delete button here - drawComponent(entity, pool->ComponentInfo()); + // Handle deletion with early out + if (createDeleteButton(componentType)) { + if (m_OnComponentDelete != nullptr) { + m_OnComponentDelete(entity, componentType); + continue; + } + } + // Draw the actual component node + drawComponentNode(entity, pool->ComponentInfo()); } ImGui::End(); } -bool EditorGUI::drawComponent(EntityWrapper entity, const ComponentInfo& ci) +bool EditorGUI::drawComponentNode(EntityWrapper entity, const ComponentInfo& ci) { if (!ImGui::CollapsingHeader(ci.Name.c_str(), nullptr, true, true)) { return false; @@ -330,3 +348,111 @@ void EditorGUI::drawComponentField_string(ComponentWrapper &c, const ComponentIn // TODO: Handle drag and drop of files } +void EditorGUI::drawModals() +{ + if (ImGui::BeginPopupModal("Import failed", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Entity import failed. Check console for more information.\n\n"); + ImGui::SetCursorPosX(ImGui::GetContentRegionAvailWidth() - 120); + if (ImGui::Button("OK", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + if (ImGui::BeginPopupModal("Save failed", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Entity save failed on an exception.\nMessage: %s\n\n", m_LastErrorMessage.c_str()); + ImGui::SetCursorPosX(ImGui::GetContentRegionAvailWidth() - 120); + if (ImGui::Button("OK", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +bool EditorGUI::createDeleteButton(const std::string& componentType) +{ + float width = ImGui::GetContentRegionAvailWidth(); + ImGuiWindow* window = ImGui::GetCurrentWindow(); + auto pos = ImGui::GetCursorScreenPos() + ImVec2(width - 14.f, 1); + ImRect bb = ImRect(pos, pos + ImVec2(14.f, 14.f)); + std::string idString = "#DELETE"; + idString += componentType; + ImGuiID id = window->GetID(idString.c_str()); + bool hovered; + bool held; + bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held); + //ImU32 col = window->Color((held && hovered) ? ImGuiCol_CloseButtonActive : hovered ? ImGuiCol_CloseButtonHovered : ImGuiCol_CloseButton); + ImU32 col = window->Color((held && hovered) ? ImGuiCol_CloseButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + window->DrawList->AddCircleFilled(bb.GetCenter(), 7.f, col, 16); + return pressed; +} + +boost::filesystem::path EditorGUI::fileOpenDialog() +{ + namespace bfs = boost::filesystem; + nfdchar_t* outPath = nullptr; + nfdresult_t result = NFD_OpenDialog("xml", bfs::absolute(m_DefaultEntityPath).string().c_str(), &outPath); + + if (result == NFD_ERROR) { + LOG_ERROR("NFD Error: %s", NFD_GetError()); + return bfs::path(); + } else if (result == NFD_CANCEL) { + return bfs::path(); + } else { + return bfs::absolute(outPath); + } +} + +boost::filesystem::path EditorGUI::fileSaveDialog() +{ + namespace bfs = boost::filesystem; + nfdchar_t* outPath = nullptr; + nfdresult_t result = NFD_SaveDialog("xml", bfs::absolute(m_DefaultEntityPath).string().c_str(), &outPath); + + if (result == NFD_ERROR) { + LOG_ERROR("NFD Error: %s", NFD_GetError()); + return bfs::path(); + } else if (result == NFD_CANCEL) { + return bfs::path(); + } else { + return bfs::absolute(outPath); + } +} + +void EditorGUI::entityImport(World* world) +{ + boost::filesystem::path filePath = fileOpenDialog(); + if (filePath.empty()) { + return; + } + + EntityWrapper entity = m_OnEntityImport(EntityWrapper(world, EntityID_Invalid), filePath); + if (entity.Valid()) { + m_EntityFiles[entity] = filePath; + SelectEntity(entity); + } else { + ImGui::OpenPopup("Import failed"); + } +} + +void EditorGUI::entitySave(EntityWrapper entity) +{ + boost::filesystem::path filePath; + if (m_EntityFiles.count(entity) == 1) { + filePath = m_EntityFiles.at(entity); + } else { + filePath = fileSaveDialog(); + } + + if (filePath.empty()) { + return; + } + + try { + m_OnEntitySave(entity, filePath); + m_EntityFiles[entity] = filePath; + } catch (const std::exception& e) { + m_LastErrorMessage = e.what(); + ImGui::OpenPopup("Save failed"); + } +} diff --git a/src/Engine/Editor/EditorSystem.cpp b/src/Engine/Editor/EditorSystem.cpp index 0a96ab9b..09cc52de 100644 --- a/src/Engine/Editor/EditorSystem.cpp +++ b/src/Engine/Editor/EditorSystem.cpp @@ -12,12 +12,7 @@ EditorSystem::EditorSystem(EventBroker* eventBroker, IRenderer* renderer, Render m_EditorWorldSystemPipeline->AddSystem(0); m_EditorWorldSystemPipeline->AddSystem(1, m_Renderer, m_RenderFrame); - auto widgetEntityFile = ResourceManager::Load("Schema/Entities/EditorWidget.xml"); - EntityFilePreprocessor fpp(widgetEntityFile); - fpp.RegisterComponents(m_EditorWorld); - EntityFileParser fp(widgetEntityFile); - EntityID widgetID = fp.MergeEntities(m_EditorWorld); - m_Widget = EntityWrapper(m_EditorWorld, widgetID); + m_Widget = importEntity(EntityWrapper(m_EditorWorld, EntityID_Invalid), "Schema/Entities/EditorWidget.xml"); m_Camera = EntityWrapper(m_EditorWorld, m_EditorWorld->CreateEntity()); m_EditorWorld->AttachComponent(m_Camera.ID, "Transform"); @@ -26,6 +21,10 @@ EditorSystem::EditorSystem(EventBroker* eventBroker, IRenderer* renderer, Render m_EditorGUI = new EditorGUI(m_EventBroker); m_EditorGUI->SetEntitySelectedCallback(std::bind(&EditorSystem::OnEntitySelected, this, std::placeholders::_1)); + m_EditorGUI->SetEntityImportCallback(std::bind(&EditorSystem::importEntity, this, std::placeholders::_1, std::placeholders::_2)); + m_EditorGUI->SetEntitySaveCallback(std::bind(&EditorSystem::OnEntitySave, this, std::placeholders::_1, std::placeholders::_2)); + m_EditorGUI->SetComponentAttachCallback(std::bind(&EditorSystem::OnComponentAttach, this, std::placeholders::_1, std::placeholders::_2)); + m_EditorGUI->SetComponentDeleteCallback(std::bind(&EditorSystem::OnComponentDelete, this, std::placeholders::_1, std::placeholders::_2)); m_EditorStats = new EditorStats(); @@ -59,3 +58,38 @@ void EditorSystem::OnEntitySelected(EntityWrapper entity) { m_Widget["Transform"]["Position"] = Transform::AbsolutePosition(entity.World, entity.ID); } + +void EditorSystem::OnEntitySave(EntityWrapper entity, boost::filesystem::path filePath) +{ + EntityFileWriter writer(filePath); + writer.WriteEntity(entity.World, entity.ID); +} + +void EditorSystem::OnComponentAttach(EntityWrapper entity, const std::string& componentType) +{ + entity.World->AttachComponent(entity.ID, componentType); +} + +void EditorSystem::OnComponentDelete(EntityWrapper entity, const std::string& componentType) +{ + entity.World->DeleteComponent(entity.ID, componentType); +} + +EntityWrapper EditorSystem::importEntity(EntityWrapper parent, boost::filesystem::path filePath) +{ + if (parent.World == nullptr) { + LOG_ERROR("Tried to import entity \"%s\" into null world!", filePath.string().c_str()); + return EntityWrapper::Invalid; + } + + try { + auto entityFile = ResourceManager::Load(filePath.string()); + EntityFilePreprocessor fpp(entityFile); + fpp.RegisterComponents(parent.World); + EntityFileParser fp(entityFile); + EntityID newEntity = fp.MergeEntities(parent.World, parent.ID); + return EntityWrapper(parent.World, newEntity); + } catch (const std::exception&) { + return EntityWrapper::Invalid; + } +} \ No newline at end of file