From 87c2db2a774300b4d6b6745108312c023e1463f5 Mon Sep 17 00:00:00 2001 From: shweet Date: Wed, 22 Oct 2025 21:27:11 -0400 Subject: [PATCH] Refactoring; structs for popups/multi-selects, additional popups in places --- .vscode/settings.json | 8 -- src/animations.cpp | 72 +++++------ src/animations.h | 10 +- src/anm2.cpp | 139 ++++++++++++++------- src/anm2.h | 20 +++- src/dockspace.cpp | 10 +- src/document.cpp | 58 +++++++-- src/document.h | 17 ++- src/document_manager.cpp | 20 +--- src/documents.cpp | 20 +--- src/documents.h | 2 + src/events.cpp | 45 +++---- src/events.h | 4 +- src/frame_properties.cpp | 2 + src/imgui.cpp | 94 +++++++++++++-- src/imgui.h | 46 ++++++- src/layers.cpp | 127 +++++++++++++++----- src/layers.h | 13 +- src/nulls.cpp | 42 +++---- src/nulls.h | 8 +- src/onionskin.cpp | 6 +- src/settings.h | 2 + src/spritesheets.cpp | 67 +++++------ src/spritesheets.h | 10 +- src/state.cpp | 3 +- src/taskbar.cpp | 39 ++---- src/taskbar.h | 7 +- src/timeline.cpp | 253 ++++++++++++++++++++++++++++++++++----- src/timeline.h | 14 ++- src/tools.cpp | 2 +- src/types.h | 44 +++++++ 31 files changed, 849 insertions(+), 355 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 73ffdc7..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "C_Cpp.formatting": "clangFormat", - "editor.formatOnSave": true, - "clang-format.style": "file", - "clangd.arguments": [ - "--compile-commands-dir=build" - ] -} \ No newline at end of file diff --git a/src/animations.cpp b/src/animations.cpp index f41f35a..73e7baf 100644 --- a/src/animations.cpp +++ b/src/animations.cpp @@ -3,33 +3,27 @@ #include #include -#include "imgui.h" - -using namespace anm2ed::document_manager; +using namespace anm2ed::document; using namespace anm2ed::settings; using namespace anm2ed::resources; using namespace anm2ed::types; namespace anm2ed::animations { - void Animations::update(DocumentManager& manager, Settings& settings, Resources& resources) + void Animations::update(Document& document, int& documentIndex, Settings& settings, Resources& resources) { - auto document = manager.get(); - auto& anm2 = document->anm2; - auto& reference = document->reference; - auto& selection = document->selectedAnimations; - storage.UserData = &selection; - storage.AdapterSetItemSelected = imgui::external_storage_set; + auto& anm2 = document.anm2; + auto& reference = document.reference; + auto& selection = document.selectedAnimations; + storage.user_data_set(&selection); if (ImGui::Begin("Animations", &settings.windowIsAnimations)) { - auto childSize = imgui::size_with_footer_get(); + auto childSize = imgui::size_without_footer_get(); if (ImGui::BeginChild("##Animations Child", childSize, ImGuiChildFlags_Borders)) { - ImGuiMultiSelectIO* io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, selection.size(), - anm2.animations.items.size()); - storage.ApplyRequests(io); + storage.begin(anm2.animations.items.size()); for (auto [i, animation] : std::views::enumerate(anm2.animations.items)) { @@ -47,7 +41,7 @@ namespace anm2ed::animations ImGui::PushFont(resources.fonts[font].get(), font::SIZE); ImGui::SetNextItemSelectionUserData(i); if (imgui::selectable_input_text(animation.name, - std::format("###Document #{} Animation #{}", manager.selected, i), + std::format("###Document #{} Animation #{}", documentIndex, i), animation.name, isSelected)) if (!isReferenced) reference = {(int)i}; ImGui::PopFont(); @@ -66,8 +60,8 @@ namespace anm2ed::animations ImGui::PopFont(); } - ImGui::TextUnformatted(std::format("Length: {}", animation.frameNum).c_str()); - ImGui::TextUnformatted(std::format("Loop: {}", animation.isLoop).c_str()); + ImGui::Text("Length: %d", animation.frameNum); + ImGui::Text("Loop: %s", animation.isLoop ? "true" : "false"); ImGui::EndTooltip(); } @@ -102,18 +96,17 @@ namespace anm2ed::animations ImGui::PopID(); } - io = ImGui::EndMultiSelect(); - storage.ApplyRequests(io); + storage.end(); } ImGui::EndChild(); auto widgetSize = imgui::widget_size_with_row_get(5); - imgui::shortcut(settings.shortcutAdd, true); + imgui::shortcut(settings.shortcutAdd); if (ImGui::Button("Add", widgetSize)) { anm2::Animation animation; - if (anm2::Animation* referenceAnimation = document->animation_get()) + if (anm2::Animation* referenceAnimation = document.animation_get()) { for (auto [id, layerAnimation] : referenceAnimation->layerAnimations) animation.layerAnimations[id] = anm2::Item(); @@ -127,6 +120,7 @@ namespace anm2ed::animations anm2.animations.items.insert(anm2.animations.items.begin() + index, animation); selection = {index}; reference = {index}; + document.change(change::ANIMATIONS); } imgui::set_item_tooltip_shortcut("Add a new animation.", settings.shortcutAdd); @@ -135,7 +129,7 @@ namespace anm2ed::animations ImGui::BeginDisabled(selection.empty()); { - imgui::shortcut(settings.shortcutDuplicate, true); + imgui::shortcut(settings.shortcutDuplicate); if (ImGui::Button("Duplicate", widgetSize)) { auto duplicated = selection; @@ -146,6 +140,7 @@ namespace anm2ed::animations selection.insert(++duplicatedEnd); selection.erase(id); } + document.change(change::ANIMATIONS); } imgui::set_item_tooltip_shortcut("Duplicate the selected animation(s).", settings.shortcutDuplicate); @@ -155,7 +150,7 @@ namespace anm2ed::animations { if (ImGui::Button("Merge", widgetSize)) { - ImGui::OpenPopup("Merge Animations"); + mergePopup.open(); mergeSelection.clear(); mergeTarget = *selection.begin(); } @@ -169,14 +164,15 @@ namespace anm2ed::animations ImGui::SameLine(); - imgui::shortcut(settings.shortcutRemove, true); + imgui::shortcut(settings.shortcutRemove); if (ImGui::Button("Remove", widgetSize)) { /* auto selectionErase = set::to_size_t(selection); - if (selectionErase.contains(document->reference.animationIndex)) document->reference.animationIndex = -1; + if (selectionErase.contains(document.reference.animationIndex)) document.reference.animationIndex = -1; vector::range_erase(anm2.animations.items, selectionErase); */ + document.change(change::ANIMATIONS); selection.clear(); } imgui::set_item_tooltip_shortcut("Remove the selected animation(s).", settings.shortcutDuplicate); @@ -185,7 +181,7 @@ namespace anm2ed::animations ImGui::BeginDisabled(selection.size() != 1); { - imgui::shortcut(settings.shortcutDefault, true); + imgui::shortcut(settings.shortcutDefault); if (ImGui::Button("Default", widgetSize)) anm2.animations.defaultAnimation = anm2.animations.items[*selection.begin()].name; imgui::set_item_tooltip_shortcut("Set the selected animation as the default.", settings.shortcutDuplicate); @@ -194,27 +190,23 @@ namespace anm2ed::animations } ImGui::EndDisabled(); - auto viewport = ImGui::GetMainViewport(); + mergePopup.trigger(); - ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * 0.5f)); - - if (ImGui::BeginPopupModal("Merge Animations", nullptr, ImGuiWindowFlags_NoResize)) + if (ImGui::BeginPopupModal(mergePopup.label, &mergePopup.isOpen, ImGuiWindowFlags_NoResize)) { auto merge_close = [&]() { mergeSelection.clear(); - ImGui::CloseCurrentPopup(); + mergePopup.close(); }; auto& type = settings.mergeType; auto& isDeleteAnimationsAfter = settings.mergeIsDeleteAnimationsAfter; - mergeStorage.UserData = &mergeSelection; - mergeStorage.AdapterSetItemSelected = imgui::external_storage_set; + mergeStorage.user_data_set(&mergeSelection); - auto footerSize = ImVec2(0, imgui::footer_height_get()); - auto optionsSize = imgui::child_size_get(2, true); + auto footerSize = imgui::footer_size_get(); + auto optionsSize = imgui::child_size_get(2); auto deleteAfterSize = imgui::child_size_get(); auto animationsSize = ImVec2(0, ImGui::GetContentRegionAvail().y - @@ -222,9 +214,7 @@ namespace anm2ed::animations if (ImGui::BeginChild("Animations", animationsSize, ImGuiChildFlags_Borders)) { - ImGuiMultiSelectIO* io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, mergeSelection.size(), - anm2.animations.items.size()); - mergeStorage.ApplyRequests(io); + mergeSelection.begin(); for (auto [i, animation] : std::views::enumerate(anm2.animations.items)) { @@ -238,14 +228,12 @@ namespace anm2ed::animations ImGui::PopID(); } - io = ImGui::EndMultiSelect(); - mergeStorage.ApplyRequests(io); + mergeSelection.end(); } ImGui::EndChild(); if (ImGui::BeginChild("Merge Options", optionsSize, ImGuiChildFlags_Borders)) { - auto size = ImVec2(optionsSize.x * 0.5f, optionsSize.y - ImGui::GetStyle().WindowPadding.y * 2); if (ImGui::BeginChild("Merge Options 1", size)) diff --git a/src/animations.h b/src/animations.h index a06a582..552b4d1 100644 --- a/src/animations.h +++ b/src/animations.h @@ -1,6 +1,7 @@ #pragma once -#include "document_manager.h" +#include "document.h" +#include "imgui.h" #include "resources.h" #include "settings.h" @@ -8,13 +9,14 @@ namespace anm2ed::animations { class Animations { - ImGuiSelectionExternalStorage mergeStorage{}; - ImGuiSelectionExternalStorage storage{}; + imgui::PopupHelper mergePopup{imgui::PopupHelper("Merge Animations")}; + imgui::MultiSelectStorage mergeStorage{}; + imgui::MultiSelectStorage storage{}; std::set mergeSelection{}; int mergeTarget{}; public: - void update(document_manager::DocumentManager& manager, settings::Settings& settings, + void update(document::Document& document, int& documentIndex, settings::Settings& settings, resources::Resources& resources); }; } \ No newline at end of file diff --git a/src/anm2.cpp b/src/anm2.cpp index a36d94c..b67c7a1 100644 --- a/src/anm2.cpp +++ b/src/anm2.cpp @@ -16,6 +16,7 @@ using namespace anm2ed::util; namespace anm2ed::anm2 { + void Reference::previous_frame(int max) { frameIndex = glm::clamp(--frameIndex, 0, max); @@ -213,20 +214,6 @@ namespace anm2ed::anm2 } } - bool Content::spritesheet_add(const std::string& directory, const std::string& path, int& id) - { - Spritesheet spritesheet(directory, path); - if (!spritesheet.is_valid()) return false; - id = map::next_id_get(spritesheets); - spritesheets[id] = std::move(spritesheet); - return true; - } - - void Content::spritesheet_remove(int& id) - { - spritesheets.erase(id); - } - std::set Content::spritesheets_unused() { std::set used; @@ -778,7 +765,16 @@ namespace anm2ed::anm2 bool Anm2::spritesheet_add(const std::string& directory, const std::string& path, int& id) { - return content.spritesheet_add(directory, path, id); + Spritesheet spritesheet(directory, path); + if (!spritesheet.is_valid()) return false; + id = map::next_id_get(content.spritesheets); + content.spritesheets[id] = std::move(spritesheet); + return true; + } + + void Anm2::spritesheet_remove(int id) + { + content.spritesheets.erase(id); } Spritesheet* Anm2::spritesheet_get(int id) @@ -786,24 +782,60 @@ namespace anm2ed::anm2 return map::find(content.spritesheets, id); } - void Anm2::spritesheet_remove(int id) - { - content.spritesheet_remove(id); - } - std::set Anm2::spritesheets_unused() { return content.spritesheets_unused(); } - void Anm2::layer_add(int& id) + Reference Anm2::layer_add(Reference reference, std::string name, int spritesheetID, locale::Type locale) { - content.layer_add(id); + auto id = reference.itemID == -1 ? map::next_id_get(content.layers) : reference.itemID; + auto& layer = content.layers[id]; + + layer.name = !name.empty() ? name : layer.name; + layer.spritesheetID = content.spritesheets.contains(spritesheetID) ? spritesheetID : 0; + + auto add = [&](Animation* animation, int id) + { + animation->layerAnimations[id] = Item(); + animation->layerOrder.push_back(id); + }; + + if (locale == locale::GLOBAL) + { + for (auto& animation : animations.items) + if (!animation.layerAnimations.contains(id)) add(&animation, id); + } + else if (locale == locale::LOCAL) + { + if (auto animation = animation_get(reference)) + if (!animation->layerAnimations.contains(id)) add(animation, id); + } + + return {reference.animationIndex, LAYER, id}; } - void Anm2::null_add(int& id) + Reference Anm2::null_add(Reference reference, std::string name, locale::Type locale) { - content.null_add(id); + auto id = reference.itemID == -1 ? map::next_id_get(content.nulls) : reference.itemID; + auto& null = content.nulls[id]; + + null.name = !name.empty() ? name : null.name; + + auto add = [&](Animation* animation, int id) { animation->nullAnimations[id] = Item(); }; + + if (locale == locale::GLOBAL) + { + for (auto& animation : animations.items) + if (!animation.nullAnimations.contains(id)) add(&animation, id); + } + else if (locale == locale::LOCAL) + { + if (auto animation = animation_get(reference)) + if (!animation->nullAnimations.contains(id)) add(animation, id); + } + + return {reference.animationIndex, LAYER, id}; } void Anm2::event_add(int& id) @@ -811,45 +843,68 @@ namespace anm2ed::anm2 content.event_add(id); } - std::set Anm2::events_unused() + std::set Anm2::events_unused(Reference reference) { - std::set used; - for (auto& animation : animations.items) - for (auto& frame : animation.triggers.frames) - used.insert(frame.eventID); + std::set used{}; + std::set unused{}; + + if (auto animation = animation_get(reference); animation) + for (auto& frame : animation->triggers.frames) + used.insert(frame.eventID); + else + for (auto& animation : animations.items) + for (auto& frame : animation.triggers.frames) + used.insert(frame.eventID); - std::set unused; for (auto& id : content.events | std::views::keys) if (!used.contains(id)) unused.insert(id); return unused; } - std::set Anm2::layers_unused() + std::set Anm2::layers_unused(Reference reference) { - std::set used; - for (auto& animation : animations.items) - for (auto& id : animation.layerAnimations | std::views::keys) - used.insert(id); + std::set used{}; + std::set unused{}; + + if (auto animation = animation_get(reference); animation) + for (auto& id : animation->layerAnimations | std::views::keys) + used.insert(id); + else + for (auto& animation : animations.items) + for (auto& id : animation.layerAnimations | std::views::keys) + used.insert(id); - std::set unused; for (auto& id : content.layers | std::views::keys) if (!used.contains(id)) unused.insert(id); return unused; } - std::set Anm2::nulls_unused() + std::set Anm2::nulls_unused(Reference reference) { - std::set used; - for (auto& animation : animations.items) - for (auto& id : animation.nullAnimations | std::views::keys) - used.insert(id); + std::set used{}; + std::set unused{}; + + if (auto animation = animation_get(reference); animation) + for (auto& id : animation->nullAnimations | std::views::keys) + used.insert(id); + else + for (auto& animation : animations.items) + for (auto& id : animation.nullAnimations | std::views::keys) + used.insert(id); - std::set unused; for (auto& id : content.nulls | std::views::keys) if (!used.contains(id)) unused.insert(id); return unused; } + + std::vector Anm2::spritesheet_names_get() + { + std::vector spritesheets{}; + for (auto& [id, spritesheet] : content.spritesheets) + spritesheets.push_back(std::format("#{} {}", id, spritesheet.path.c_str())); + return spritesheets; + } } \ No newline at end of file diff --git a/src/anm2.h b/src/anm2.h index ef9a820..5257a11 100644 --- a/src/anm2.h +++ b/src/anm2.h @@ -21,6 +21,10 @@ namespace anm2ed::anm2 constexpr auto MERGED_STRING = "(Merged)"; + constexpr auto LAYER_FORMAT = "#{} {} (Spritesheet: #{})"; + constexpr auto NULL_FORMAT = "#{} {}"; + constexpr auto SPRITESHEET_FORMAT = "#%d %s"; + enum Type { NONE, @@ -44,6 +48,8 @@ namespace anm2ed::anm2 auto operator<=>(const Reference&) const = default; }; + constexpr anm2::Reference REFERENCE_DEFAULT = {-1, anm2::NONE, -1, -1, -1}; + class Info { public: @@ -228,11 +234,15 @@ namespace anm2ed::anm2 Spritesheet* spritesheet_get(int id); void spritesheet_remove(int id); std::set spritesheets_unused(); - void layer_add(int& id); - void null_add(int& id); + int layer_add(); + Reference layer_add(Reference reference = REFERENCE_DEFAULT, std::string name = {}, int spritesheetID = 0, + types::locale::Type locale = types::locale::GLOBAL); + Reference null_add(Reference reference = REFERENCE_DEFAULT, std::string name = {}, + types::locale::Type locale = types::locale::GLOBAL); void event_add(int& id); - std::set events_unused(); - std::set layers_unused(); - std::set nulls_unused(); + std::set events_unused(Reference reference = REFERENCE_DEFAULT); + std::set layers_unused(Reference reference = REFERENCE_DEFAULT); + std::set nulls_unused(Reference reference = REFERENCE_DEFAULT); + std::vector spritesheet_names_get(); }; } \ No newline at end of file diff --git a/src/dockspace.cpp b/src/dockspace.cpp index 855482a..a86d1f3 100644 --- a/src/dockspace.cpp +++ b/src/dockspace.cpp @@ -33,17 +33,17 @@ namespace anm2ed::dockspace { if (ImGui::DockSpace(ImGui::GetID("##DockSpace"), ImVec2(), ImGuiDockNodeFlags_PassthruCentralNode)) { - if (manager.get()) + if (auto document = manager.get(); document) { if (settings.windowIsAnimationPreview) animationPreview.update(manager, settings, resources, playback); - if (settings.windowIsAnimations) animations.update(manager, settings, resources); + if (settings.windowIsAnimations) animations.update(*document, manager.selected, settings, resources); if (settings.windowIsEvents) events.update(manager, settings, resources); if (settings.windowIsFrameProperties) frameProperties.update(manager, settings); - if (settings.windowIsLayers) layers.update(manager, settings, resources); - if (settings.windowIsNulls) nulls.update(manager, settings, resources); + if (settings.windowIsLayers) layers.update(*document, settings, resources); + if (settings.windowIsNulls) nulls.update(*document, manager.selected, settings, resources); if (settings.windowIsOnionskin) onionskin.update(settings); if (settings.windowIsSpritesheetEditor) spritesheetEditor.update(manager, settings, resources); - if (settings.windowIsSpritesheets) spritesheets.update(manager, settings, resources, dialog); + if (settings.windowIsSpritesheets) spritesheets.update(*document, settings, resources, dialog); if (settings.windowIsTimeline) timeline.update(manager, settings, resources, playback); if (settings.windowIsTools) tools.update(settings, resources); } diff --git a/src/document.cpp b/src/document.cpp index 8b002a3..3727f79 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -5,13 +5,21 @@ using namespace anm2ed::anm2; using namespace anm2ed::filesystem; +using namespace anm2ed::types; namespace anm2ed::document { - Document::Document() = default; + Document::Document() + { + for (auto& value : isJustChanged) + value = true; + } Document::Document(const std::string& path, bool isNew, std::string* errorString) { + for (auto& value : isJustChanged) + value = true; + if (!path_is_exist(path)) return; if (isNew) @@ -23,7 +31,8 @@ namespace anm2ed::document } this->path = path; - hash_set(); + on_change(); + clean(); } bool Document::save(const std::string& path, std::string* errorString) @@ -32,7 +41,7 @@ namespace anm2ed::document if (anm2.serialize(this->path, errorString)) { - hash_set(); + clean(); return true; } @@ -40,18 +49,20 @@ namespace anm2ed::document } void Document::hash_set() + { + hash = anm2.hash(); + } + + void Document::clean() { saveHash = anm2.hash(); hash = saveHash; } - void Document::hash_time(double time, double interval) + void Document::change(change::Type type) { - if (time - lastHashTime > interval) - { - hash = anm2.hash(); - lastHashTime = time; - } + hash_set(); + isJustChanged[type] = true; } bool Document::is_dirty() @@ -59,6 +70,11 @@ namespace anm2ed::document return hash != saveHash; } + bool Document::is_just_changed(types::change::Type type) + { + return isJustChanged[type]; + } + std::string Document::directory_get() { return path.parent_path(); @@ -79,6 +95,11 @@ namespace anm2ed::document return anm2.frame_get(reference); } + anm2::Item* Document::item_get() + { + return anm2.item_get(reference); + } + anm2::Spritesheet* Document::spritesheet_get() { return anm2.spritesheet_get(referenceSpritesheet); @@ -88,4 +109,23 @@ namespace anm2ed::document { return !path.empty(); } + + void Document::on_change() + { + if (is_just_changed(change::SPRITESHEETS)) + { + spritesheetNames = anm2.spritesheet_names_get(); + spritesheetNamesCstr.clear(); + for (auto& name : spritesheetNames) + spritesheetNamesCstr.push_back(name.c_str()); + } + } + + void Document::update() + { + on_change(); + + for (auto& value : isJustChanged) + value = false; + } }; \ No newline at end of file diff --git a/src/document.h b/src/document.h index d16377b..7097ce6 100644 --- a/src/document.h +++ b/src/document.h @@ -1,9 +1,11 @@ #pragma once -#include "anm2.h" #include #include +#include "anm2.h" +#include "types.h" + #include namespace anm2ed::document @@ -22,6 +24,7 @@ namespace anm2ed::document int overlayIndex{}; int referenceSpritesheet{-1}; + int referenceLayer{-1}; std::set selectedEvents{}; std::set selectedLayers{}; @@ -29,9 +32,12 @@ namespace anm2ed::document std::set selectedAnimations{}; std::set selectedSpritesheets{}; + std::vector spritesheetNames{}; + std::vector spritesheetNamesCstr{}; + uint64_t hash{}; uint64_t saveHash{}; - double lastHashTime{}; + bool isJustChanged[types::change::COUNT]{}; bool isOpen{true}; Document(); @@ -39,12 +45,17 @@ namespace anm2ed::document Document(const std::string& path, bool isNew = false, std::string* errorString = nullptr); bool save(const std::string& path = {}, std::string* errorString = nullptr); void hash_set(); - void hash_time(double time, double interval = 1.0); + void clean(); + void on_change(); + void change(types::change::Type type); + bool is_just_changed(types::change::Type type); bool is_dirty(); + void update(); std::string directory_get(); std::string filename_get(); anm2::Animation* animation_get(); anm2::Frame* frame_get(); + anm2::Item* item_get(); anm2::Spritesheet* spritesheet_get(); bool is_valid(); }; diff --git a/src/document_manager.cpp b/src/document_manager.cpp index f31bb73..2ad1207 100644 --- a/src/document_manager.cpp +++ b/src/document_manager.cpp @@ -1,9 +1,7 @@ #include "document_manager.h" -#include "toast.h" #include "util.h" -using namespace anm2ed::toast; using namespace anm2ed::util; namespace anm2ed::document_manager @@ -27,10 +25,8 @@ namespace anm2ed::document_manager documents.emplace_back(std::move(document)); selected = documents.size() - 1; pendingSelected = selected; - toasts.add(std::format("Opened document: {}", path)); return true; } - toasts.add_error(std::format("Failed to open document: {} ({})", path, errorString)); return false; } @@ -47,10 +43,7 @@ namespace anm2ed::document_manager document->path = !path.empty() ? path : document->path.string(); - if (document->save(document->path, &errorString)) - toasts.add(std::format("Saved document: {}", document->path.string())); - else - toasts.add_error(std::format("Failed to save document: {} ({})", document->path.string(), errorString)); + document->save(document->path, &errorString); } void DocumentManager::save(const std::string& path) @@ -62,15 +55,4 @@ namespace anm2ed::document_manager { documents.erase(documents.begin() + index); } - - void DocumentManager::spritesheet_add(const std::string& path) - { - auto document = get(); - if (!document) return; - - if (int id{}; document->anm2.spritesheet_add(document->directory_get(), path, id)) - toasts.add(std::format("Added spritesheet #{}: {}", id, path)); - else - toasts.add(std::format("Failed to add spritesheet #{}: {}", id, path)); - } } diff --git a/src/documents.cpp b/src/documents.cpp index 59d3bf8..1059a0d 100644 --- a/src/documents.cpp +++ b/src/documents.cpp @@ -32,10 +32,8 @@ namespace anm2ed::documents for (auto [i, document] : std::views::enumerate(manager.documents)) { auto isDirty = document.is_dirty(); - auto isRequested = i == manager.pendingSelected; auto isSelected = i == manager.selected; - - if (isSelected) document.hash_time(ImGui::GetTime()); + auto isRequested = i == manager.pendingSelected; auto font = isDirty ? font::ITALICS : font::REGULAR; @@ -67,23 +65,18 @@ namespace anm2ed::documents else manager.close(i); } + + if (isSelected) document.update(); } ImGui::EndTabBar(); } - if (isOpenCloseDocumentPopup) - { - ImGui::OpenPopup("Close Document"); - isOpenCloseDocumentPopup = false; - } + closePopup.trigger(); if (isCloseDocument) { - ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(ImVec2(), ImGuiCond_None); - - if (ImGui::BeginPopupModal("Close Document", nullptr, ImGuiWindowFlags_NoResize)) + if (ImGui::BeginPopupModal(closePopup.label, &closePopup.isOpen, ImGuiWindowFlags_NoResize)) { auto closeDocument = manager.get(closeDocumentIndex); @@ -96,8 +89,7 @@ namespace anm2ed::documents auto close = [&]() { closeDocumentIndex = 0; - isCloseDocument = false; - ImGui::CloseCurrentPopup(); + closePopup.close(); }; if (ImGui::Button("Yes", widgetSize)) diff --git a/src/documents.h b/src/documents.h index abe724d..f5bb60a 100644 --- a/src/documents.h +++ b/src/documents.h @@ -1,6 +1,7 @@ #pragma once #include "document_manager.h" +#include "imgui.h" #include "resources.h" #include "taskbar.h" @@ -11,6 +12,7 @@ namespace anm2ed::documents bool isCloseDocument{}; bool isOpenCloseDocumentPopup{}; int closeDocumentIndex{}; + imgui::PopupHelper closePopup{imgui::PopupHelper("Close Document", imgui::POPUP_TO_CONTENT)}; public: float height{}; diff --git a/src/events.cpp b/src/events.cpp index c2c624c..7e69b83 100644 --- a/src/events.cpp +++ b/src/events.cpp @@ -2,8 +2,6 @@ #include -#include "imgui.h" - using namespace anm2ed::document_manager; using namespace anm2ed::settings; using namespace anm2ed::resources; @@ -13,22 +11,22 @@ namespace anm2ed::events { void Events::update(DocumentManager& manager, Settings& settings, Resources& resources) { + auto& document = *manager.get(); + auto& anm2 = document.anm2; + auto& selection = document.selectedEvents; + + if (document.is_just_changed(change::EVENTS)) unusedEventIDs = anm2.events_unused(); + + storage.user_data_set(&selection); + if (ImGui::Begin("Events", &settings.windowIsEvents)) { - auto document = manager.get(); - anm2::Anm2& anm2 = document->anm2; - - auto& selection = document->selectedEvents; - storage.UserData = &selection; - storage.AdapterSetItemSelected = imgui::external_storage_set; - - auto childSize = imgui::size_with_footer_get(); + auto childSize = imgui::size_without_footer_get(); + bool isRenamed{}; if (ImGui::BeginChild("##Events Child", childSize, true)) { - ImGuiMultiSelectIO* io = - ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, selection.size(), anm2.content.events.size()); - storage.ApplyRequests(io); + storage.begin(anm2.content.events.size()); for (auto& [id, event] : anm2.content.events) { @@ -36,8 +34,9 @@ namespace anm2ed::events ImGui::PushID(id); ImGui::SetNextItemSelectionUserData(id); - imgui::selectable_input_text(event.name, std::format("###Document #{} Event #{}", manager.selected, id), - event.name, isSelected); + if (imgui::selectable_input_text(event.name, std::format("###Document #{} Event #{}", manager.selected, id), + event.name, isSelected, 0, &isRenamed)) + if (isRenamed) document.change(change::EVENTS); if (ImGui::BeginItemTooltip()) { ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); @@ -48,31 +47,33 @@ namespace anm2ed::events ImGui::PopID(); } - io = ImGui::EndMultiSelect(); - storage.ApplyRequests(io); + storage.end(); } ImGui::EndChild(); auto widgetSize = imgui::widget_size_with_row_get(2); - imgui::shortcut(settings.shortcutAdd, true); + imgui::shortcut(settings.shortcutAdd); if (ImGui::Button("Add", widgetSize)) { int id{}; anm2.event_add(id); selection = {id}; + document.change(change::EVENTS); } imgui::set_item_tooltip_shortcut("Add an event.", settings.shortcutAdd); ImGui::SameLine(); - std::set unusedEventIDs = anm2.events_unused(); - - imgui::shortcut(settings.shortcutRemove, true); + imgui::shortcut(settings.shortcutRemove); ImGui::BeginDisabled(unusedEventIDs.empty()); { if (ImGui::Button("Remove Unused", widgetSize)) + { for (auto& id : unusedEventIDs) - anm2.content.layers.erase(id); + anm2.content.events.erase(id); + document.change(change::EVENTS); + unusedEventIDs.clear(); + } } ImGui::EndDisabled(); imgui::set_item_tooltip_shortcut("Remove unused events (i.e., ones not used by any trigger in any animation.)", diff --git a/src/events.h b/src/events.h index 9573295..f38f3bf 100644 --- a/src/events.h +++ b/src/events.h @@ -1,6 +1,7 @@ #pragma once #include "document_manager.h" +#include "imgui.h" #include "resources.h" #include "settings.h" @@ -8,7 +9,8 @@ namespace anm2ed::events { class Events { - ImGuiSelectionExternalStorage storage{}; + imgui::MultiSelectStorage storage{}; + std::set unusedEventIDs{}; public: void update(document_manager::DocumentManager& manager, settings::Settings& settings, diff --git a/src/frame_properties.cpp b/src/frame_properties.cpp index 4b57cbf..35ec7ff 100644 --- a/src/frame_properties.cpp +++ b/src/frame_properties.cpp @@ -100,7 +100,9 @@ namespace anm2ed::frame_properties "%s", "Toggle the frame interpolating; i.e., blending its values into the next frame based on the time."); ImGui::SameLine(); + ImGui::EndDisabled(); ImGui::Checkbox("Round", &settings.propertiesIsRound); + ImGui::BeginDisabled(!frame); ImGui::SetItemTooltip( "%s", "When toggled, decimal values will be snapped to their nearest whole value when changed."); diff --git a/src/imgui.cpp b/src/imgui.cpp index 3b6d13d..c3a3007 100644 --- a/src/imgui.cpp +++ b/src/imgui.cpp @@ -6,8 +6,12 @@ #include #include +using namespace anm2ed::types; +using namespace glm; + namespace anm2ed::imgui { + std::string chord_to_string(ImGuiKeyChord chord) { std::string result; @@ -71,14 +75,19 @@ namespace anm2ed::imgui ImGui::GetStyle().ItemSpacing.y * (itemCount); } - ImVec2 size_with_footer_get(int rowCount) + ImVec2 footer_size_get(int itemCount) + { + return ImVec2(ImGui::GetContentRegionAvail().x, footer_height_get(itemCount)); + } + + ImVec2 size_without_footer_get(int rowCount) { return ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - footer_height_get(rowCount)); } - ImVec2 child_size_get(int rowCount, bool isContentRegionAvail) + ImVec2 child_size_get(int rowCount) { - return ImVec2(isContentRegionAvail ? ImGui::GetContentRegionAvail().x : 0, + return ImVec2(ImGui::GetContentRegionAvail().x, (ImGui::GetFrameHeightWithSpacing() * rowCount) + (ImGui::GetStyle().WindowPadding.y * 2.0f)); } @@ -107,8 +116,13 @@ namespace anm2ed::imgui ImGui::Combo(label.c_str(), index, items.data(), (int)items.size()); } + void combo_strings(const std::string& label, int* index, std::vector& strings) + { + ImGui::Combo(label.c_str(), index, strings.data(), (int)strings.size()); + } + bool selectable_input_text(const std::string& label, const std::string& id, std::string& text, bool isSelected, - ImGuiSelectableFlags flags) + ImGuiSelectableFlags flags, bool* isRenamed) { static std::string editID{}; static bool isJustEdit{}; @@ -128,6 +142,7 @@ namespace anm2ed::imgui { editID.clear(); isActivated = true; + if (isRenamed) *isRenamed = true; } if (ImGui::IsItemDeactivatedAfterEdit() || ImGui::IsKeyPressed(ImGuiKey_Escape)) editID.clear(); } @@ -146,10 +161,9 @@ namespace anm2ed::imgui return isActivated; } - void set_item_tooltip_shortcut(const std::string& tooltip, const std::string& shortcut) + void set_item_tooltip_shortcut(const char* tooltip, const std::string& shortcut) { - auto text = shortcut.empty() ? tooltip : std::format("{}\n(Shortcut: {})", tooltip, shortcut); - ImGui::SetItemTooltip("%s", text.c_str()); + ImGui::SetItemTooltip("%s\n(Shortcut: %s)", tooltip, shortcut.c_str()); } void external_storage_set(ImGuiSelectionExternalStorage* self, int id, bool isSelected) @@ -219,17 +233,71 @@ namespace anm2ed::imgui return false; } - bool shortcut(std::string string, bool isSet, bool isGlobal, bool isPopupBlock) + bool shortcut(std::string string, shortcut::Type type) { - if (isPopupBlock && ImGui::GetTopMostPopupModal() != nullptr) return false; - auto flags = isGlobal ? ImGuiInputFlags_RouteGlobal : ImGuiInputFlags_RouteFocused; - - if (isSet) + if (ImGui::GetTopMostPopupModal() != nullptr) return false; + auto flags = type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal + : ImGuiInputFlags_RouteFocused; + if (type == shortcut::GLOBAL_SET || type == shortcut::FOCUSED_SET) { ImGui::SetNextItemShortcut(string_to_chord(string), flags); - return true; + return false; } return ImGui::Shortcut(string_to_chord(string), flags); } + + MultiSelectStorage::MultiSelectStorage() + { + internal.AdapterSetItemSelected = external_storage_set; + } + + void MultiSelectStorage::user_data_set(std::set* userData) + { + internal.UserData = userData; + this->userData = userData; + } + + void MultiSelectStorage::begin(size_t size) + { + auto io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, userData ? userData->size() : 0, size); + internal.ApplyRequests(io); + } + + void MultiSelectStorage::end() + { + auto io = ImGui::EndMultiSelect(); + internal.ApplyRequests(io); + } + + PopupHelper::PopupHelper(const char* label, float percent, bool isNoHeight) + { + this->label = label; + this->percent = percent; + this->isNoHeight = isNoHeight; + } + + void PopupHelper::open() + { + isOpen = true; + isTriggered = true; + } + + void PopupHelper::trigger() + { + if (isTriggered) ImGui::OpenPopup(label); + isTriggered = false; + + auto viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, to_imvec2(vec2(0.5f))); + if (isNoHeight) + ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * percent, 0)); + else + ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * percent)); + } + + void PopupHelper::close() + { + isOpen = false; + } } diff --git a/src/imgui.h b/src/imgui.h index 11f9d71..7206a54 100644 --- a/src/imgui.h +++ b/src/imgui.h @@ -1,12 +1,19 @@ #pragma once #include +#include #include #include #include +#include "types.h" + namespace anm2ed::imgui { + constexpr auto POPUP_TO_CONTENT = 0.0f; + constexpr auto POPUP_SMALL = 0.25f; + constexpr auto POPUP_NORMAL = 0.5f; + const std::unordered_map KEY_MAP = {{"A", ImGuiKey_A}, {"B", ImGuiKey_B}, {"C", ImGuiKey_C}, @@ -122,17 +129,46 @@ namespace anm2ed::imgui float row_widget_width_get(int count, float width = ImGui::GetContentRegionAvail().x); ImVec2 widget_size_with_row_get(int count, float width = ImGui::GetContentRegionAvail().x); float footer_height_get(int itemCount = 1); - ImVec2 size_with_footer_get(int rowCount = 1); - ImVec2 child_size_get(int rowCount = 1, bool isContentRegionAvail = false); + ImVec2 footer_size_get(int itemCount = 1); + ImVec2 size_without_footer_get(int rowCount = 1); + ImVec2 child_size_get(int rowCount = 1); int input_text_callback(ImGuiInputTextCallbackData* data); bool input_text_string(const char* label, std::string* string, ImGuiInputTextFlags flags = 0); void combo_strings(const std::string& label, int* index, std::vector& strings); + void combo_strings(const std::string& label, int* index, std::vector& strings); bool selectable_input_text(const std::string& label, const std::string& id, std::string& text, - bool isSelected = false, ImGuiSelectableFlags flags = 0); - void set_item_tooltip_shortcut(const std::string& tooltip, const std::string& shortcut = {}); + bool isSelected = false, ImGuiSelectableFlags flags = 0, bool* isRenamed = nullptr); + void set_item_tooltip_shortcut(const char* tooltip, const std::string& shortcut = {}); void external_storage_set(ImGuiSelectionExternalStorage* self, int id, bool isSelected); ImVec2 icon_size_get(); bool chord_held(ImGuiKeyChord chord); bool chord_repeating(ImGuiKeyChord chord, float delay = 0.125f, float rate = 0.025f); - bool shortcut(std::string string, bool isSet = false, bool isGlobal = false, bool isPopupBlock = true); + bool shortcut(std::string string, types::shortcut::Type type = types::shortcut::FOCUSED_SET); + + class MultiSelectStorage + { + public: + ImGuiSelectionExternalStorage internal{}; + std::set* userData{}; + + MultiSelectStorage(); + void user_data_set(std::set* userData); + void begin(size_t size); + void end(); + }; + + class PopupHelper + { + public: + const char* label{}; + bool isOpen{}; + bool isTriggered{}; + bool isNoHeight{}; + float percent{}; + + PopupHelper(const char* label, float percent = POPUP_NORMAL, bool isNoHeight = false); + void open(); + void trigger(); + void close(); + }; } \ No newline at end of file diff --git a/src/layers.cpp b/src/layers.cpp index 07536d9..a7b958a 100644 --- a/src/layers.cpp +++ b/src/layers.cpp @@ -2,80 +2,151 @@ #include -#include "imgui.h" +#include "util.h" -using namespace anm2ed::document_manager; +using namespace anm2ed::document; using namespace anm2ed::settings; using namespace anm2ed::resources; using namespace anm2ed::types; +using namespace anm2ed::util; namespace anm2ed::layers { - void Layers::update(DocumentManager& manager, Settings& settings, Resources& resources) + void Layers::update(Document& document, Settings& settings, Resources& resources) { + auto& anm2 = document.anm2; + auto& selection = document.selectedLayers; + auto& referenceLayer = document.referenceLayer; + + if (document.is_just_changed(change::LAYERS)) unusedLayerIDs = anm2.layers_unused(); + + storage.user_data_set(&selection); + + auto properties_popup_open = [&](int id = -1) + { + if (id == -1) + { + isAdd = true; + editLayer = anm2::Layer(); + } + else + editLayer = anm2.content.layers.at(id); + + propertiesPopup.open(); + }; + if (ImGui::Begin("Layers", &settings.windowIsLayers)) { - auto document = manager.get(); - anm2::Anm2& anm2 = document->anm2; - - auto& selection = document->selectedLayers; - storage.UserData = &selection; - storage.AdapterSetItemSelected = imgui::external_storage_set; - - auto childSize = imgui::size_with_footer_get(); + auto childSize = imgui::size_without_footer_get(); if (ImGui::BeginChild("##Layers Child", childSize, true)) { - ImGuiMultiSelectIO* io = - ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, selection.size(), anm2.content.layers.size()); - storage.ApplyRequests(io); + storage.begin(anm2.content.layers.size()); for (auto& [id, layer] : anm2.content.layers) { auto isSelected = selection.contains(id); + auto isReferenced = referenceLayer == id; ImGui::PushID(id); + ImGui::SetNextItemSelectionUserData(id); - imgui::selectable_input_text(std::format("#{} {}", id, layer.name), - std::format("###Document #{} Layer #{}", manager.selected, id), layer.name, - isSelected); + if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE); + ImGui::Selectable(std::format("#{} {} (Spritesheet: #{})", id, layer.name, layer.spritesheetID).c_str(), + isSelected); + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + { + referenceLayer = id; + properties_popup_open(id); + } + + if (isReferenced) ImGui::PopFont(); if (ImGui::BeginItemTooltip()) { ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(layer.name.c_str()); - ImGui::TextUnformatted(std::format("ID: {}", id).c_str()); - ImGui::TextUnformatted(std::format("Spritesheet ID: {}", layer.spritesheetID).c_str()); ImGui::PopFont(); + ImGui::Text("ID: %d", id); + ImGui::Text("Spritesheet ID: %d", layer.spritesheetID); ImGui::EndTooltip(); } ImGui::PopID(); } - io = ImGui::EndMultiSelect(); - storage.ApplyRequests(io); + storage.end(); } ImGui::EndChild(); auto widgetSize = imgui::widget_size_with_row_get(2); - imgui::shortcut(settings.shortcutAdd, true); - ImGui::Button("Add", widgetSize); + imgui::shortcut(settings.shortcutAdd); + if (ImGui::Button("Add", widgetSize)) properties_popup_open(); imgui::set_item_tooltip_shortcut("Add a layer.", settings.shortcutAdd); ImGui::SameLine(); - std::set unusedLayersIDs = anm2.layers_unused(); - - imgui::shortcut(settings.shortcutRemove, true); - ImGui::BeginDisabled(unusedLayersIDs.empty()); + imgui::shortcut(settings.shortcutRemove); + ImGui::BeginDisabled(unusedLayerIDs.empty()); { if (ImGui::Button("Remove Unused", widgetSize)) - for (auto& id : unusedLayersIDs) + { + for (auto& id : unusedLayerIDs) anm2.content.layers.erase(id); + document.change(change::LAYERS); + unusedLayerIDs.clear(); + } } ImGui::EndDisabled(); imgui::set_item_tooltip_shortcut("Remove unused layers (i.e., ones not used in any animation.)", settings.shortcutRemove); } ImGui::End(); + + propertiesPopup.trigger(); + + if (ImGui::BeginPopupModal(propertiesPopup.label, &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) + { + auto childSize = imgui::child_size_get(2); + auto& layer = editLayer; + + auto close = [&]() + { + isAdd = false; + editLayer = anm2::Layer(); + propertiesPopup.close(); + }; + + if (ImGui::BeginChild("Child", childSize, ImGuiChildFlags_Borders)) + { + imgui::input_text_string("Name", &layer.name); + ImGui::SetItemTooltip("Set the item's name."); + imgui::combo_strings("Spritesheet", &layer.spritesheetID, document.spritesheetNames); + ImGui::SetItemTooltip("Set the layer item's spritesheet."); + } + ImGui::EndChild(); + + auto widgetSize = imgui::widget_size_with_row_get(2); + + if (ImGui::Button(isAdd ? "Add" : "Confirm", widgetSize)) + { + if (isAdd) + { + auto id = map::next_id_get(anm2.content.layers); + anm2.content.layers[id] = editLayer; + referenceLayer = id; + } + else + anm2.content.layers[referenceLayer] = editLayer; + document.change(change::LAYERS); + close(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Cancel", widgetSize)) close(); + + ImGui::EndPopup(); + } + + referenceLayer = propertiesPopup.isOpen ? referenceLayer : -1; } } diff --git a/src/layers.h b/src/layers.h index 5007e68..ed1538f 100644 --- a/src/layers.h +++ b/src/layers.h @@ -1,17 +1,22 @@ #pragma once -#include "document_manager.h" +#include "document.h" #include "resources.h" #include "settings.h" +#include "imgui.h" + namespace anm2ed::layers { class Layers { - ImGuiSelectionExternalStorage storage{}; + bool isAdd{}; + imgui::PopupHelper propertiesPopup{imgui::PopupHelper("Layer Properties", imgui::POPUP_SMALL, true)}; + imgui::MultiSelectStorage storage; + anm2::Layer editLayer{}; + std::set unusedLayerIDs{}; public: - void update(document_manager::DocumentManager& manager, settings::Settings& settings, - resources::Resources& resources); + void update(document::Document& document, settings::Settings& settings, resources::Resources& resources); }; } \ No newline at end of file diff --git a/src/nulls.cpp b/src/nulls.cpp index 2d9f9a5..fd6eef4 100644 --- a/src/nulls.cpp +++ b/src/nulls.cpp @@ -2,74 +2,68 @@ #include -#include "imgui.h" - -using namespace anm2ed::document_manager; +using namespace anm2ed::document; using namespace anm2ed::settings; using namespace anm2ed::resources; using namespace anm2ed::types; namespace anm2ed::nulls { - void Nulls::update(DocumentManager& manager, Settings& settings, Resources& resources) + void Nulls::update(Document& document, int& documentIndex, Settings& settings, Resources& resources) { + auto& anm2 = document.anm2; + auto& selection = document.selectedNulls; + + if (document.is_just_changed(change::NULLS)) unusedNullsIDs = anm2.nulls_unused(); + + storage.user_data_set(&selection); + if (ImGui::Begin("Nulls", &settings.windowIsNulls)) { - auto document = manager.get(); - anm2::Anm2& anm2 = document->anm2; - - auto& selection = document->selectedNulls; - storage.UserData = &selection; - storage.AdapterSetItemSelected = imgui::external_storage_set; - - auto childSize = imgui::size_with_footer_get(); + auto childSize = imgui::size_without_footer_get(); if (ImGui::BeginChild("##Nulls Child", childSize, true)) { - ImGuiMultiSelectIO* io = - ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, selection.size(), anm2.content.nulls.size()); - storage.ApplyRequests(io); + storage.begin(anm2.content.nulls.size()); for (auto& [id, null] : anm2.content.nulls) { - const bool isSelected = selection.contains(id); + auto isSelected = selection.contains(id); ImGui::PushID(id); ImGui::SetNextItemSelectionUserData(id); imgui::selectable_input_text(std::format("#{} {}", id, null.name), - std::format("###Document #{} Null #{}", manager.selected, id), null.name, + std::format("###Document #{} Null #{}", documentIndex, id), null.name, isSelected); if (ImGui::BeginItemTooltip()) { ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(null.name.c_str()); - ImGui::TextUnformatted(std::format("ID: {}", id).c_str()); ImGui::PopFont(); + ImGui::Text("ID: %d", id); ImGui::EndTooltip(); } ImGui::PopID(); } - io = ImGui::EndMultiSelect(); - storage.ApplyRequests(io); + storage.end(); } ImGui::EndChild(); auto widgetSize = imgui::widget_size_with_row_get(2); - imgui::shortcut(settings.shortcutAdd, true); + imgui::shortcut(settings.shortcutAdd); ImGui::Button("Add", widgetSize); imgui::set_item_tooltip_shortcut("Add a null.", settings.shortcutAdd); ImGui::SameLine(); - std::set unusedNullsIDs = anm2.nulls_unused(); - - imgui::shortcut(settings.shortcutRemove, true); + imgui::shortcut(settings.shortcutRemove); ImGui::BeginDisabled(unusedNullsIDs.empty()); { if (ImGui::Button("Remove Unused", widgetSize)) for (auto& id : unusedNullsIDs) anm2.content.nulls.erase(id); + document.change(change::NULLS); } ImGui::EndDisabled(); imgui::set_item_tooltip_shortcut("Remove unused nulls (i.e., ones not used in any animation.)", diff --git a/src/nulls.h b/src/nulls.h index a85bb6f..3acc7bb 100644 --- a/src/nulls.h +++ b/src/nulls.h @@ -1,6 +1,7 @@ #pragma once -#include "document_manager.h" +#include "document.h" +#include "imgui.h" #include "resources.h" #include "settings.h" @@ -8,10 +9,11 @@ namespace anm2ed::nulls { class Nulls { - ImGuiSelectionExternalStorage storage{}; + imgui::MultiSelectStorage storage{}; + std::set unusedNullsIDs{}; public: - void update(document_manager::DocumentManager& manager, settings::Settings& settings, + void update(document::Document& document, int& documentIndex, settings::Settings& settings, resources::Resources& resources); }; } \ No newline at end of file diff --git a/src/onionskin.cpp b/src/onionskin.cpp index 7064d6c..970aaf1 100644 --- a/src/onionskin.cpp +++ b/src/onionskin.cpp @@ -5,6 +5,7 @@ #include "imgui.h" using namespace anm2ed::settings; +using namespace anm2ed::types; using namespace glm; namespace anm2ed::onionskin @@ -23,7 +24,7 @@ namespace anm2ed::onionskin ImGui::PopID(); }; - imgui::shortcut(settings.shortcutOnionskin, true, true); + imgui::shortcut(settings.shortcutOnionskin); ImGui::Checkbox("Enabled", &settings.onionskinIsEnabled); order_configure("Before", settings.onionskinBeforeCount, settings.onionskinBeforeColor); @@ -36,7 +37,8 @@ namespace anm2ed::onionskin ImGui::RadioButton("After", &settings.onionskinDrawOrder, ABOVE); } - if (imgui::shortcut(settings.shortcutOnionskin)) settings.onionskinIsEnabled = !settings.onionskinIsEnabled; + if (imgui::shortcut(settings.shortcutOnionskin), shortcut::GLOBAL) + settings.onionskinIsEnabled = !settings.onionskinIsEnabled; ImGui::End(); } diff --git a/src/settings.h b/src/settings.h index 0197ee9..3fa327f 100644 --- a/src/settings.h +++ b/src/settings.h @@ -120,6 +120,8 @@ namespace anm2ed::settings X(BAKE_IS_ROUND_ROTATION, bakeIsRoundRotation, "Round Rotation", BOOL, true) \ \ X(TIMELINE_ADD_ITEM_TYPE, timelineAddItemType, "Add Item Type", INT, anm2::LAYER) \ + X(TIMELINE_ADD_ITEM_LOCALITY, timelineAddItemLocale, "Add Item Locale", INT, types::locale::GLOBAL) \ + X(TIMELINE_ADD_ITEM_SOURCE, timelineAddItemSource, "Add Item Source", INT, types::source::NEW) \ X(TIMELINE_IS_SHOW_UNUSED, timelineIsShowUnused, "##Show Unused", BOOL, true) \ \ X(ONIONSKIN_IS_ENABLED, onionskinIsEnabled, "Enabled", BOOL, false) \ diff --git a/src/spritesheets.cpp b/src/spritesheets.cpp index e5bbe52..5de52d5 100644 --- a/src/spritesheets.cpp +++ b/src/spritesheets.cpp @@ -1,35 +1,33 @@ #include "spritesheets.h" -#include - #include "imgui.h" #include "toast.h" -#include "types.h" +#include using namespace anm2ed::anm2; using namespace anm2ed::settings; using namespace anm2ed::resources; using namespace anm2ed::dialog; -using namespace anm2ed::document_manager; +using namespace anm2ed::document; using namespace anm2ed::types; using namespace anm2ed::toast; using namespace glm; namespace anm2ed::spritesheets { - void Spritesheets::update(DocumentManager& manager, Settings& settings, Resources& resources, Dialog& dialog) + void Spritesheets::update(Document& document, Settings& settings, Resources& resources, Dialog& dialog) { + auto& anm2 = document.anm2; + auto& selection = document.selectedSpritesheets; + + if (document.is_just_changed(change::SPRITESHEETS)) unusedSpritesheetIDs = anm2.spritesheets_unused(); + if (ImGui::Begin("Spritesheets", &settings.windowIsSpritesheets)) { - auto& document = *manager.get(); - auto& anm2 = document.anm2; - auto& selection = document.selectedSpritesheets; auto style = ImGui::GetStyle(); - static ImGuiSelectionExternalStorage storage{}; - storage.UserData = &selection; - storage.AdapterSetItemSelected = imgui::external_storage_set; + storage.user_data_set(&selection); - auto childSize = imgui::size_with_footer_get(2); + auto childSize = imgui::size_without_footer_get(2); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); @@ -37,12 +35,10 @@ namespace anm2ed::spritesheets { auto spritesheetChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() * 4); - ImGuiMultiSelectIO* io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape, selection.size(), - anm2.content.spritesheets.size()); - storage.ApplyRequests(io); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); + storage.begin(anm2.content.spritesheets.size()); + for (auto& [id, spritesheet] : anm2.content.spritesheets) { ImGui::PushID(id); @@ -94,8 +90,8 @@ namespace anm2ed::spritesheets ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(spritesheet.path.c_str()); ImGui::PopFont(); - ImGui::TextUnformatted(std::format("ID: {}", id).c_str()); - ImGui::TextUnformatted(std::format("Size: {} x {}", texture.size.x, texture.size.y).c_str()); + ImGui::Text("ID: %d", id); + ImGui::Text("Size: %d x %d", texture.size.x, texture.size.y); } ImGui::EndChild(); @@ -120,7 +116,7 @@ namespace anm2ed::spritesheets spritesheetChildSize.y - spritesheetChildSize.y / 2 - ImGui::GetTextLineHeight() / 2)); if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE); - ImGui::TextUnformatted(std::format("#{} {}", id, spritesheet.path.string()).c_str()); + ImGui::Text(SPRITESHEET_FORMAT, id, spritesheet.path.c_str()); if (isReferenced) ImGui::PopFont(); } ImGui::EndChild(); @@ -128,8 +124,7 @@ namespace anm2ed::spritesheets ImGui::PopID(); } - io = ImGui::EndMultiSelect(); - storage.ApplyRequests(io); + storage.end(); ImGui::PopStyleVar(); } @@ -139,13 +134,16 @@ namespace anm2ed::spritesheets auto rowOneWidgetSize = imgui::widget_size_with_row_get(4); - imgui::shortcut(settings.shortcutAdd, true); + imgui::shortcut(settings.shortcutAdd); if (ImGui::Button("Add", rowOneWidgetSize)) dialog.spritesheet_open(); imgui::set_item_tooltip_shortcut("Add a new spritesheet.", settings.shortcutAdd); if (dialog.is_selected_file(dialog::SPRITESHEET_OPEN)) { - manager.spritesheet_add(dialog.path); + int id{}; + anm2.spritesheet_add(document.directory_get(), dialog.path, id); + selection = {id}; + document.change(change::SPRITESHEETS); dialog.reset(); } @@ -186,19 +184,19 @@ namespace anm2ed::spritesheets ImGui::SameLine(); - auto unused = anm2.spritesheets_unused(); - - ImGui::BeginDisabled(unused.empty()); + ImGui::BeginDisabled(unusedSpritesheetIDs.empty()); { - imgui::shortcut(settings.shortcutRemove, true); + imgui::shortcut(settings.shortcutRemove); if (ImGui::Button("Remove Unused", rowOneWidgetSize)) { - for (auto& id : unused) + for (auto& id : unusedSpritesheetIDs) { Spritesheet& spritesheet = anm2.content.spritesheets[id]; toasts.add(std::format("Removed spritesheet #{}: {}", id, spritesheet.path.string())); anm2.spritesheet_remove(id); } + unusedSpritesheetIDs.clear(); + document.change(change::SPRITESHEETS); } imgui::set_item_tooltip_shortcut("Remove all unused spritesheets (i.e., not used in any layer.).", settings.shortcutRemove); @@ -207,7 +205,7 @@ namespace anm2ed::spritesheets auto rowTwoWidgetSize = imgui::widget_size_with_row_get(3); - imgui::shortcut(settings.shortcutSelectAll, true); + imgui::shortcut(settings.shortcutSelectAll); ImGui::BeginDisabled(selection.size() == anm2.content.spritesheets.size()); { if (ImGui::Button("Select All", rowTwoWidgetSize)) @@ -219,13 +217,11 @@ namespace anm2ed::spritesheets ImGui::SameLine(); - imgui::shortcut(settings.shortcutSelectNone, true); + imgui::shortcut(settings.shortcutSelectNone); ImGui::BeginDisabled(selection.empty()); - { - if (ImGui::Button("Select None", rowTwoWidgetSize)) selection.clear(); - } - ImGui::EndDisabled(); + if (ImGui::Button("Select None", rowTwoWidgetSize)) selection.clear(); imgui::set_item_tooltip_shortcut("Unselect all spritesheets.", settings.shortcutSelectNone); + ImGui::EndDisabled(); ImGui::SameLine(); @@ -235,7 +231,8 @@ namespace anm2ed::spritesheets { for (auto& id : selection) { - if (Spritesheet& spritesheet = anm2.content.spritesheets[id]; spritesheet.save(document.directory_get())) + Spritesheet& spritesheet = anm2.content.spritesheets[id]; + if (spritesheet.save(document.directory_get())) toasts.add(std::format("Saved spritesheet #{}: {}", id, spritesheet.path.string())); else toasts.add(std::format("Unable to save spritesheet #{}: {}", id, spritesheet.path.string())); diff --git a/src/spritesheets.h b/src/spritesheets.h index dae06b8..7bed4ce 100644 --- a/src/spritesheets.h +++ b/src/spritesheets.h @@ -1,7 +1,8 @@ #pragma once #include "dialog.h" -#include "document_manager.h" +#include "document.h" +#include "imgui.h" #include "resources.h" #include "settings.h" @@ -9,8 +10,11 @@ namespace anm2ed::spritesheets { class Spritesheets { + imgui::MultiSelectStorage storage{}; + std::set unusedSpritesheetIDs{}; + public: - void update(document_manager::DocumentManager& manager, settings::Settings& settings, - resources::Resources& resources, dialog::Dialog& dialog); + void update(document::Document& document, settings::Settings& settings, resources::Resources& resources, + dialog::Dialog& dialog); }; } diff --git a/src/state.cpp b/src/state.cpp index b698828..2a247b6 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -60,10 +60,11 @@ namespace anm2ed::state ImGui::NewFrame(); taskbar.update(settings, dialog, manager, isQuit); - documents.update(taskbar, manager, resources); dockspace.update(taskbar, documents, manager, settings, resources, dialog, playback); toasts.update(); + documents.update(taskbar, manager, resources); + ImGui::GetStyle().FontScaleMain = settings.displayScale; SDL_GetWindowSize(window, &settings.windowSize.x, &settings.windowSize.y); } diff --git a/src/taskbar.cpp b/src/taskbar.cpp index 2ef70d8..a769476 100644 --- a/src/taskbar.cpp +++ b/src/taskbar.cpp @@ -4,12 +4,10 @@ #include #include "imgui.h" -#include "types.h" using namespace anm2ed::settings; using namespace anm2ed::dialog; using namespace anm2ed::document_manager; -using namespace anm2ed::imgui; using namespace anm2ed::types; namespace anm2ed::taskbar @@ -91,41 +89,25 @@ namespace anm2ed::taskbar if (ImGui::BeginMenu("Settings")) { - if (ImGui::MenuItem("Configure")) isOpenConfigurePopup = true; + if (ImGui::MenuItem("Configure")) configurePopup.open(); ImGui::EndMenu(); } if (ImGui::BeginMenu("Help")) { - if (ImGui::MenuItem("About")) isOpenAboutPopup = true; + if (ImGui::MenuItem("About")) aboutPopup.open(); ImGui::EndMenu(); } ImGui::EndMainMenuBar(); } - if (isOpenAboutPopup) + configurePopup.trigger(); + + if (ImGui::BeginPopupModal(configurePopup.label, &configurePopup.isOpen, ImGuiWindowFlags_NoResize)) { - ImGui::OpenPopup("About"); - isOpenAboutPopup = false; - } - - if (isOpenConfigurePopup) - { - ImGui::OpenPopup("Configure"); - editSettings = settings; - isOpenConfigurePopup = false; - } - - auto viewport = ImGui::GetMainViewport(); - - ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * 0.5f)); - - if (ImGui::BeginPopupModal("Configure", nullptr, ImGuiWindowFlags_NoResize)) - { - auto childSize = imgui::size_with_footer_get(2); + auto childSize = imgui::size_without_footer_get(2); if (ImGui::BeginTabBar("##Configure Tabs")) { @@ -230,7 +212,7 @@ namespace anm2ed::taskbar if (ImGui::Button("Save", widgetSize)) { settings = editSettings; - ImGui::CloseCurrentPopup(); + configurePopup.close(); } ImGui::SetItemTooltip("Use the configured settings."); @@ -247,12 +229,11 @@ namespace anm2ed::taskbar ImGui::EndPopup(); } - ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_None, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(to_imvec2(to_vec2(viewport->Size) * 0.5f)); + aboutPopup.trigger(); - if (ImGui::BeginPopupModal("About", nullptr, ImGuiWindowFlags_NoResize)) + if (ImGui::BeginPopupModal(aboutPopup.label, &aboutPopup.isOpen, ImGuiWindowFlags_NoResize)) { - if (ImGui::Button("Close")) ImGui::CloseCurrentPopup(); + if (ImGui::Button("Close")) aboutPopup.close(); ImGui::EndPopup(); } diff --git a/src/taskbar.h b/src/taskbar.h index 146c0c6..257b5aa 100644 --- a/src/taskbar.h +++ b/src/taskbar.h @@ -2,16 +2,17 @@ #include "dialog.h" #include "document_manager.h" +#include "imgui.h" #include "settings.h" namespace anm2ed::taskbar { class Taskbar { - bool isOpenConfigurePopup{}; - bool isOpenAboutPopup{}; - int selectedShortcut{-1}; + imgui::PopupHelper configurePopup{imgui::PopupHelper("Configure")}; + imgui::PopupHelper aboutPopup{imgui::PopupHelper("About")}; settings::Settings editSettings{}; + int selectedShortcut{}; public: float height{}; diff --git a/src/timeline.cpp b/src/timeline.cpp index 0b965a1..ef50288 100644 --- a/src/timeline.cpp +++ b/src/timeline.cpp @@ -68,7 +68,8 @@ namespace anm2ed::timeline color = isActive ? ROOT_COLOR_ACTIVE : ROOT_COLOR; break; case anm2::LAYER: - label = std::format("#{} {}", id, anm2.content.layers[id].name); + label = std::format("#{} {} (Spritesheet: #{})", id, anm2.content.layers.at(id).name, + anm2.content.layers[id].spritesheetID); icon = icon::LAYER; color = isActive ? LAYER_COLOR_ACTIVE : LAYER_COLOR; break; @@ -165,10 +166,12 @@ namespace anm2ed::timeline ImGui::SetCursorPos(cursorPos); + ImGui::BeginDisabled(); ImGui::Text("(?)"); ImGui::SetItemTooltip("%s", std::format(HELP_FORMAT, settings.shortcutNextFrame, settings.shortcutPreviousFrame, settings.shortcutShortenFrame, settings.shortcutExtendFrame) .c_str()); + ImGui::EndDisabled(); } } ImGui::EndChild(); @@ -177,9 +180,11 @@ namespace anm2ed::timeline index++; } - void Timeline::items_child(anm2::Anm2& anm2, anm2::Reference& reference, anm2::Animation* animation, - Settings& settings, Resources& resources) + void Timeline::items_child(Document& document, anm2::Animation* animation, Settings& settings, Resources& resources) { + auto& anm2 = document.anm2; + auto& reference = document.reference; + auto itemsChildSize = ImVec2(ImGui::GetTextLineHeightWithSpacing() * 15, ImGui::GetContentRegionAvail().y); if (ImGui::BeginChild("##Items Child", itemsChildSize, ImGuiChildFlags_Borders)) @@ -250,13 +255,24 @@ namespace anm2ed::timeline auto widgetSize = imgui::widget_size_with_row_get(2); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); + ImGui::BeginDisabled(!animation); { - ImGui::Button("Add", widgetSize); + if (ImGui::Button("Add", widgetSize)) propertiesPopup.open(); + ImGui::SetItemTooltip("%s", "Add a new item to the animation."); ImGui::SameLine(); - ImGui::Button("Remove", widgetSize); + + ImGui::BeginDisabled(document.item_get()); + { + ImGui::Button("Remove", widgetSize); + ImGui::SetItemTooltip("%s", "Remove the selected items from the animation."); + } + ImGui::EndDisabled(); } ImGui::EndDisabled(); + + ImGui::PopStyleVar(); } ImGui::EndChild(); } @@ -317,10 +333,6 @@ namespace anm2ed::timeline colorActiveHidden = to_imvec4(to_vec4(colorActive) * COLOR_HIDDEN_MULTIPLIER); colorHoveredHidden = to_imvec4(to_vec4(colorHovered) * COLOR_HIDDEN_MULTIPLIER); - ImGui::PushStyleColor(ImGuiCol_Button, isVisible ? color : colorHidden); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, isVisible ? colorActive : colorActiveHidden); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, isVisible ? colorHovered : colorHoveredHidden); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); if (ImGui::BeginChild("##Frames Child", childSize, ImGuiChildFlags_Borders)) @@ -420,11 +432,10 @@ namespace anm2ed::timeline { anm2::Reference frameReference = {reference.animationIndex, type, id, (int)i}; auto isSelected = baseReference == frameReference; + auto isFrameVisible = isVisible && frame.isVisible; frameTime += frame.delay; - if (isSelected) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); - ImGui::PushID(i); auto size = ImVec2(frameSize.x * frame.delay, frameSize.y); @@ -435,12 +446,11 @@ namespace anm2ed::timeline if (type == anm2::TRIGGER) ImGui::SetCursorPos(ImVec2(cursorPos.x + frameSize.x * frame.atFrame, cursorPos.y)); - if (!frame.isVisible) - { - ImGui::PushStyleColor(ImGuiCol_Button, colorHidden); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, colorActiveHidden); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colorHoveredHidden); - } + ImGui::PushStyleColor(ImGuiCol_Button, isFrameVisible ? color : colorHidden); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, isFrameVisible ? colorActive : colorActiveHidden); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, isFrameVisible ? colorHovered : colorHoveredHidden); + + if (isSelected) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); if (ImGui::Button("##Frame Button", size)) { @@ -451,7 +461,8 @@ namespace anm2ed::timeline } if (type != anm2::TRIGGER) ImGui::SameLine(); - if (!frame.isVisible) ImGui::PopStyleColor(3); + ImGui::PopStyleColor(3); + if (isSelected) ImGui::PopStyleColor(); auto imageMin = ImVec2(ImGui::GetItemRectMin().x, ImGui::GetItemRectMax().y - (ImGui::GetItemRectSize().y / 2) - (imageSize.y / 2)); @@ -459,15 +470,12 @@ namespace anm2ed::timeline drawList->AddImage(resources.icons[icon].id, imageMin, imageMax); - if (isSelected) ImGui::PopStyleColor(); - ImGui::PopID(); } } } ImGui::EndChild(); ImGui::PopStyleVar(); - ImGui::PopStyleColor(3); index++; ImGui::PopID(); @@ -600,19 +608,19 @@ namespace anm2ed::timeline auto label = playback.isPlaying ? "Pause" : "Play"; auto tooltip = playback.isPlaying ? "Pause the animation." : "Play the animation."; - imgui::shortcut(settings.shortcutPlayPause, true); + imgui::shortcut(settings.shortcutPlayPause); if (ImGui::Button(label, widgetSize)) playback.toggle(); imgui::set_item_tooltip_shortcut(tooltip, settings.shortcutPlayPause); ImGui::SameLine(); - imgui::shortcut(settings.shortcutAdd, true); + imgui::shortcut(settings.shortcutAdd); ImGui::Button("Insert Frame", widgetSize); imgui::set_item_tooltip_shortcut("Insert a frame, based on the current selection.", settings.shortcutAdd); ImGui::SameLine(); - imgui::shortcut(settings.shortcutRemove, true); + imgui::shortcut(settings.shortcutRemove); ImGui::Button("Delete Frame", widgetSize); imgui::set_item_tooltip_shortcut("Delete the selected frames.", settings.shortcutRemove); @@ -662,6 +670,187 @@ namespace anm2ed::timeline ImGui::SetCursorPos(cursorPos); } + void Timeline::popups(Document& document, anm2::Animation* animation, Settings& settings) + { + auto item_properties_reset = [&]() + { + addItemName.clear(); + addItemSpritesheetID = {}; + addItemID = -1; + isUnusedItemsSet = false; + }; + + auto& anm2 = document.anm2; + auto& reference = document.reference; + + propertiesPopup.trigger(); + + if (ImGui::BeginPopupModal(propertiesPopup.label, &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) + { + auto item_properties_close = [&]() + { + item_properties_reset(); + propertiesPopup.close(); + }; + + auto& type = settings.timelineAddItemType; + auto& locale = settings.timelineAddItemLocale; + auto& source = settings.timelineAddItemSource; + + if (!isUnusedItemsSet) + { + unusedItems = type == anm2::LAYER ? anm2.layers_unused(reference) + : type == anm2::NULL_ ? anm2.nulls_unused(reference) + : std::set{}; + + isUnusedItemsSet = true; + } + + auto footerSize = imgui::footer_size_get(); + auto optionsSize = imgui::child_size_get(11); + auto itemsSize = ImVec2(0, ImGui::GetContentRegionAvail().y - + (optionsSize.y + footerSize.y + ImGui::GetStyle().ItemSpacing.y * 4)); + + if (ImGui::BeginChild("Options", optionsSize, ImGuiChildFlags_Borders)) + { + ImGui::SeparatorText("Type"); + + auto size = ImVec2(ImGui::GetContentRegionAvail().x * 0.5f, ImGui::GetFrameHeightWithSpacing()); + + if (ImGui::BeginChild("Type Layer", size)) + { + ImGui::RadioButton("Layer", &type, anm2::LAYER); + ImGui::SetItemTooltip("Layers are a basic visual element in an animation, used for displaying spritesheets."); + } + ImGui::EndChild(); + + ImGui::SameLine(); + + if (ImGui::BeginChild("Type Null", size)) + { + ImGui::RadioButton("Null", &type, anm2::NULL_); + ImGui::SetItemTooltip( + "Nulls are invisible elements in an animation, used for interfacing with a game engine."); + } + ImGui::EndChild(); + + ImGui::SeparatorText("Source"); + + bool isNewOnly = unusedItems.empty(); + if (isNewOnly) source = source::NEW; + + if (ImGui::BeginChild("Source New", size)) + { + ImGui::RadioButton("New", &source, source::NEW); + ImGui::SetItemTooltip("Create a new item to be used."); + } + ImGui::EndChild(); + + ImGui::SameLine(); + + if (ImGui::BeginChild("Source Existing", size)) + { + ImGui::BeginDisabled(isNewOnly); + ImGui::RadioButton("Existing", &source, source::EXISTING); + ImGui::EndDisabled(); + ImGui::SetItemTooltip("Use a pre-existing, presently unused item."); + } + ImGui::EndChild(); + + ImGui::SeparatorText("Locale"); + + if (ImGui::BeginChild("Locale Global", size)) + { + ImGui::RadioButton("Global", &locale, locale::GLOBAL); + ImGui::SetItemTooltip("The item will be inserted into all animations, if not already present."); + } + ImGui::EndChild(); + + ImGui::SameLine(); + + if (ImGui::BeginChild("Locale Local", size)) + { + ImGui::RadioButton("Local", &locale, locale::LOCAL); + ImGui::SetItemTooltip("The item will only be inserted into this animation."); + } + ImGui::EndChild(); + + ImGui::SeparatorText("Options"); + + ImGui::BeginDisabled(source == source::EXISTING); + { + imgui::input_text_string("Name", &addItemName); + ImGui::SetItemTooltip("Set the item's name."); + ImGui::BeginDisabled(type != anm2::LAYER); + { + auto spritesheets = anm2.spritesheet_names_get(); + imgui::combo_strings("Spritesheet", &addItemSpritesheetID, spritesheets); + ImGui::SetItemTooltip("Set the layer item's spritesheet."); + } + ImGui::EndDisabled(); + } + ImGui::EndDisabled(); + } + ImGui::EndChild(); + + if (ImGui::BeginChild("Items", itemsSize, ImGuiChildFlags_Borders)) + { + if (animation && source == source::EXISTING) + { + for (auto id : unusedItems) + { + auto isSelected = addItemID == id; + + ImGui::PushID(id); + + if (type == anm2::LAYER) + { + auto& layer = anm2.content.layers[id]; + if (ImGui::Selectable( + std::format("#{} {} (Spritesheet: #{})", id, layer.name, layer.spritesheetID).c_str(), + isSelected)) + addItemID = id; + } + else if (type == anm2::NULL_) + { + auto& null = anm2.content.nulls[id]; + if (ImGui::Selectable(std::format("#{} {}", id, null.name).c_str(), isSelected)) addItemID = id; + } + + ImGui::PopID(); + } + } + } + ImGui::EndChild(); + + auto widgetSize = imgui::widget_size_with_row_get(2); + + if (ImGui::Button("Add", widgetSize)) + { + anm2::Reference addReference; + + if (type == anm2::LAYER) + addReference = anm2.layer_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, + addItemSpritesheetID, (locale::Type)locale); + else if (type == anm2::NULL_) + addReference = + anm2.null_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, (locale::Type)locale); + + reference = addReference; + + item_properties_close(); + } + ImGui::SetItemTooltip("Add the item, with the settings specified."); + + ImGui::SameLine(); + + if (ImGui::Button("Cancel", widgetSize)) item_properties_close(); + ImGui::SetItemTooltip("Cancel adding an item."); + + ImGui::EndPopup(); + } + } + void Timeline::update(DocumentManager& manager, Settings& settings, Resources& resources, Playback& playback) { auto& document = *manager.get(); @@ -676,20 +865,28 @@ namespace anm2ed::timeline { isWindowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); frames_child(document, animation, settings, resources, playback); - items_child(anm2, reference, animation, settings, resources); + items_child(document, animation, settings, resources); } ImGui::PopStyleVar(); ImGui::End(); - if (imgui::shortcut(settings.shortcutPlayPause, false, true)) playback.toggle(); + popups(document, animation, settings); + + if (imgui::shortcut(settings.shortcutPlayPause, shortcut::GLOBAL)) playback.toggle(); if (animation) { if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutPreviousFrame))) - playback.decrement(animation->frameNum); + { + playback.decrement(settings.playbackIsClampPlayhead ? animation->frameNum : anm2::FRAME_NUM_MAX); + reference.frameTime = playback.time; + } if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutNextFrame))) - playback.increment(animation->frameNum); + { + playback.increment(settings.playbackIsClampPlayhead ? animation->frameNum : anm2::FRAME_NUM_MAX); + reference.frameTime = playback.time; + } } if (imgui::chord_repeating(imgui::string_to_chord(settings.shortcutShortenFrame))) diff --git a/src/timeline.h b/src/timeline.h index 5b32711..799a6e7 100644 --- a/src/timeline.h +++ b/src/timeline.h @@ -3,6 +3,7 @@ #include "anm2.h" #include "document.h" #include "document_manager.h" +#include "imgui.h" #include "playback.h" #include "resources.h" #include "settings.h" @@ -14,20 +15,29 @@ namespace anm2ed::timeline bool isDragging{}; bool isWindowHovered{}; bool isHorizontalScroll{}; + imgui::PopupHelper propertiesPopup{imgui::PopupHelper("Item Properties")}; + std::string addItemName{}; + int addItemSpritesheetID{}; + bool addItemIsRect{}; + int addItemID{-1}; + bool isUnusedItemsSet{}; + std::set unusedItems{}; glm::vec2 scroll{}; ImDrawList* pickerLineDrawList{}; ImGuiStyle style{}; void item_child(anm2::Anm2& anm2, anm2::Reference& reference, anm2::Animation* animation, settings::Settings& settings, resources::Resources& resources, anm2::Type type, int id, int& index); - void items_child(anm2::Anm2& anm2, anm2::Reference& reference, anm2::Animation* animation, - settings::Settings& settings, resources::Resources& resources); + void items_child(Document& document, anm2::Animation* animation, settings::Settings& settings, + resources::Resources& resources); void frame_child(document::Document& document, anm2::Animation* animation, settings::Settings& settings, resources::Resources& resources, playback::Playback& playback, anm2::Type type, int id, int& index, float width); void frames_child(document::Document& document, anm2::Animation* animation, settings::Settings& settings, resources::Resources& resources, playback::Playback& playback); + void popups(document::Document& document, anm2::Animation* animation, settings::Settings& settings); + public: void update(document_manager::DocumentManager& manager, settings::Settings& settings, resources::Resources& resources, playback::Playback& playback); diff --git a/src/tools.cpp b/src/tools.cpp index 09eb1db..3e32d0f 100644 --- a/src/tools.cpp +++ b/src/tools.cpp @@ -55,7 +55,7 @@ namespace anm2ed::tools auto member = SHORTCUT_MEMBERS[info.shortcut]; - if (imgui::shortcut(settings.*member, false, true, i == tool::COLOR ? false : true)) tool_switch((tool::Type)i); + if (imgui::shortcut(settings.*member, shortcut::GLOBAL_SET)) tool_switch((tool::Type)i); if (i == tool::COLOR) { diff --git a/src/types.h b/src/types.h index a22daa7..a4b7b82 100644 --- a/src/types.h +++ b/src/types.h @@ -4,6 +4,50 @@ #include #include +namespace anm2ed::types::change +{ + enum Type + { + LAYERS, + NULLS, + SPRITESHEETS, + EVENTS, + ANIMATIONS, + ITEMS, + FRAMES, + COUNT + }; +} + +namespace anm2ed::types::shortcut +{ + enum Type + { + FOCUSED, + GLOBAL, + FOCUSED_SET, + GLOBAL_SET + }; +} + +namespace anm2ed::types::locale +{ + enum Type + { + LOCAL, + GLOBAL + }; +} + +namespace anm2ed::types::source +{ + enum Type + { + NEW, + EXISTING + }; +} + namespace anm2ed::types::merge { enum Type