514 lines
17 KiB
C++
514 lines
17 KiB
C++
#include "window.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <format>
|
|
#include <fstream>
|
|
#include <vector>
|
|
|
|
#include <imgui.h>
|
|
#include <imgui_internal.h>
|
|
|
|
#include "../log.hpp"
|
|
#include "../util/physfs.hpp"
|
|
#include "../util/preferences.hpp"
|
|
|
|
namespace game::developer
|
|
{
|
|
bool gIsDocumentUndoMenuRequested{};
|
|
bool gIsDocumentRedoMenuRequested{};
|
|
|
|
namespace window_detail
|
|
{
|
|
constexpr auto RECENT_FILES_MAX = 10;
|
|
constexpr auto CENTER_RATIO = 0.5f;
|
|
constexpr auto DEVELOPER_WINDOW_SIZE_RATIO = 0.75f;
|
|
constexpr auto DEVELOPER_WINDOW_FLAGS = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings;
|
|
constexpr auto EDITOR_CHILD_FLAGS = ImGuiWindowFlags_MenuBar;
|
|
constexpr auto DIALOGUE_SCHEMA_FILENAME = "dialogue.xml";
|
|
|
|
using Schema = resource::xml::Schema;
|
|
using Element = Schema::Element;
|
|
|
|
#define GAME_DEVELOPER_SCHEMA_FILES(X) \
|
|
X(CHARACTER, "character.xml", "Character", Element::CHARACTER) \
|
|
X(DIALOGUE, "dialogue.xml", "Dialogue", Element::DIALOGUE) \
|
|
X(ITEMS, "items.xml", "Items", Element::ITEM_SCHEMA) \
|
|
X(AREAS, "areas.xml", "Areas", Element::AREAS) \
|
|
X(MENU, "menu.xml", "Menu", Element::MENU) \
|
|
X(CURSOR, "cursor.xml", "Cursor", Element::CURSOR) \
|
|
X(PARTICLES, "particles.xml", "Particles", Element::PARTICLES) \
|
|
X(STRINGS, "strings.xml", "Strings", Element::STRINGS)
|
|
|
|
struct SchemaInfo
|
|
{
|
|
std::string_view filename{};
|
|
std::string_view label{};
|
|
Element::Type type{};
|
|
};
|
|
|
|
constexpr SchemaInfo SCHEMA_INFOS[]{
|
|
#define GAME_DEVELOPER_SCHEMA_INFO(SYMBOL, FILENAME, LABEL, TYPE) {.filename = FILENAME, .label = LABEL, .type = TYPE},
|
|
GAME_DEVELOPER_SCHEMA_FILES(GAME_DEVELOPER_SCHEMA_INFO)
|
|
#undef GAME_DEVELOPER_SCHEMA_INFO
|
|
};
|
|
|
|
std::filesystem::path path_normalized_get(const std::filesystem::path& path)
|
|
{
|
|
std::error_code error{};
|
|
auto normalized = std::filesystem::weakly_canonical(path, error);
|
|
if (!error) return normalized;
|
|
return std::filesystem::absolute(path, error).lexically_normal();
|
|
}
|
|
|
|
std::filesystem::path recent_file_path_get() { return util::preferences::path() / "recent.txt"; }
|
|
|
|
std::string document_path_label_get(const std::filesystem::path& path)
|
|
{
|
|
auto filename = path.filename();
|
|
if (filename.empty()) return path.string();
|
|
return filename.string();
|
|
}
|
|
|
|
bool shortcut_is_pressed(ImGuiKeyChord chord)
|
|
{
|
|
auto& io = ImGui::GetIO();
|
|
return !io.WantTextInput && !ImGui::IsAnyItemActive() &&
|
|
ImGui::Shortcut(chord, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused);
|
|
}
|
|
|
|
#undef GAME_DEVELOPER_SCHEMA_FILES
|
|
}
|
|
|
|
using namespace window_detail;
|
|
|
|
void Window::recent_files_load()
|
|
{
|
|
if (isRecentFilesLoaded) return;
|
|
isRecentFilesLoaded = true;
|
|
recentFiles.clear();
|
|
|
|
std::ifstream file(recent_file_path_get());
|
|
if (!file.is_open()) return;
|
|
|
|
std::string line{};
|
|
while (std::getline(file, line))
|
|
{
|
|
if (line.empty()) continue;
|
|
recentFiles.push_back(path_normalized_get(line));
|
|
if ((int)recentFiles.size() >= RECENT_FILES_MAX) break;
|
|
}
|
|
}
|
|
|
|
void Window::recent_files_save()
|
|
{
|
|
auto recentPath = recent_file_path_get();
|
|
if (recentPath.empty()) return;
|
|
std::error_code error{};
|
|
if (!recentPath.parent_path().empty()) std::filesystem::create_directories(recentPath.parent_path(), error);
|
|
|
|
std::ofstream file(recentPath, std::ios::trunc);
|
|
if (!file.is_open())
|
|
{
|
|
logger.warning(std::format("Failed to write {}", recentPath.string()));
|
|
return;
|
|
}
|
|
|
|
for (auto& path : recentFiles)
|
|
file << path.string() << '\n';
|
|
}
|
|
|
|
void Window::recent_file_push(const std::filesystem::path& path)
|
|
{
|
|
recent_files_load();
|
|
auto normalized = path_normalized_get(path);
|
|
if (normalized.empty()) return;
|
|
|
|
std::erase_if(recentFiles, [&](const auto& recent) { return recent == normalized; });
|
|
recentFiles.insert(recentFiles.begin(), normalized);
|
|
if ((int)recentFiles.size() > RECENT_FILES_MAX) recentFiles.resize(RECENT_FILES_MAX);
|
|
recent_files_save();
|
|
}
|
|
|
|
Document Window::document_load(const std::filesystem::path& path)
|
|
{
|
|
Document document{};
|
|
document.path = path_normalized_get(path);
|
|
|
|
for (auto& info : SCHEMA_INFOS)
|
|
{
|
|
Document::SchemaFile schemaFile{};
|
|
schemaFile.filename = info.filename;
|
|
schemaFile.label = info.label;
|
|
schemaFile.type = info.type;
|
|
schemaFile.path = document.path / schemaFile.filename;
|
|
schemaFile.isExists = std::filesystem::is_regular_file(schemaFile.path);
|
|
if (schemaFile.isExists)
|
|
{
|
|
schemaFile.isValid = schemaFile.schema.load(schemaFile.path);
|
|
auto* root = schemaFile.schema.root();
|
|
if (!root || root->type != schemaFile.type) schemaFile.isValid = false;
|
|
}
|
|
document.schemas.emplace_back(std::move(schemaFile));
|
|
}
|
|
|
|
return document;
|
|
}
|
|
|
|
void Window::folder_open(const std::filesystem::path& path)
|
|
{
|
|
auto normalizedPath = path_normalized_get(path);
|
|
std::error_code error{};
|
|
if (!std::filesystem::is_directory(normalizedPath, error))
|
|
{
|
|
logger.warning(std::format("Ignored {}", normalizedPath.string()));
|
|
return;
|
|
}
|
|
|
|
auto it = std::ranges::find_if(documents, [&](const auto& document) { return document.path == normalizedPath; });
|
|
if (it != documents.end())
|
|
{
|
|
*it = document_load(normalizedPath);
|
|
it->history_clear();
|
|
it->isDirty = false;
|
|
selectedDocument = (int)std::distance(documents.begin(), it);
|
|
logger.info(std::format("Opened {}", normalizedPath.string()));
|
|
toasts.push(std::format("Opened {}", normalizedPath.string()));
|
|
}
|
|
else
|
|
{
|
|
documents.push_back(document_load(normalizedPath));
|
|
selectedDocument = (int)documents.size() - 1;
|
|
logger.info(std::format("Opened {}", normalizedPath.string()));
|
|
toasts.push(std::format("Opened {}", normalizedPath.string()));
|
|
}
|
|
recent_file_push(normalizedPath);
|
|
}
|
|
|
|
void Window::save(int index)
|
|
{
|
|
if (index < 0 || index >= (int)documents.size()) return;
|
|
auto& document = documents[index];
|
|
auto* dialogue = document.dialogue_schema_get();
|
|
if (!dialogue || !dialogue->isExists)
|
|
{
|
|
toasts.push("No dialogue.xml to save");
|
|
return;
|
|
}
|
|
|
|
logger.info(std::format("Wrote to {}", dialogue->path.string()));
|
|
dialogue->schema.serialize(dialogue->path);
|
|
documents[index].isDirty = false;
|
|
toasts.push(std::format("Wrote to {}", dialogue->path.string()));
|
|
recent_file_push(documents[index].path);
|
|
}
|
|
|
|
void Window::export_to(int index, const std::filesystem::path& path)
|
|
{
|
|
if (index < 0 || index >= (int)documents.size()) return;
|
|
if (documents[index].isDirty) save(index);
|
|
auto destination = path_normalized_get(path);
|
|
if (util::physfs::archive_write(documents[index].path, destination))
|
|
toasts.push(std::format("Exported {}", destination.string()));
|
|
else
|
|
toasts.push(std::format("Failed to export {}", destination.string()));
|
|
}
|
|
|
|
void Window::close(int index)
|
|
{
|
|
if (index < 0 || index >= (int)documents.size()) return;
|
|
auto label = document_path_label_get(documents[index].path);
|
|
documents.erase(documents.begin() + index);
|
|
selectedDocument = documents.empty() ? 0 : std::clamp(index, 0, (int)documents.size() - 1);
|
|
toasts.push(std::format("Closed {}", label));
|
|
}
|
|
|
|
void Window::undo(int index)
|
|
{
|
|
if (index < 0 || index >= (int)documents.size()) return;
|
|
auto& document = documents[index];
|
|
if (!document.can_undo()) return;
|
|
|
|
auto message = document.undo_apply();
|
|
if (!message.empty()) toasts.push(std::format("Undo: {}", message));
|
|
}
|
|
|
|
void Window::redo(int index)
|
|
{
|
|
if (index < 0 || index >= (int)documents.size()) return;
|
|
auto& document = documents[index];
|
|
if (!document.can_redo()) return;
|
|
|
|
auto message = document.redo_apply();
|
|
if (!message.empty()) toasts.push(std::format("Redo: {}", message));
|
|
}
|
|
|
|
void Window::dropped_file_open(const char* path)
|
|
{
|
|
if (!path) return;
|
|
folder_open(path);
|
|
}
|
|
|
|
void Window::editor_shortcuts_update()
|
|
{
|
|
if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_O)) dialog.folder_open(util::Dialog::SCHEMA_FOLDER_OPEN);
|
|
if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z))
|
|
redo(selectedDocument);
|
|
else if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_Z))
|
|
undo(selectedDocument);
|
|
if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_S))
|
|
dialog.file_save(util::Dialog::DOCUMENT_EXPORT);
|
|
else if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_S))
|
|
save(selectedDocument);
|
|
if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_W)) close(selectedDocument);
|
|
}
|
|
|
|
void Window::dialog_selection_update()
|
|
{
|
|
if (dialog.is_selected(util::Dialog::SCHEMA_FOLDER_OPEN))
|
|
{
|
|
folder_open(dialog.path);
|
|
dialog.reset();
|
|
}
|
|
else if (dialog.is_selected(util::Dialog::DOCUMENT_EXPORT))
|
|
{
|
|
export_to(selectedDocument, dialog.path);
|
|
dialog.reset();
|
|
}
|
|
}
|
|
|
|
void Window::editor_menu_update()
|
|
{
|
|
if (!ImGui::BeginMenuBar()) return;
|
|
|
|
if (ImGui::BeginMenu("File"))
|
|
{
|
|
if (ImGui::MenuItem("Open Folder", "Ctrl+O")) dialog.folder_open(util::Dialog::SCHEMA_FOLDER_OPEN);
|
|
if (ImGui::BeginMenu("Open Recent", !recentFiles.empty()))
|
|
{
|
|
for (int recentIndex = 0; recentIndex < (int)recentFiles.size(); ++recentIndex)
|
|
{
|
|
ImGui::PushID(recentIndex);
|
|
auto label = recentFiles[recentIndex].string();
|
|
if (ImGui::MenuItem(label.c_str())) folder_open(recentFiles[recentIndex]);
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
if (ImGui::MenuItem("Save", "Ctrl+S", false, selectedDocument >= 0 && selectedDocument < (int)documents.size()))
|
|
save(selectedDocument);
|
|
if (ImGui::MenuItem("Export", "Ctrl+Shift+S", false,
|
|
selectedDocument >= 0 && selectedDocument < (int)documents.size()))
|
|
dialog.file_save(util::Dialog::DOCUMENT_EXPORT);
|
|
if (ImGui::MenuItem("Close", "Ctrl+W", false, selectedDocument >= 0 && selectedDocument < (int)documents.size()))
|
|
close(selectedDocument);
|
|
ImGui::EndMenu();
|
|
}
|
|
if (ImGui::BeginMenu("Edit"))
|
|
{
|
|
auto isCanUndo =
|
|
selectedDocument >= 0 && selectedDocument < (int)documents.size() && documents[selectedDocument].can_undo();
|
|
auto isCanRedo =
|
|
selectedDocument >= 0 && selectedDocument < (int)documents.size() && documents[selectedDocument].can_redo();
|
|
if (ImGui::MenuItem("Undo", "Ctrl+Z", false, isCanUndo)) undo(selectedDocument);
|
|
if (ImGui::MenuItem("Redo", "Ctrl+Shift+Z", false, isCanRedo)) redo(selectedDocument);
|
|
ImGui::EndMenu();
|
|
}
|
|
ImGui::EndMenuBar();
|
|
}
|
|
|
|
void Window::editor_documents_update(Resources& resources)
|
|
{
|
|
if (documents.empty() || !ImGui::BeginTabBar("schemas", ImGuiTabBarFlags_Reorderable)) return;
|
|
|
|
auto closeRequest = -1;
|
|
for (int i = 0; i < (int)documents.size(); ++i)
|
|
{
|
|
ImGui::PushID(i);
|
|
auto label = document_path_label_get(documents[i].path) + (documents[i].isDirty ? " *" : "");
|
|
label += std::format("###{}", documents[i].path.string());
|
|
|
|
auto isOpen = true;
|
|
auto isTabOpen = ImGui::BeginTabItem(label.c_str(), &isOpen);
|
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal))
|
|
ImGui::SetTooltip("%s", documents[i].path.string().c_str());
|
|
|
|
if (isTabOpen)
|
|
{
|
|
selectedDocument = i;
|
|
if (ImGui::BeginTabBar("##Document Tabs"))
|
|
{
|
|
if (ImGui::BeginTabItem("Dialogue"))
|
|
{
|
|
document_dialogue_update(resources, documents[i]);
|
|
ImGui::EndTabItem();
|
|
}
|
|
ImGui::EndTabBar();
|
|
}
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (!isOpen) closeRequest = i;
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndTabBar();
|
|
if (closeRequest >= 0) close(closeRequest);
|
|
}
|
|
|
|
void Window::document_dialogue_update(Resources& resources, Document& document)
|
|
{
|
|
auto* dialogue = document.dialogue_schema_get();
|
|
if (!dialogue || !dialogue->isExists)
|
|
{
|
|
ImGui::TextUnformatted("No dialogue.xml found.");
|
|
return;
|
|
}
|
|
if (!dialogue->isValid)
|
|
{
|
|
ImGui::TextUnformatted("dialogue.xml is not valid.");
|
|
return;
|
|
}
|
|
|
|
if (ImGui::IsAnyItemActive()) document.edit_snapshot_begin();
|
|
std::vector<Editor::LoadedSchema> loadedSchemas{};
|
|
for (auto& schemaFile : document.schemas)
|
|
{
|
|
if (!schemaFile.isValid) continue;
|
|
loadedSchemas.push_back({.path = &schemaFile.path, .schema = &schemaFile.schema});
|
|
}
|
|
|
|
auto before = dialogue->schema;
|
|
if (document.editor.update(resources, dialogue->schema, dialogue->path, dialog, loadedSchemas))
|
|
{
|
|
before = document.edit_snapshot_take_or(std::move(before));
|
|
if (before.element_serialize(0) != dialogue->schema.element_serialize(0))
|
|
{
|
|
auto message = Document::change_message_get(before, dialogue->schema);
|
|
document.snapshot_push(std::move(before), message);
|
|
}
|
|
else
|
|
document.isDirty = true;
|
|
}
|
|
else if (!ImGui::IsAnyItemActive())
|
|
document.edit_snapshot_clear();
|
|
}
|
|
|
|
void Window::editor_update(Resources& resources)
|
|
{
|
|
if (ImGui::BeginChild("##Editor", ImVec2(), ImGuiChildFlags_None, EDITOR_CHILD_FLAGS))
|
|
{
|
|
editor_menu_update();
|
|
|
|
if (ImGui::BeginPopupContextWindow("developer-context",
|
|
ImGuiPopupFlags_MouseButtonRight | ImGuiPopupFlags_NoOpenOverItems))
|
|
{
|
|
auto isCanUndo =
|
|
selectedDocument >= 0 && selectedDocument < (int)documents.size() && documents[selectedDocument].can_undo();
|
|
auto isCanRedo =
|
|
selectedDocument >= 0 && selectedDocument < (int)documents.size() && documents[selectedDocument].can_redo();
|
|
if (ImGui::MenuItem("Undo", "Ctrl+Z", false, isCanUndo)) isContextUndoRequested = true;
|
|
if (ImGui::MenuItem("Redo", "Ctrl+Shift+Z", false, isCanRedo)) isContextRedoRequested = true;
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
editor_documents_update(resources);
|
|
}
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
void Window::update(Resources& resources, SDL_Window* window, state::Play* play)
|
|
{
|
|
dialog.window_set(window);
|
|
auto* settingsRoot = resources.settings.root();
|
|
|
|
if (!settingsRoot || !settingsRoot->isDeveloperConsoleEnabled)
|
|
{
|
|
resources.isDeveloper = false;
|
|
isDeveloperOpen = false;
|
|
action = {};
|
|
console.action = {};
|
|
return;
|
|
}
|
|
|
|
if (!resources.isDeveloper && shortcut_is_pressed(ImGuiKey_GraveAccent))
|
|
{
|
|
resources.isDeveloper = true;
|
|
if (selectedTab == Tab::CONSOLE) console.focus_request();
|
|
}
|
|
|
|
if (!resources.isDeveloper)
|
|
{
|
|
isDeveloperOpen = false;
|
|
return;
|
|
}
|
|
|
|
recent_files_load();
|
|
|
|
auto* viewport = ImGui::GetMainViewport();
|
|
auto windowSize =
|
|
ImVec2(viewport->WorkSize.x * DEVELOPER_WINDOW_SIZE_RATIO, viewport->WorkSize.y * DEVELOPER_WINDOW_SIZE_RATIO);
|
|
auto windowPosition = ImVec2(viewport->WorkPos.x + (viewport->WorkSize.x * CENTER_RATIO),
|
|
viewport->WorkPos.y + (viewport->WorkSize.y * CENTER_RATIO));
|
|
ImGui::SetNextWindowPos(windowPosition, ImGuiCond_Appearing, ImVec2(CENTER_RATIO, CENTER_RATIO));
|
|
ImGui::SetNextWindowSize(windowSize, ImGuiCond_Appearing);
|
|
|
|
if (!isDeveloperOpen)
|
|
{
|
|
ImGui::SetNextWindowFocus();
|
|
if (selectedTab == Tab::CONSOLE) console.focus_request();
|
|
}
|
|
isDeveloperOpen = true;
|
|
|
|
isContextUndoRequested = false;
|
|
isContextRedoRequested = false;
|
|
action = {};
|
|
console.action = {};
|
|
gIsDocumentUndoMenuRequested = false;
|
|
gIsDocumentRedoMenuRequested = false;
|
|
|
|
if (selectedTab == Tab::EDITOR) editor_shortcuts_update();
|
|
dialog_selection_update();
|
|
|
|
auto gameStyle = ImGui::GetStyle();
|
|
ImGui::GetStyle() = ImGuiStyle{};
|
|
ImGui::StyleColorsDark();
|
|
ImGui::PushFont(resources.font.get(), resource::Font::SMALL);
|
|
|
|
if (ImGui::Begin("Developer", &resources.isDeveloper, DEVELOPER_WINDOW_FLAGS))
|
|
{
|
|
auto isFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
|
|
if (isFocused && (shortcut_is_pressed(ImGuiKey_GraveAccent) || shortcut_is_pressed(ImGuiKey_Escape)))
|
|
resources.isDeveloper = false;
|
|
|
|
if (ImGui::BeginTabBar("##Developer Tabs"))
|
|
{
|
|
auto isWasConsole = selectedTab == Tab::CONSOLE;
|
|
if (ImGui::BeginTabItem("Console"))
|
|
{
|
|
selectedTab = Tab::CONSOLE;
|
|
if (!isWasConsole) console.focus_request();
|
|
console.update(resources, play);
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Editor"))
|
|
{
|
|
selectedTab = Tab::EDITOR;
|
|
editor_update(resources);
|
|
ImGui::EndTabItem();
|
|
}
|
|
ImGui::EndTabBar();
|
|
}
|
|
}
|
|
ImGui::End();
|
|
|
|
if (gIsDocumentUndoMenuRequested) isContextUndoRequested = true;
|
|
if (gIsDocumentRedoMenuRequested) isContextRedoRequested = true;
|
|
action = console.action;
|
|
if (isContextUndoRequested) undo(selectedDocument);
|
|
if (isContextRedoRequested) redo(selectedDocument);
|
|
|
|
toasts.update();
|
|
|
|
ImGui::PopFont();
|
|
ImGui::GetStyle() = gameStyle;
|
|
}
|
|
}
|