From 07096c487bfa1397182743eba0272482b1fc162d Mon Sep 17 00:00:00 2001 From: shweet Date: Tue, 11 Nov 2025 11:25:46 -0500 Subject: [PATCH] fuck git and fuck vs code --- CMakeLists.txt | 11 +- src/anm2/animation.cpp | 2 + src/anm2/animation.h | 3 +- src/anm2/anm2.cpp | 48 +- src/anm2/anm2.h | 16 +- src/anm2/anm2_animations.cpp | 5 +- src/anm2/anm2_events.cpp | 10 +- src/anm2/anm2_items.cpp | 14 +- src/anm2/anm2_layers.cpp | 20 +- src/anm2/anm2_nulls.cpp | 20 +- src/anm2/anm2_spritesheets.cpp | 5 - src/anm2/content.cpp | 9 + src/anm2/content.h | 1 + src/anm2/frame.cpp | 12 +- src/anm2/frame.h | 7 +- src/anm2/item.cpp | 110 ++- src/anm2/item.h | 2 + src/anm2/sound.cpp | 25 +- src/anm2/sound.h | 1 + src/anm2/spritesheet.cpp | 10 +- src/document.cpp | 69 +- src/document.h | 5 +- src/imgui/dockspace.cpp | 2 +- src/imgui/documents.cpp | 6 +- src/imgui/imgui_.cpp | 6 +- src/imgui/imgui_.h | 2 +- src/imgui/taskbar.cpp | 686 ++++++--------- src/imgui/taskbar.h | 14 +- src/imgui/window/animation_preview.cpp | 20 +- src/imgui/window/animations.cpp | 45 +- src/imgui/window/events.cpp | 8 +- src/imgui/window/frame_properties.cpp | 291 +++++-- src/imgui/window/layers.cpp | 8 +- src/imgui/window/nulls.cpp | 8 +- src/imgui/window/onionskin.cpp | 4 +- src/imgui/window/onionskin.h | 4 +- src/imgui/window/sounds.cpp | 8 +- src/imgui/window/spritesheet_editor.cpp | 8 +- src/imgui/window/spritesheets.cpp | 34 +- src/imgui/window/timeline.cpp | 1009 +++++++++++++++-------- src/imgui/window/timeline.h | 22 +- src/imgui/window/tools.cpp | 4 +- src/loader.cpp | 5 +- src/main.cpp | 2 +- src/manager.cpp | 6 + src/manager.h | 5 + src/playback.cpp | 5 +- src/resource/audio.cpp | 149 +++- src/resource/audio.h | 7 +- src/resource/icon.h | 14 - src/resources.cpp | 4 + src/resources.h | 2 + src/settings.h | 24 +- src/snapshots.h | 3 +- src/state.cpp | 4 +- src/state.h | 2 +- src/storage.cpp | 2 + src/storage.h | 10 +- src/types.h | 31 +- src/util/filesystem_.cpp | 27 +- src/util/filesystem_.h | 10 - src/util/vector_.h | 30 +- 62 files changed, 1635 insertions(+), 1301 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1781c6d..7fd8ceb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ set(SDLMIXER_INSTALL OFF CACHE BOOL "" FORCE) add_subdirectory(external/SDL_mixer EXCLUDE_FROM_ALL) add_subdirectory(external/lunasvg) +add_subdirectory(external/libxm/src EXCLUDE_FROM_ALL) set(GLAD_SRC ${CMAKE_CURRENT_SOURCE_DIR}/include/glad/glad.cpp) @@ -82,7 +83,6 @@ add_executable(${PROJECT_NAME} ${PROJECT_SRC} ) - if (WIN32) enable_language(RC) target_sources(${PROJECT_NAME} PRIVATE Icon.rc) @@ -114,6 +114,13 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE IMGUI_ENABLE_DOCKING ) + +if (MSVC) + target_compile_definitions(${PROJECT_NAME} PRIVATE restrict=__restrict) +else () + target_compile_definitions(${PROJECT_NAME} PRIVATE restrict=__restrict__) +endif () + target_include_directories(${PROJECT_NAME} PRIVATE external external/imgui @@ -130,7 +137,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE src/util ) -target_link_libraries(${PROJECT_NAME} PRIVATE GL SDL3-static SDL3_mixer::SDL3_mixer lunasvg) +target_link_libraries(${PROJECT_NAME} PRIVATE GL SDL3-static SDL3_mixer::SDL3_mixer lunasvg xm) message(STATUS "System: ${CMAKE_SYSTEM_NAME}") message(STATUS "Project: ${PROJECT_NAME}") diff --git a/src/anm2/animation.cpp b/src/anm2/animation.cpp index 61d2bb6..4098953 100644 --- a/src/anm2/animation.cpp +++ b/src/anm2/animation.cpp @@ -134,6 +134,8 @@ namespace anm2ed::anm2 return length; } + void Animation::fit_length() { frameNum = length(); } + vec4 Animation::rect(bool isRootTransform) { constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; diff --git a/src/anm2/animation.h b/src/anm2/animation.h index 0cc68a8..9502f63 100644 --- a/src/anm2/animation.h +++ b/src/anm2/animation.h @@ -8,7 +8,7 @@ namespace anm2ed::anm2 { constexpr auto FRAME_NUM_MIN = 1; - constexpr auto FRAME_NUM_MAX = FRAME_DELAY_MAX; + constexpr auto FRAME_NUM_MAX = FRAME_DURATION_MAX; class Animation { @@ -30,6 +30,7 @@ namespace anm2ed::anm2 void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*); std::string to_string(); int length(); + void fit_length(); glm::vec4 rect(bool); }; diff --git a/src/anm2/anm2.cpp b/src/anm2/anm2.cpp index 79963a5..7543fba 100644 --- a/src/anm2/anm2.cpp +++ b/src/anm2/anm2.cpp @@ -12,10 +12,7 @@ using namespace glm; namespace anm2ed::anm2 { - Anm2::Anm2() - { - info.createdOn = time::get("%d-%B-%Y %I:%M:%S"); - } + Anm2::Anm2() { info.createdOn = time::get("%d-%B-%Y %I:%M:%S"); } Anm2::Anm2(const std::string& path, std::string* errorString) { @@ -37,25 +34,6 @@ namespace anm2ed::anm2 animations = Animations((XMLElement*)animationsElement); } - bool Anm2::serialize(const std::string& path, std::string* errorString) - { - XMLDocument document; - - auto* element = document.NewElement("AnimatedActor"); - document.InsertFirstChild(element); - - info.serialize(document, element); - content.serialize(document, element); - animations.serialize(document, element); - - if (document.SaveFile(path.c_str()) != XML_SUCCESS) - { - if (errorString) *errorString = document.ErrorStr(); - return false; - } - return true; - } - XMLElement* Anm2::to_element(XMLDocument& document) { auto element = document.NewElement("AnimatedActor"); @@ -68,6 +46,19 @@ namespace anm2ed::anm2 return element; } + bool Anm2::serialize(const std::string& path, std::string* errorString) + { + XMLDocument document; + document.InsertFirstChild(to_element(document)); + + if (document.SaveFile(path.c_str()) != XML_SUCCESS) + { + if (errorString) *errorString = document.ErrorStr(); + return false; + } + return true; + } + std::string Anm2::to_string() { XMLDocument document{}; @@ -75,15 +66,12 @@ namespace anm2ed::anm2 return xml::document_to_string(document); } - uint64_t Anm2::hash() - { - return std::hash{}(to_string()); - } + uint64_t Anm2::hash() { return std::hash{}(to_string()); } - Frame* Anm2::frame_get(Reference reference) + Frame* Anm2::frame_get(int animationIndex, Type itemType, int frameIndex, int itemID) { - if (auto item = item_get(reference); item) - if (vector::in_bounds(item->frames, reference.frameIndex)) return &item->frames[reference.frameIndex]; + if (auto item = item_get(animationIndex, itemType, itemID); item) + if (vector::in_bounds(item->frames, frameIndex)) return &item->frames[frameIndex]; return nullptr; } } \ No newline at end of file diff --git a/src/anm2/anm2.h b/src/anm2/anm2.h index 3f970ff..a4191aa 100644 --- a/src/anm2/anm2.h +++ b/src/anm2/anm2.h @@ -19,7 +19,6 @@ namespace anm2ed::anm2 Type itemType{NONE}; int itemID{-1}; int frameIndex{-1}; - int frameTime{-1}; auto operator<=>(const Reference&) const = default; }; @@ -40,22 +39,23 @@ namespace anm2ed::anm2 Spritesheet* spritesheet_get(int); bool spritesheet_add(const std::string&, const std::string&, int&); - void spritesheet_remove(int); std::vector spritesheet_labels_get(); std::set spritesheets_unused(); bool spritesheets_deserialize(const std::string&, const std::string&, types::merge::Type type, std::string*); void layer_add(int&); - std::set layers_unused(Reference = {}); + std::set layers_unused(); + std::set layers_unused(Animation&); bool layers_deserialize(const std::string&, types::merge::Type, std::string*); void null_add(int&); - std::set nulls_unused(Reference = {}); + std::set nulls_unused(); + std::set nulls_unused(Animation&); bool nulls_deserialize(const std::string&, types::merge::Type, std::string*); void event_add(int&); std::vector event_labels_get(); - std::set events_unused(Reference = {}); + std::set events_unused(); bool events_deserialize(const std::string&, types::merge::Type, std::string*); bool sound_add(const std::string& directory, const std::string& path, int& id); @@ -63,16 +63,16 @@ namespace anm2ed::anm2 std::set sounds_unused(); bool sounds_deserialize(const std::string&, const std::string&, types::merge::Type, std::string*); - Animation* animation_get(Reference); + Animation* animation_get(int); std::vector animation_labels_get(); int animations_merge(int, std::set&, types::merge::Type = types::merge::APPEND, bool = true); bool animations_deserialize(const std::string&, int, std::set&, std::string* = nullptr); - Item* item_get(Reference); + Item* item_get(int, Type, int = -1); Reference layer_animation_add(Reference = {}, std::string = {}, int = 0, types::locale::Type = types::locale::GLOBAL); Reference null_animation_add(Reference = {}, std::string = {}, types::locale::Type = types::locale::GLOBAL); - Frame* frame_get(Reference); + Frame* frame_get(int, Type, int, int = -1); }; } diff --git a/src/anm2/anm2_animations.cpp b/src/anm2/anm2_animations.cpp index 11762d9..2d79033 100644 --- a/src/anm2/anm2_animations.cpp +++ b/src/anm2/anm2_animations.cpp @@ -8,10 +8,7 @@ using namespace tinyxml2; namespace anm2ed::anm2 { - Animation* Anm2::animation_get(Reference reference) - { - return vector::find(animations.items, reference.animationIndex); - } + Animation* Anm2::animation_get(int animationIndex) { return vector::find(animations.items, animationIndex); } std::vector Anm2::animation_labels_get() { diff --git a/src/anm2/anm2_events.cpp b/src/anm2/anm2_events.cpp index 903e6d8..1bfba78 100644 --- a/src/anm2/anm2_events.cpp +++ b/src/anm2/anm2_events.cpp @@ -25,17 +25,13 @@ namespace anm2ed::anm2 return labels; } - std::set Anm2::events_unused(Reference reference) + std::set Anm2::events_unused() { std::set used{}; - if (auto animation = animation_get(reference); animation) - for (auto& frame : animation->triggers.frames) + for (auto& animation : animations.items) + 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) diff --git a/src/anm2/anm2_items.cpp b/src/anm2/anm2_items.cpp index b74df0e..f0d7741 100644 --- a/src/anm2/anm2_items.cpp +++ b/src/anm2/anm2_items.cpp @@ -9,18 +9,18 @@ using namespace anm2ed::util; namespace anm2ed::anm2 { - Item* Anm2::item_get(Reference reference) + Item* Anm2::item_get(int animationIndex, Type type, int id) { - if (Animation* animation = animation_get(reference)) + if (Animation* animation = animation_get(animationIndex)) { - switch (reference.itemType) + switch (type) { case ROOT: return &animation->rootAnimation; case LAYER: - return unordered_map::find(animation->layerAnimations, reference.itemID); + return unordered_map::find(animation->layerAnimations, id); case NULL_: - return map::find(animation->nullAnimations, reference.itemID); + return map::find(animation->nullAnimations, id); case TRIGGER: return &animation->triggers; default: @@ -51,7 +51,7 @@ namespace anm2ed::anm2 } else if (locale == locale::LOCAL) { - if (auto animation = animation_get(reference)) + if (auto animation = animation_get(reference.animationIndex)) if (!animation->layerAnimations.contains(id)) add(animation, id); } @@ -74,7 +74,7 @@ namespace anm2ed::anm2 } else if (locale == locale::LOCAL) { - if (auto animation = animation_get(reference)) + if (auto animation = animation_get(reference.animationIndex)) if (!animation->nullAnimations.contains(id)) add(animation, id); } diff --git a/src/anm2/anm2_layers.cpp b/src/anm2/anm2_layers.cpp index 9a48ac1..14ec4a1 100644 --- a/src/anm2/anm2_layers.cpp +++ b/src/anm2/anm2_layers.cpp @@ -16,18 +16,14 @@ namespace anm2ed::anm2 content.layers[id] = Layer(); } - std::set Anm2::layers_unused(Reference reference) + std::set Anm2::layers_unused() { std::set used{}; std::set unused{}; - if (auto animation = animation_get(reference); animation) - for (auto& id : animation->layerAnimations | std::views::keys) + for (auto& animation : animations.items) + 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); for (auto& id : content.layers | std::views::keys) if (!used.contains(id)) unused.insert(id); @@ -35,6 +31,16 @@ namespace anm2ed::anm2 return unused; } + std::set Anm2::layers_unused(Animation& animation) + { + std::set unused{}; + + for (auto& id : content.layers | std::views::keys) + if (!animation.layerAnimations.contains(id)) unused.insert(id); + + return unused; + } + bool Anm2::layers_deserialize(const std::string& string, merge::Type type, std::string* errorString) { XMLDocument document{}; diff --git a/src/anm2/anm2_nulls.cpp b/src/anm2/anm2_nulls.cpp index 49ed6ce..126c7a9 100644 --- a/src/anm2/anm2_nulls.cpp +++ b/src/anm2/anm2_nulls.cpp @@ -16,18 +16,14 @@ namespace anm2ed::anm2 content.nulls[id] = Null(); } - std::set Anm2::nulls_unused(Reference reference) + std::set Anm2::nulls_unused() { std::set used{}; std::set unused{}; - if (auto animation = animation_get(reference); animation) - for (auto& id : animation->nullAnimations | std::views::keys) + for (auto& animation : animations.items) + 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); for (auto& id : content.nulls | std::views::keys) if (!used.contains(id)) unused.insert(id); @@ -35,6 +31,16 @@ namespace anm2ed::anm2 return unused; } + std::set Anm2::nulls_unused(Animation& animation) + { + std::set unused{}; + + for (auto& id : content.nulls | std::views::keys) + if (!animation.nullAnimations.contains(id)) unused.insert(id); + + return unused; + } + bool Anm2::nulls_deserialize(const std::string& string, merge::Type type, std::string* errorString) { XMLDocument document{}; diff --git a/src/anm2/anm2_spritesheets.cpp b/src/anm2/anm2_spritesheets.cpp index 9849db2..288d994 100644 --- a/src/anm2/anm2_spritesheets.cpp +++ b/src/anm2/anm2_spritesheets.cpp @@ -16,11 +16,6 @@ namespace anm2ed::anm2 return map::find(content.spritesheets, id); } - void Anm2::spritesheet_remove(int id) - { - content.spritesheets.erase(id); - } - bool Anm2::spritesheet_add(const std::string& directory, const std::string& path, int& id) { Spritesheet spritesheet(directory, path); diff --git a/src/anm2/content.cpp b/src/anm2/content.cpp index ff007ed..ff1b87b 100644 --- a/src/anm2/content.cpp +++ b/src/anm2/content.cpp @@ -24,6 +24,10 @@ namespace anm2ed::anm2 if (auto eventsElement = element->FirstChildElement("Events")) for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event")) events[id] = Event(child, id); + + if (auto eventsElement = element->FirstChildElement("Sounds")) + for (auto child = eventsElement->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound")) + sounds[id] = Sound(child, id); } void Content::serialize(XMLDocument& document, XMLElement* parent) @@ -50,6 +54,11 @@ namespace anm2ed::anm2 event.serialize(document, eventsElement, id); element->InsertEndChild(eventsElement); + auto soundsElement = document.NewElement("Sounds"); + for (auto& [id, sound] : sounds) + sound.serialize(document, soundsElement, id); + element->InsertEndChild(soundsElement); + parent->InsertEndChild(element); } diff --git a/src/anm2/content.h b/src/anm2/content.h index 330c433..88752fd 100644 --- a/src/anm2/content.h +++ b/src/anm2/content.h @@ -2,6 +2,7 @@ #include +#include "anm2_type.h" #include "event.h" #include "layer.h" #include "null.h" diff --git a/src/anm2/frame.cpp b/src/anm2/frame.cpp index 746a308..70bcb9d 100644 --- a/src/anm2/frame.cpp +++ b/src/anm2/frame.cpp @@ -25,7 +25,7 @@ namespace anm2ed::anm2 } element->QueryFloatAttribute("XScale", &scale.x); element->QueryFloatAttribute("YScale", &scale.y); - element->QueryIntAttribute("Delay", &delay); + element->QueryIntAttribute("Delay", &duration); element->QueryBoolAttribute("Visible", &isVisible); xml::query_color_attribute(element, "RedTint", tint.r); xml::query_color_attribute(element, "GreenTint", tint.g); @@ -40,6 +40,7 @@ namespace anm2ed::anm2 else { element->QueryIntAttribute("EventId", &eventID); + element->QueryIntAttribute("SoundId", &soundID); element->QueryIntAttribute("AtFrame", &atFrame); } } @@ -54,7 +55,7 @@ namespace anm2ed::anm2 case NULL_: element->SetAttribute("XPosition", position.x); element->SetAttribute("YPosition", position.y); - element->SetAttribute("Delay", delay); + element->SetAttribute("Delay", duration); element->SetAttribute("Visible", isVisible); element->SetAttribute("XScale", scale.x); element->SetAttribute("YScale", scale.y); @@ -79,7 +80,7 @@ namespace anm2ed::anm2 element->SetAttribute("Height", size.y); element->SetAttribute("XScale", scale.x); element->SetAttribute("YScale", scale.y); - element->SetAttribute("Delay", delay); + element->SetAttribute("Delay", duration); element->SetAttribute("Visible", isVisible); element->SetAttribute("RedTint", math::float_to_uint8(tint.r)); element->SetAttribute("GreenTint", math::float_to_uint8(tint.g)); @@ -93,6 +94,7 @@ namespace anm2ed::anm2 break; case TRIGGER: element->SetAttribute("EventId", eventID); + element->SetAttribute("SoundId", soundID); element->SetAttribute("AtFrame", atFrame); break; default: @@ -114,9 +116,9 @@ namespace anm2ed::anm2 return xml::document_to_string(document); } - void Frame::shorten() { delay = glm::clamp(--delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); } + void Frame::shorten() { duration = glm::clamp(--duration, FRAME_DURATION_MIN, FRAME_DURATION_MAX); } - void Frame::extend() { delay = glm::clamp(++delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); } + void Frame::extend() { duration = glm::clamp(++duration, FRAME_DURATION_MIN, FRAME_DURATION_MAX); } bool Frame::is_visible(Type type) { diff --git a/src/anm2/frame.h b/src/anm2/frame.h index f17a086..7149641 100644 --- a/src/anm2/frame.h +++ b/src/anm2/frame.h @@ -9,14 +9,14 @@ namespace anm2ed::anm2 { - constexpr auto FRAME_DELAY_MIN = 1; - constexpr auto FRAME_DELAY_MAX = 100000; + constexpr auto FRAME_DURATION_MIN = 1; + constexpr auto FRAME_DURATION_MAX = 100000; #define MEMBERS \ X(isVisible, bool, true) \ X(isInterpolated, bool, false) \ X(rotation, float, 0.0f) \ - X(delay, int, FRAME_DELAY_MIN) \ + X(duration, int, FRAME_DURATION_MIN) \ X(atFrame, int, -1) \ X(eventID, int, -1) \ X(soundID, int, -1) \ @@ -53,5 +53,4 @@ namespace anm2ed::anm2 }; #undef MEMBERS - } \ No newline at end of file diff --git a/src/anm2/item.cpp b/src/anm2/item.cpp index c569091..a66cae6 100644 --- a/src/anm2/item.cpp +++ b/src/anm2/item.cpp @@ -1,4 +1,5 @@ #include "item.h" +#include #include #include "vector_.h" @@ -30,6 +31,8 @@ namespace anm2ed::anm2 if (type == NULL_) element->SetAttribute("NullId", id); if (type == LAYER || type == NULL_) element->SetAttribute("Visible", isVisible); + if (type == TRIGGER) frames_sort_by_at_frame(); + for (auto& frame : frames) frame.serialize(document, element, type); @@ -57,11 +60,16 @@ namespace anm2ed::anm2 length = frame.atFrame > length ? frame.atFrame : length; else for (auto& frame : frames) - length += frame.delay; + length += frame.duration; return length; } + void Item::frames_sort_by_at_frame() + { + std::sort(frames.begin(), frames.end(), [](const Frame& a, const Frame& b) { return a.atFrame < b.atFrame; }); + } + Frame Item::frame_generate(float time, Type type) { Frame frame{}; @@ -70,8 +78,8 @@ namespace anm2ed::anm2 if (frames.empty()) return frame; Frame* frameNext = nullptr; - int delayCurrent = 0; - int delayNext = 0; + int durationCurrent = 0; + int durationNext = 0; for (auto [i, iFrame] : std::views::enumerate(frames)) { @@ -87,9 +95,9 @@ namespace anm2ed::anm2 { frame = iFrame; - delayNext += frame.delay; + durationNext += frame.duration; - if (time >= delayCurrent && time < delayNext) + if (time >= durationCurrent && time < durationNext) { if (i + 1 < (int)frames.size()) frameNext = &frames[i + 1]; @@ -98,13 +106,13 @@ namespace anm2ed::anm2 break; } - delayCurrent += frame.delay; + durationCurrent += frame.duration; } } - if (type != TRIGGER && frame.isInterpolated && frameNext && frame.delay > 1) + if (type != TRIGGER && frame.isInterpolated && frameNext && frame.duration > 1) { - auto interpolation = (time - delayCurrent) / (delayNext - delayCurrent); + auto interpolation = (time - durationCurrent) / (durationNext - durationCurrent); frame.rotation = glm::mix(frame.rotation, frameNext->rotation, interpolation); frame.position = glm::mix(frame.position, frameNext->position, interpolation); @@ -133,7 +141,7 @@ namespace anm2ed::anm2 { case ADJUST: if (change.rotation) frame.rotation = *change.rotation; - if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, *change.delay); + if (change.duration) frame.duration = std::max(FRAME_DURATION_MIN, *change.duration); if (change.crop) frame.crop = *change.crop; if (change.pivot) frame.pivot = *change.pivot; if (change.position) frame.position = *change.position; @@ -145,7 +153,7 @@ namespace anm2ed::anm2 case ADD: if (change.rotation) frame.rotation += *change.rotation; - if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, frame.delay + *change.delay); + if (change.duration) frame.duration = std::max(FRAME_DURATION_MIN, frame.duration + *change.duration); if (change.crop) frame.crop += *change.crop; if (change.pivot) frame.pivot += *change.pivot; if (change.position) frame.position += *change.position; @@ -157,7 +165,7 @@ namespace anm2ed::anm2 case SUBTRACT: if (change.rotation) frame.rotation -= *change.rotation; - if (change.delay) frame.delay = std::max(FRAME_DELAY_MIN, frame.delay - *change.delay); + if (change.duration) frame.duration = std::max(FRAME_DURATION_MIN, frame.duration - *change.duration); if (change.crop) frame.crop -= *change.crop; if (change.pivot) frame.pivot -= *change.pivot; if (change.position) frame.position -= *change.position; @@ -177,22 +185,50 @@ namespace anm2ed::anm2 if (document.Parse(string.c_str()) == XML_SUCCESS) { - if (!document.FirstChildElement("Frame")) + int count{}; + if (document.FirstChildElement("Frame") && type != anm2::TRIGGER) { - if (errorString) *errorString = "No valid frame(s)."; + start = std::clamp(start, 0, (int)frames.size()); + for (auto element = document.FirstChildElement("Frame"); element; + element = element->NextSiblingElement("Frame")) + { + auto index = start + count; + frames.insert(frames.begin() + start + count, Frame(element, type)); + indices.insert(index); + count++; + } + + return true; + } + else if (document.FirstChildElement("Trigger") && type == anm2::TRIGGER) + { + auto has_conflict = [&](int value) + { + for (auto& trigger : frames) + if (trigger.atFrame == value) return true; + return false; + }; + + for (auto element = document.FirstChildElement("Trigger"); element; + element = element->NextSiblingElement("Trigger")) + { + Frame trigger(element, type); + trigger.atFrame = start + count; + while (has_conflict(trigger.atFrame)) + trigger.atFrame++; + frames.push_back(trigger); + indices.insert(trigger.atFrame); + count++; + } + + frames_sort_by_at_frame(); + return true; + } + else + { + if (errorString) *errorString = type == anm2::TRIGGER ? "No valid trigger(s)." : "No valid frame(s)."; return false; } - - int count{}; - for (auto element = document.FirstChildElement("Frame"); element; element = element->NextSiblingElement("Frame")) - { - auto index = start + count; - frames.insert(frames.begin() + start + count, Frame(element, type)); - indices.insert(index); - count++; - } - - return true; } else if (errorString) *errorString = document.ErrorStr(); @@ -205,18 +241,18 @@ namespace anm2ed::anm2 if (!vector::in_bounds(frames, index)) return; Frame& frame = frames[index]; - if (frame.delay == FRAME_DELAY_MIN) return; + if (frame.duration == FRAME_DURATION_MIN) return; Frame frameNext = vector::in_bounds(frames, index + 1) ? frames[index + 1] : frame; - int delay{}; + int duration{}; int i = index; - while (delay < frame.delay) + while (duration < frame.duration) { Frame baked = frame; - float interpolation = (float)delay / frame.delay; - baked.delay = std::min(interval, frame.delay - delay); + float interpolation = (float)duration / frame.duration; + baked.duration = std::min(interval, frame.duration - duration); baked.isInterpolated = (i == index) ? frame.isInterpolated : false; baked.rotation = glm::mix(frame.rotation, frameNext.rotation, interpolation); baked.position = glm::mix(frame.position, frameNext.position, interpolation); @@ -232,16 +268,17 @@ namespace anm2ed::anm2 frames.insert(frames.begin() + i, baked); i++; - delay += baked.delay; + duration += baked.duration; } } - void Item::frames_generate_from_grid(ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, int count, int delay) + void Item::frames_generate_from_grid(ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, int count, + int duration) { for (int i = 0; i < count; i++) { Frame frame{}; - frame.delay = delay; + frame.duration = duration; frame.pivot = pivot; frame.size = size; frame.crop = startPosition + ivec2(size.x * (i % columns), size.y * (i / columns)); @@ -249,4 +286,11 @@ namespace anm2ed::anm2 frames.emplace_back(frame); } } -} \ No newline at end of file + + int Item::frame_index_from_at_frame_get(int atFrame) + { + for (auto [i, frame] : std::views::enumerate(frames)) + if (frame.atFrame == atFrame) return i; + return -1; + } +} diff --git a/src/anm2/item.h b/src/anm2/item.h index 9bf0228..4f4e7e8 100644 --- a/src/anm2/item.h +++ b/src/anm2/item.h @@ -24,5 +24,7 @@ namespace anm2ed::anm2 bool frames_deserialize(const std::string&, Type, int, std::set&, std::string*); void frames_bake(int, int, bool, bool); void frames_generate_from_grid(glm::ivec2, glm::ivec2, glm::ivec2, int, int, int); + void frames_sort_by_at_frame(); + int frame_index_from_at_frame_get(int); }; } \ No newline at end of file diff --git a/src/anm2/sound.cpp b/src/anm2/sound.cpp index d6f9b8a..9a14d13 100644 --- a/src/anm2/sound.cpp +++ b/src/anm2/sound.cpp @@ -9,10 +9,7 @@ using namespace tinyxml2; namespace anm2ed::anm2 { - Sound::Sound(const Sound& other) : path(other.path) - { - audio = path.empty() ? Audio() : Audio(path.c_str()); - } + Sound::Sound(const Sound& other) : path(other.path) { audio = path.empty() ? Audio() : Audio(path.c_str()); } Sound& Sound::operator=(const Sound& other) { @@ -49,6 +46,11 @@ namespace anm2ed::anm2 return element; } + void Sound::serialize(XMLDocument& document, XMLElement* parent, int id) + { + parent->InsertEndChild(to_element(document, id)); + } + std::string Sound::to_string(int id) { XMLDocument document{}; @@ -56,18 +58,9 @@ namespace anm2ed::anm2 return xml::document_to_string(document); } - void Sound::reload(const std::string& directory) - { - *this = Sound(directory, this->path); - } + void Sound::reload(const std::string& directory) { *this = Sound(directory, this->path); } - bool Sound::is_valid() - { - return audio.is_valid(); - } + bool Sound::is_valid() { return audio.is_valid(); } - void Sound::play() - { - audio.play(); - } + void Sound::play() { audio.play(); } } \ No newline at end of file diff --git a/src/anm2/sound.h b/src/anm2/sound.h index 951af67..a0af85a 100644 --- a/src/anm2/sound.h +++ b/src/anm2/sound.h @@ -25,6 +25,7 @@ namespace anm2ed::anm2 Sound(const std::string&, const std::string&); tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int); std::string to_string(int); + void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int); void reload(const std::string&); bool is_valid(); void play(); diff --git a/src/anm2/spritesheet.cpp b/src/anm2/spritesheet.cpp index 7f0ce56..2eec783 100644 --- a/src/anm2/spritesheet.cpp +++ b/src/anm2/spritesheet.cpp @@ -56,14 +56,8 @@ namespace anm2ed::anm2 return texture.write_png(this->path); } - void Spritesheet::reload(const std::string& directory) - { - *this = Spritesheet(directory, this->path); - } + void Spritesheet::reload(const std::string& directory) { *this = Spritesheet(directory, this->path); } - bool Spritesheet::is_valid() - { - return texture.is_valid(); - } + bool Spritesheet::is_valid() { return texture.is_valid(); } } \ No newline at end of file diff --git a/src/document.cpp b/src/document.cpp index c602acb..fcc8a01 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -36,9 +36,9 @@ namespace anm2ed : path(std::move(other.path)), snapshots(std::move(other.snapshots)), current(snapshots.current), anm2(current.anm2), reference(current.reference), playback(current.playback), animation(current.animation), merge(current.merge), event(current.event), layer(current.layer), null(current.null), sound(current.sound), - spritesheet(current.spritesheet), message(current.message), previewZoom(other.previewZoom), - previewPan(other.previewPan), editorPan(other.editorPan), editorZoom(other.editorZoom), - overlayIndex(other.overlayIndex), hoveredFrame(other.hoveredFrame), hash(other.hash), saveHash(other.saveHash), + spritesheet(current.spritesheet), frames(current.frames), message(current.message), + previewZoom(other.previewZoom), previewPan(other.previewPan), editorPan(other.editorPan), + editorZoom(other.editorZoom), overlayIndex(other.overlayIndex), saveHash(other.saveHash), autosaveHash(other.autosaveHash), lastAutosaveTime(other.lastAutosaveTime), isOpen(other.isOpen), isForceDirty(other.isForceDirty), isAnimationPreviewSet(other.isAnimationPreviewSet), isSpritesheetEditorSet(other.isSpritesheetEditorSet) @@ -56,7 +56,6 @@ namespace anm2ed editorPan = other.editorPan; editorZoom = other.editorZoom; overlayIndex = other.overlayIndex; - hoveredFrame = other.hoveredFrame; hash = other.hash; saveHash = other.saveHash; autosaveHash = other.autosaveHash; @@ -118,10 +117,7 @@ namespace anm2ed return false; } - void Document::hash_set() - { - hash = anm2.hash(); - } + void Document::hash_set() { hash = anm2.hash(); } void Document::clean() { @@ -194,50 +190,23 @@ namespace anm2ed } } - bool Document::is_dirty() - { - return hash != saveHash; - } - - bool Document::is_autosave_dirty() - { - return hash != autosaveHash; - } - - std::filesystem::path Document::directory_get() - { - return path.parent_path(); - } - - std::filesystem::path Document::filename_get() - { - return path.filename(); - } - - bool Document::is_valid() - { - return !path.empty(); - } + bool Document::is_dirty() { return hash != saveHash; } + bool Document::is_autosave_dirty() { return hash != autosaveHash; } + std::filesystem::path Document::directory_get() { return path.parent_path(); } + std::filesystem::path Document::filename_get() { return path.filename(); } + bool Document::is_valid() { return !path.empty(); } anm2::Frame* Document::frame_get() { - return anm2.frame_get(reference); + return anm2.frame_get(reference.animationIndex, reference.itemType, reference.frameIndex, reference.itemID); } anm2::Item* Document::item_get() { - return anm2.item_get(reference); - } - - anm2::Animation* Document::animation_get() - { - return anm2.animation_get(reference); - } - - anm2::Spritesheet* Document::spritesheet_get() - { - return anm2.spritesheet_get(spritesheet.reference); + return anm2.item_get(reference.animationIndex, reference.itemType, reference.itemID); } + anm2::Animation* Document::animation_get() { return anm2.animation_get(reference.animationIndex); } + anm2::Spritesheet* Document::spritesheet_get() { return anm2.spritesheet_get(spritesheet.reference); } void Document::spritesheet_add(const std::string& path) { @@ -277,14 +246,6 @@ namespace anm2ed change(Document::ALL); } - bool Document::is_able_to_undo() - { - return !snapshots.undoStack.is_empty(); - } - - bool Document::is_able_to_redo() - { - return !snapshots.redoStack.is_empty(); - } - + bool Document::is_able_to_undo() { return !snapshots.undoStack.is_empty(); } + bool Document::is_able_to_redo() { return !snapshots.redoStack.is_empty(); } } diff --git a/src/document.h b/src/document.h index 545d153..6043f2f 100644 --- a/src/document.h +++ b/src/document.h @@ -14,6 +14,7 @@ namespace anm2ed public: enum ChangeType { + INFO, LAYERS, NULLS, SPRITESHEETS, @@ -33,6 +34,7 @@ namespace anm2ed anm2::Anm2& anm2 = current.anm2; anm2::Reference& reference = current.reference; + float& frameTime = current.frameTime; Playback& playback = current.playback; Storage& animation = current.animation; Storage& merge = current.merge; @@ -41,6 +43,7 @@ namespace anm2ed Storage& null = current.null; Storage& sound = current.sound; Storage& spritesheet = current.spritesheet; + Storage& frames = current.frames; std::string& message = current.message; float previewZoom{200}; @@ -49,8 +52,6 @@ namespace anm2ed float editorZoom{200}; int overlayIndex{-1}; - anm2::Reference hoveredFrame{}; - uint64_t hash{}; uint64_t saveHash{}; uint64_t autosaveHash{}; diff --git a/src/imgui/dockspace.cpp b/src/imgui/dockspace.cpp index 4a5a99d..2ca7218 100644 --- a/src/imgui/dockspace.cpp +++ b/src/imgui/dockspace.cpp @@ -34,7 +34,7 @@ namespace anm2ed::imgui if (settings.windowIsFrameProperties) frameProperties.update(manager, settings); if (settings.windowIsLayers) layers.update(manager, settings, resources, clipboard); if (settings.windowIsNulls) nulls.update(manager, settings, resources, clipboard); - if (settings.windowIsOnionskin) onionskin.update(settings); + if (settings.windowIsOnionskin) onionskin.update(manager, settings); if (settings.windowIsSounds) sounds.update(manager, settings, resources, dialog, clipboard); if (settings.windowIsSpritesheetEditor) spritesheetEditor.update(manager, settings, resources); if (settings.windowIsSpritesheets) spritesheets.update(manager, settings, resources, dialog, clipboard); diff --git a/src/imgui/documents.cpp b/src/imgui/documents.cpp index 49b611e..c4d419f 100644 --- a/src/imgui/documents.cpp +++ b/src/imgui/documents.cpp @@ -38,9 +38,9 @@ namespace anm2ed::imgui if (ImGui::BeginTabBar("Documents Bar", ImGuiTabBarFlags_Reorderable)) { auto documentsCount = (int)manager.documents.size(); - bool closeShortcut = imgui::shortcut(settings.shortcutClose, shortcut::GLOBAL) && !closePopup.is_open(); + bool isCloseShortcut = shortcut(manager.chords[SHORTCUT_CLOSE], shortcut::GLOBAL) && !closePopup.is_open(); int closeShortcutIndex = - closeShortcut && manager.selected >= 0 && manager.selected < documentsCount ? manager.selected : -1; + isCloseShortcut && manager.selected >= 0 && manager.selected < documentsCount ? manager.selected : -1; std::vector closeIndices{}; closeIndices.reserve(documentsCount); @@ -85,7 +85,7 @@ namespace anm2ed::imgui ImGui::PushFont(resources.fonts[font].get(), font::SIZE); if (ImGui::BeginTabItem(label.c_str(), &document.isOpen, flags)) { - manager.set(i); + if (manager.selected != i) manager.set(i); if (isRequested) manager.pendingSelected = -1; diff --git a/src/imgui/imgui_.cpp b/src/imgui/imgui_.cpp index c761f03..b1d7dea 100644 --- a/src/imgui/imgui_.cpp +++ b/src/imgui/imgui_.cpp @@ -248,18 +248,18 @@ namespace anm2ed::imgui return false; } - bool shortcut(std::string string, shortcut::Type type) + bool shortcut(ImGuiKeyChord chord, shortcut::Type type) { if (ImGui::GetTopMostPopupModal() != nullptr) return false; int 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); + ImGui::SetNextItemShortcut(chord, flags); return false; } - return ImGui::Shortcut(string_to_chord(string), flags); + return ImGui::Shortcut(chord, flags); } MultiSelectStorage::MultiSelectStorage() { internal.AdapterSetItemSelected = external_storage_set; } diff --git a/src/imgui/imgui_.h b/src/imgui/imgui_.h index cef732f..f9bc156 100644 --- a/src/imgui/imgui_.h +++ b/src/imgui/imgui_.h @@ -174,7 +174,7 @@ namespace anm2ed::imgui ImVec2 icon_size_get(); bool chord_held(ImGuiKeyChord); bool chord_repeating(ImGuiKeyChord, float = ImGui::GetIO().KeyRepeatDelay, float = ImGui::GetIO().KeyRepeatRate); - bool shortcut(std::string, types::shortcut::Type = types::shortcut::FOCUSED_SET); + bool shortcut(ImGuiKeyChord, types::shortcut::Type = types::shortcut::FOCUSED_SET); class MultiSelectStorage : public std::set { diff --git a/src/imgui/taskbar.cpp b/src/imgui/taskbar.cpp index 7d624d4..ff58ef7 100644 --- a/src/imgui/taskbar.cpp +++ b/src/imgui/taskbar.cpp @@ -1,19 +1,19 @@ #include "taskbar.h" +#include #include -#include -#include +#include +#include #include -#include -#include #include +#include #include +#include #include "math_.h" #include "render.h" #include "shader.h" -#include "toast.h" #include "types.h" #include "icon.h" @@ -26,115 +26,11 @@ using namespace glm; namespace anm2ed::imgui { -#ifdef __unix__ - - namespace - { - constexpr std::array ICON_SIZES{16, 24, 32, 48, 64, 128, 256}; - - bool ensure_parent_directory_exists(const std::filesystem::path& path) - { - std::error_code ec; - std::filesystem::create_directories(path.parent_path(), ec); - if (ec) - { - toasts.warning(std::format("Could not create directory for {} ({})", path.string(), ec.message())); - return false; - } - return true; - } - - bool write_binary_blob(const std::filesystem::path& path, const std::uint8_t* data, size_t size) - { - if (!ensure_parent_directory_exists(path)) return false; - - std::ofstream file(path, std::ios::binary | std::ios::trunc); - if (!file.is_open()) - { - toasts.warning(std::format("Could not open {} for writing", path.string())); - return false; - } - - file.write(reinterpret_cast(data), static_cast(size)); - return true; - } - - bool run_command_checked(const std::string& command, const std::string& description) - { - auto result = std::system(command.c_str()); - if (result != 0) - { - toasts.warning(std::format("{} failed (exit code {})", description, result)); - return false; - } - return true; - } - - bool install_icon_set(const std::string& context, const std::string& iconName, const std::filesystem::path& path) - { - bool success = true; - for (auto size : ICON_SIZES) - { - auto command = std::format("xdg-icon-resource install --noupdate --novendor --context {} --size {} \"{}\" {}", - context, size, path.string(), iconName); - success &= run_command_checked(command, std::format("Install {} icon ({}px)", iconName, size)); - } - return success; - } - - bool uninstall_icon_set(const std::string& context, const std::string& iconName) - { - bool success = true; - for (auto size : ICON_SIZES) - { - auto command = - std::format("xdg-icon-resource uninstall --noupdate --context {} --size {} {}", context, size, iconName); - success &= run_command_checked(command, std::format("Remove {} icon ({}px)", iconName, size)); - } - return success; - } - - bool remove_file_if_exists(const std::filesystem::path& path) - { - std::error_code ec; - if (!std::filesystem::exists(path, ec)) return true; - std::filesystem::remove(path, ec); - if (ec) - { - toasts.warning(std::format("Could not remove {} ({})", path.string(), ec.message())); - return false; - } - return true; - } - } - - constexpr auto MIME_TYPE = R"( - - - Anm2 Animation - - -)"; - - constexpr auto DESKTOP_ENTRY_FORMAT = R"([Desktop Entry] -Type=Application -Name=Anm2Ed -Icon=anm2ed -Comment=Animation editor for .anm2 files -Exec={} -Terminal=false -Categories=Graphics;Development; -MimeType=application/x-anm2+xml; -)"; - -#endif - Taskbar::Taskbar() : generate(vec2()) {} void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting) { auto document = manager.get(); - auto reference = document ? &document->reference : nullptr; auto animation = document ? document->animation_get() : nullptr; auto item = document ? document->item_get() : nullptr; @@ -194,10 +90,10 @@ MimeType=application/x-anm2+xml; if (ImGui::BeginMenu("Wizard")) { - ImGui::BeginDisabled(!item || document->reference.itemType != anm2::LAYER); - if (ImGui::MenuItem("Generate Animation From Grid")) generatePopup.open(); - if (ImGui::MenuItem("Change All Frame Properties")) changePopup.open(); - ImGui::EndDisabled(); + if (ImGui::MenuItem("Generate Animation From Grid", nullptr, false, + item && document->reference.itemType == anm2::LAYER)) + generatePopup.open(); + ImGui::Separator(); if (ImGui::MenuItem("Render Animation", nullptr, false, animation)) renderPopup.open(); ImGui::EndMenu(); @@ -208,8 +104,9 @@ MimeType=application/x-anm2+xml; ImGui::MenuItem("Always Loop", nullptr, &settings.playbackIsLoop); ImGui::SetItemTooltip("%s", "Animations will always loop during playback, even if looping isn't set."); - ImGui::MenuItem("Clamp Playhead", nullptr, &settings.playbackIsClampPlayhead); - ImGui::SetItemTooltip("%s", "The playhead will always clamp to the animation's length."); + ImGui::MenuItem("Clamp", nullptr, &settings.playbackIsClamp); + ImGui::SetItemTooltip("%s", "Operations will always be clamped to within the animation's bounds.\nFor example, " + "dragging the playhead, or triggers."); ImGui::EndMenu(); } @@ -230,146 +127,12 @@ MimeType=application/x-anm2+xml; configurePopup.open(); } - ImGui::Separator(); - - if (ImGui::MenuItem("Associate .anm2 Files with Editor", nullptr, false, - !isAnm2Association || !isAbleToAssociateAnm2)) - { -#ifdef _WIN32 - -#elif __unix__ - auto cache_icons = []() - { - auto programIconPath = std::filesystem::path(filesystem::path_icon_get()); - auto fileIconPath = std::filesystem::path(filesystem::path_icon_file_get()); - auto iconBytes = std::size(resource::icon::PROGRAM); - - bool isSuccess = write_binary_blob(programIconPath, resource::icon::PROGRAM, iconBytes) && - write_binary_blob(fileIconPath, resource::icon::PROGRAM, iconBytes); - - if (isSuccess) - { - isSuccess = install_icon_set("apps", "anm2ed", programIconPath) && - install_icon_set("mimetypes", "application-x-anm2+xml", fileIconPath) && - run_command_checked("xdg-icon-resource forceupdate --theme hicolor", "Refresh icon cache"); - } - - remove_file_if_exists(programIconPath); - remove_file_if_exists(fileIconPath); - - if (isSuccess) toasts.info("Cached program and file icons."); - return isSuccess; - }; - - auto register_mime = []() - { - auto path = std::filesystem::path(filesystem::path_mime_get()); - if (!ensure_parent_directory_exists(path)) return false; - - std::ofstream file(path, std::ofstream::out | std::ofstream::trunc); - if (!file.is_open()) - { - toasts.warning(std::format("Could not write .anm2 MIME type: {}", path.string())); - return false; - } - - file << MIME_TYPE; - file.close(); - toasts.info(std::format("Wrote .anm2 MIME type to: {}", path.string())); - - auto mimeRoot = path.parent_path().parent_path(); - auto command = std::format("update-mime-database \"{}\"", mimeRoot.string()); - return run_command_checked(command, "Update MIME database"); - }; - - auto register_desktop_entry = []() - { - auto path = std::filesystem::path(filesystem::path_application_get()); - if (!ensure_parent_directory_exists(path)) return false; - - std::ofstream file(path, std::ofstream::out | std::ofstream::trunc); - if (!file.is_open()) - { - toasts.warning(std::format("Could not write desktop entry: {}", path.string())); - return false; - } - - auto desktopEntry = std::format(DESKTOP_ENTRY_FORMAT, filesystem::path_executable_get()); - file << desktopEntry; - file.close(); - toasts.info(std::format("Wrote desktop entry to: {}", path.string())); - - auto desktopDir = path.parent_path(); - auto desktopUpdate = - std::format("update-desktop-database \"{}\"", desktopDir.empty() ? "." : desktopDir.string()); - auto desktopFileName = path.filename().string(); - auto setDefault = std::format("xdg-mime default {} application/x-anm2+xml", - desktopFileName.empty() ? path.string() : desktopFileName); - - auto databaseUpdated = run_command_checked(desktopUpdate, "Update desktop database"); - auto defaultRegistered = run_command_checked(setDefault, "Set default handler for .anm2"); - return databaseUpdated && defaultRegistered; - }; - - auto iconsCached = cache_icons(); - auto mimeRegistered = register_mime(); - auto desktopRegistered = register_desktop_entry(); - - isAnm2Association = iconsCached && mimeRegistered && desktopRegistered; - if (isAnm2Association) - toasts.info("Associated .anm2 files with the editor."); - else - toasts.warning("Association incomplete. Please review the warnings above."); -#endif - } - ImGui::SetItemTooltip( - "Associate .anm2 files with the application (i.e., clicking on them in a file explorer will " - "open the application)."); - - if (ImGui::MenuItem("Remove .anm2 File Association", nullptr, false, - isAnm2Association || !isAbleToAssociateAnm2)) - { -#ifdef _WIN32 - -#elif __unix__ - { - auto iconsRemoved = - uninstall_icon_set("apps", "anm2ed") && uninstall_icon_set("mimetypes", "application-x-anm2+xml") && - run_command_checked("xdg-icon-resource forceupdate --theme hicolor", "Refresh icon cache"); - if (iconsRemoved) - toasts.info("Removed cached icons."); - else - toasts.warning("Could not remove all cached icons."); - } - - { - auto path = std::filesystem::path(filesystem::path_mime_get()); - auto removed = remove_file_if_exists(path); - if (removed) toasts.info(std::format("Removed .anm2 MIME type: {}", path.string())); - - auto mimeRoot = path.parent_path().parent_path(); - run_command_checked(std::format("update-mime-database \"{}\"", mimeRoot.string()), "Update MIME database"); - } - - { - auto path = std::filesystem::path(filesystem::path_application_get()); - if (remove_file_if_exists(path)) toasts.info(std::format("Removed desktop entry: {}", path.string())); - - auto desktopDir = path.parent_path(); - run_command_checked( - std::format("update-desktop-database \"{}\"", desktopDir.empty() ? "." : desktopDir.string()), - "Update desktop database"); - } -#endif - isAnm2Association = false; - } - ImGui::SetItemTooltip("Unassociate .anm2 files with the application."); - ImGui::EndMenu(); } if (ImGui::BeginMenu("Help")) { + if (ImGui::MenuItem("About")) aboutPopup.open(); ImGui::EndMenu(); } @@ -476,134 +239,6 @@ MimeType=application/x-anm2+xml; ImGui::EndPopup(); } - changePopup.trigger(); - - if (ImGui::BeginPopupModal(changePopup.label, &changePopup.isOpen, ImGuiWindowFlags_NoResize)) - { - auto& isCrop = settings.changeIsCrop; - auto& isSize = settings.changeIsSize; - auto& isPosition = settings.changeIsPosition; - auto& isPivot = settings.changeIsPivot; - auto& isScale = settings.changeIsScale; - auto& isRotation = settings.changeIsRotation; - auto& isDelay = settings.changeIsDelay; - auto& isTint = settings.changeIsTint; - auto& isColorOffset = settings.changeIsColorOffset; - auto& isVisibleSet = settings.changeIsVisibleSet; - auto& isInterpolatedSet = settings.changeIsInterpolatedSet; - auto& crop = settings.changeCrop; - auto& size = settings.changeSize; - auto& position = settings.changePosition; - auto& pivot = settings.changePivot; - auto& scale = settings.changeScale; - auto& rotation = settings.changeRotation; - auto& delay = settings.changeDelay; - auto& tint = settings.changeTint; - auto& colorOffset = settings.changeColorOffset; - auto& isVisible = settings.changeIsVisible; - auto& isInterpolated = settings.changeIsInterpolated; - - auto& isFromSelectedFrame = settings.changeIsFromSelectedFrame; - auto& numberFrames = settings.changeNumberFrames; - - auto propertiesSize = child_size_get(10); - - if (ImGui::BeginChild("##Properties", propertiesSize, ImGuiChildFlags_Borders)) - { -#define PROPERTIES_WIDGET(body) \ - ImGui::Checkbox(checkboxLabel, &isEnabled); \ - ImGui::SameLine(); \ - ImGui::BeginDisabled(!isEnabled); \ - body; \ - ImGui::EndDisabled(); - - auto bool_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, bool& value) - { PROPERTIES_WIDGET(ImGui::Checkbox(valueLabel, &value)); }; - - auto color3_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec3& value) - { PROPERTIES_WIDGET(ImGui::ColorEdit3(valueLabel, value_ptr(value))); }; - - auto color4_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec4& value) - { PROPERTIES_WIDGET(ImGui::ColorEdit4(valueLabel, value_ptr(value))); }; - - auto float2_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec2& value) - { PROPERTIES_WIDGET(ImGui::InputFloat2(valueLabel, value_ptr(value), math::vec2_format_get(value))); }; - - auto float_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, float& value) - { PROPERTIES_WIDGET(ImGui::InputFloat(valueLabel, &value, STEP, STEP_FAST, math::float_format_get(value))); }; - - auto int_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, int& value) - { PROPERTIES_WIDGET(ImGui::InputInt(valueLabel, &value, STEP, STEP_FAST)); }; - -#undef PROPERTIES_WIDGET - - float2_value("##Is Crop", "Crop", isCrop, crop); - float2_value("##Is Size", "Size", isSize, size); - float2_value("##Is Position", "Position", isPosition, position); - float2_value("##Is Pivot", "Pivot", isPivot, pivot); - float2_value("##Is Scale", "Scale", isScale, scale); - float_value("##Is Rotation", "Rotation", isRotation, rotation); - int_value("##Is Delay", "Delay", isDelay, delay); - color4_value("##Is Tint", "Tint", isTint, tint); - color3_value("##Is Color Offset", "Color Offset", isColorOffset, colorOffset); - bool_value("##Is Visible", "Visible", isVisibleSet, isVisible); - ImGui::SameLine(); - bool_value("##Is Interpolated", "Interpolated", isInterpolatedSet, isInterpolated); - } - ImGui::EndChild(); - - auto settingsSize = child_size_get(2); - - if (ImGui::BeginChild("##Settings", settingsSize, ImGuiChildFlags_Borders)) - { - ImGui::Checkbox("From Selected Frame", &isFromSelectedFrame); - ImGui::SetItemTooltip("The frames after the currently referenced frame will be changed with these values.\nIf" - " off, will use all frames."); - - ImGui::BeginDisabled(!isFromSelectedFrame); - input_int_range("Number of Frames", numberFrames, anm2::FRAME_NUM_MIN, - item->frames.size() - reference->frameIndex); - ImGui::SetItemTooltip("Set the number of frames that will be changed."); - ImGui::EndDisabled(); - } - ImGui::EndChild(); - - auto widgetSize = widget_size_with_row_get(4); - - auto frame_change = [&](anm2::ChangeType type) - { - anm2::FrameChange frameChange; - if (isCrop) frameChange.crop = std::make_optional(crop); - if (isSize) frameChange.size = std::make_optional(size); - if (isPosition) frameChange.position = std::make_optional(position); - if (isPivot) frameChange.pivot = std::make_optional(pivot); - if (isScale) frameChange.scale = std::make_optional(scale); - if (isRotation) frameChange.rotation = std::make_optional(rotation); - if (isDelay) frameChange.delay = std::make_optional(delay); - if (isTint) frameChange.tint = std::make_optional(tint); - if (isColorOffset) frameChange.colorOffset = std::make_optional(colorOffset); - if (isVisibleSet) frameChange.isVisible = std::make_optional(isVisible); - if (isInterpolatedSet) frameChange.isInterpolated = std::make_optional(isInterpolated); - - DOCUMENT_EDIT_PTR(document, "Change Frame Properties", Document::FRAMES, - item->frames_change(frameChange, type, - isFromSelectedFrame && document->frame_get() ? reference->frameIndex : 0, - isFromSelectedFrame ? numberFrames : -1)); - - changePopup.close(); - }; - - if (ImGui::Button("Add", widgetSize)) frame_change(anm2::ADD); - ImGui::SameLine(); - if (ImGui::Button("Subtract", widgetSize)) frame_change(anm2::SUBTRACT); - ImGui::SameLine(); - if (ImGui::Button("Adjust", widgetSize)) frame_change(anm2::ADJUST); - ImGui::SameLine(); - if (ImGui::Button("Cancel", widgetSize)) changePopup.close(); - - ImGui::EndPopup(); - } - configurePopup.trigger(); if (ImGui::BeginPopupModal(configurePopup.label, &configurePopup.isOpen, ImGuiWindowFlags_NoResize)) @@ -612,20 +247,44 @@ MimeType=application/x-anm2+xml; if (ImGui::BeginTabBar("##Configure Tabs")) { - if (ImGui::BeginTabItem("General")) + if (ImGui::BeginTabItem("Display")) { if (ImGui::BeginChild("##Tab Child", childSize, true)) { - ImGui::SeparatorText("File"); + input_float_range("UI Scale", editSettings.uiScale, 0.5f, 2.0f, 0.25f, 0.25f, "%.2f"); + ImGui::SetItemTooltip("Change the scale of the UI."); - ImGui::Checkbox("Autosaving", &editSettings.fileIsAutosave); + ImGui::Checkbox("Vsync", &editSettings.isVsync); + ImGui::SetItemTooltip("Toggle vertical sync; synchronizes program update rate with monitor refresh rate."); + } + ImGui::EndChild(); + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("File")) + { + if (ImGui::BeginChild("##Tab Child", childSize, true)) + { + ImGui::SeparatorText("Autosave"); + + ImGui::Checkbox("Enabled", &editSettings.fileIsAutosave); ImGui::SetItemTooltip("Enables autosaving of documents."); ImGui::BeginDisabled(!editSettings.fileIsAutosave); - input_int_range("Autosave Time (minutes)", editSettings.fileAutosaveTime, 0, 10); + input_int_range("Time (minutes)", editSettings.fileAutosaveTime, 0, 10); ImGui::SetItemTooltip("If changed, will autosave documents using this interval."); ImGui::EndDisabled(); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Input")) + { + if (ImGui::BeginChild("##Tab Child", childSize, true)) + { ImGui::SeparatorText("Keyboard"); input_float_range("Repeat Delay (seconds)", editSettings.keyboardRepeatDelay, 0.05f, 1.0f, 0.05f, 0.05f, @@ -636,17 +295,9 @@ MimeType=application/x-anm2+xml; "%.3f"); ImGui::SetItemTooltip("Set how often, after repeating begins, key inputs will be fired."); - ImGui::SeparatorText("UI"); + ImGui::SeparatorText("Zoom"); - input_float_range("UI Scale", editSettings.uiScale, 0.5f, 2.0f, 0.25f, 0.25f, "%.2f"); - ImGui::SetItemTooltip("Change the scale of the UI."); - - ImGui::Checkbox("Vsync", &editSettings.isVsync); - ImGui::SetItemTooltip("Toggle vertical sync; synchronizes program update rate with monitor refresh rate."); - - ImGui::SeparatorText("View"); - - input_float_range("Zoom Step", editSettings.viewZoomStep, 10.0f, 250.0f, 10.0f, 10.0f, "%.0f"); + input_float_range("Step", editSettings.viewZoomStep, 10.0f, 250.0f, 10.0f, 10.0f, "%.0f"); ImGui::SetItemTooltip("When zooming in/out with mouse or shortcut, this value will be used."); } ImGui::EndChild(); @@ -724,6 +375,7 @@ MimeType=application/x-anm2+xml; if (ImGui::Button("Save", widgetSize)) { settings = editSettings; + manager.chords_set(settings); configurePopup.close(); } ImGui::SetItemTooltip("Use the configured settings."); @@ -745,9 +397,6 @@ MimeType=application/x-anm2+xml; if (ImGui::BeginPopupModal(renderPopup.label, &renderPopup.isOpen, ImGuiWindowFlags_NoResize)) { - auto animation = document ? document->animation_get() : nullptr; - if (!animation) renderPopup.close(); - auto& playback = document->playback; auto& ffmpegPath = settings.renderFFmpegPath; auto& path = settings.renderPath; @@ -802,39 +451,47 @@ MimeType=application/x-anm2+xml; if (ImGui::Combo("Type", &type, render::STRINGS, render::COUNT)) replace_extension(); ImGui::SetItemTooltip("Set the type of the output."); - ImGui::BeginDisabled(type != render::PNGS); - input_text_string("Format", &format); - ImGui::SetItemTooltip( - "For outputted images, each image will use this format.\n{} represents the index of each image."); - ImGui::EndDisabled(); + if (type == render::PNGS) + { + ImGui::Separator(); + input_text_string("Format", &format); + ImGui::SetItemTooltip( + "For outputted images, each image will use this format.\n{} represents the index of each image."); + } - ImGui::BeginDisabled(!isRange); - input_int_range("Start", start, 0, animation->frameNum - 1); - ImGui::SetItemTooltip("Set the starting time of the animation."); - input_int_range("End", end, start + 1, animation->frameNum); - ImGui::SetItemTooltip("Set the ending time of the animation."); - ImGui::EndDisabled(); - - ImGui::BeginDisabled(!isRaw); - input_float_range("Scale", scale, 1.0f, 100.0f, STEP, STEP_FAST, "%.1fx"); - ImGui::SetItemTooltip("Set the output scale of the animation."); - ImGui::EndDisabled(); + ImGui::Separator(); if (ImGui::Checkbox("Custom Range", &isRange)) if (!isRange) range_to_length(); ImGui::SetItemTooltip("Toggle using a custom range for the animation."); - ImGui::SameLine(); + if (isRange) + { + input_int_range("Start", start, 0, animation->frameNum - 1); + ImGui::SetItemTooltip("Set the starting time of the animation."); + input_int_range("End", end, start + 1, animation->frameNum); + ImGui::SetItemTooltip("Set the ending time of the animation."); + } + + ImGui::Separator(); ImGui::Checkbox("Raw", &isRaw); ImGui::SetItemTooltip("Record only the raw animation; i.e., only its layers, to its bounds."); - ImGui::SameLine(); + if (isRaw) + { + input_float_range("Scale", scale, 1.0f, 100.0f, STEP, STEP_FAST, "%.1fx"); + ImGui::SetItemTooltip("Set the output scale of the animation."); + } + + ImGui::Separator(); ImGui::Checkbox("Sound", &settings.timelineIsSound); ImGui::SetItemTooltip("Toggle sounds playing with triggers.\nBind sounds to events in the Events window.\nThe " "output animation will use the played sounds."); + ImGui::Separator(); + if (ImGui::Button("Render", widgetSize)) { manager.isRecordingStart = true; @@ -857,14 +514,205 @@ MimeType=application/x-anm2+xml; if (ImGui::BeginPopupModal(aboutPopup.label, &aboutPopup.isOpen, ImGuiWindowFlags_NoResize)) { - if (ImGui::Button("Close")) aboutPopup.close(); + struct Credit + { + const char* string{}; + font::Type font{font::REGULAR}; + }; + + struct ScrollingCredit + { + int index{}; + float offset{}; + }; + + struct CreditsState + { + std::vector active{}; + float spawnTimer{1.0f}; + int nextIndex{}; + }; + + static constexpr auto ANM2ED_LABEL = "Anm2Ed"; + static constexpr auto VERSION_LABEL = "Version 2.0"; + static constexpr auto CREDIT_DELAY = 1.0f; + static constexpr auto CREDIT_SCROLL_SPEED = 25.0f; + + static constexpr Credit CREDITS[] = { + {"Anm2Ed", font::BOLD}, + {"License: GPLv3"}, + {""}, + {"Designer", font::BOLD}, + {"Shweet"}, + {""}, + {"Additional Help", font::BOLD}, + {"im-tem"}, + {""}, + {"Based on the work of:", font::BOLD}, + {"Adrian Gavrilita"}, + {"Simon Parzer"}, + {"Matt Kapuszczak"}, + {""}, + {"XM Music", font::BOLD}, + {"Drozerix"}, + {"\"Keygen Wraith\""}, + {"https://modarchive.org/module.php?207854"}, + {"License: CC0"}, + {""}, + {"Libraries", font::BOLD}, + {"Dear ImGui"}, + {"https://github.com/ocornut/imgui"}, + {"License: MIT"}, + {""}, + {"SDL"}, + {"https://github.com/libsdl-org/SDL"}, + {"License: zlib"}, + {""}, + {"SDL_mixer"}, + {"https://github.com/libsdl-org/SDL_mixer"}, + {"License: zlib"}, + {""}, + {"tinyxml2"}, + {"https://github.com/leethomason/tinyxml2"}, + {"License: zlib"}, + {""}, + {"glm"}, + {"https://github.com/g-truc/glm"}, + {"License: MIT"}, + {""}, + {"lunasvg"}, + {"https://github.com/sammycage/lunasvg"}, + {"License: MIT"}, + {""}, + {"libxm"}, + {"https://github.com/Artefact2/libxm"}, + {"License: WTFPL"}, + {""}, + {"Icons", font::BOLD}, + {"Remix Icons"}, + {"remixicon.com"}, + {"License: Apache"}, + {""}, + {"Font", font::BOLD}, + {"Noto Sans"}, + {"https://fonts.google.com/noto/specimen/Noto+Sans"}, + {"License: OFL"}, + {""}, + {"Special Thanks", font::BOLD}, + {"Edmund McMillen"}, + {"Florian Himsl"}, + {"Tyrone Rodriguez"}, + {"The-Vinh Truong (_kilburn)"}, + {"Everyone who waited patiently for this to be finished"}, + {"Everyone else who has worked on The Binding of Isaac!"}, + {""}, + {""}, + {""}, + {""}, + {""}, + {"enjoy the jams :)"}, + {""}, + {""}, + {""}, + {""}, + {""}, + }; + static constexpr auto CREDIT_COUNT = (int)(sizeof(CREDITS) / sizeof(Credit)); + + static CreditsState creditsState{}; + + auto credits_reset = [&]() + { + resources.music.play(true); + creditsState = {}; + creditsState.spawnTimer = CREDIT_DELAY; + }; + + if (aboutPopup.isJustOpened) credits_reset(); + + auto size = ImGui::GetContentRegionAvail(); + + ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE_LARGE); + + ImGui::SetCursorPosX((size.x - ImGui::CalcTextSize(ANM2ED_LABEL).x) / 2); + ImGui::Text(ANM2ED_LABEL); + + ImGui::SetCursorPosX((size.x - ImGui::CalcTextSize(VERSION_LABEL).x) / 2); + ImGui::Text(VERSION_LABEL); + + ImGui::PopFont(); + + auto creditRegionPos = ImGui::GetCursorScreenPos(); + auto creditRegionSize = ImGui::GetContentRegionAvail(); + + if (creditRegionSize.y > 0.0f && creditRegionSize.x > 0.0f) + { + auto drawList = ImGui::GetWindowDrawList(); + auto clipMax = ImVec2(creditRegionPos.x + creditRegionSize.x, creditRegionPos.y + creditRegionSize.y); + drawList->PushClipRect(creditRegionPos, clipMax, true); + + auto delta = ImGui::GetIO().DeltaTime; + creditsState.spawnTimer -= delta; + auto maxVisible = std::max(1, (int)std::floor(creditRegionSize.y / (float)font::SIZE)); + + while (creditsState.active.size() < (size_t)maxVisible && creditsState.spawnTimer <= 0.0f) + { + creditsState.active.push_back({creditsState.nextIndex, 0.0f}); + creditsState.nextIndex = (creditsState.nextIndex + 1) % CREDIT_COUNT; + creditsState.spawnTimer += CREDIT_DELAY; + } + + auto baseY = clipMax.y - (float)font::SIZE; + auto baseColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); + auto fadeSpan = (float)font::SIZE * 2.0f; + + for (auto it = creditsState.active.begin(); it != creditsState.active.end();) + { + it->offset += CREDIT_SCROLL_SPEED * delta; + auto yPos = baseY - it->offset; + if (yPos + font::SIZE < creditRegionPos.y) + { + it = creditsState.active.erase(it); + continue; + } + + const auto& credit = CREDITS[it->index]; + auto fontPtr = resources.fonts[credit.font].get(); + auto textSize = fontPtr->CalcTextSizeA((float)font::SIZE, FLT_MAX, 0.0f, credit.string); + auto xPos = creditRegionPos.x + (creditRegionSize.x - textSize.x) * 0.5f; + + auto alpha = 1.0f; + auto topDist = yPos - creditRegionPos.y; + if (topDist < fadeSpan) alpha *= std::clamp(topDist / fadeSpan, 0.0f, 1.0f); + auto bottomDist = (creditRegionPos.y + creditRegionSize.y) - (yPos + font::SIZE); + if (bottomDist < fadeSpan) alpha *= std::clamp(bottomDist / fadeSpan, 0.0f, 1.0f); + if (alpha <= 0.0f) + { + ++it; + continue; + } + + auto color = baseColor; + color.w *= alpha; + + drawList->AddText(fontPtr, (float)font::SIZE, ImVec2(xPos, yPos), ImGui::GetColorU32(color), credit.string); + ++it; + } + + drawList->PopClipRect(); + } + ImGui::EndPopup(); } - if (shortcut(settings.shortcutNew, shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_NEW); - if (shortcut(settings.shortcutOpen, shortcut::GLOBAL)) dialog.file_open(dialog::ANM2_OPEN); - if (shortcut(settings.shortcutSave, shortcut::GLOBAL)) document->save(); - if (shortcut(settings.shortcutSaveAs, shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_SAVE); - if (shortcut(settings.shortcutExit, shortcut::GLOBAL)) isQuitting = true; + if (resources.music.is_playing() && !aboutPopup.isOpen) resources.music.stop(); + + aboutPopup.end(); + + if (shortcut(manager.chords[SHORTCUT_NEW], shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_NEW); + if (shortcut(manager.chords[SHORTCUT_OPEN], shortcut::GLOBAL)) dialog.file_open(dialog::ANM2_OPEN); + if (shortcut(manager.chords[SHORTCUT_SAVE], shortcut::GLOBAL)) manager.save(); + if (shortcut(manager.chords[SHORTCUT_SAVE_AS], shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_SAVE); + if (shortcut(manager.chords[SHORTCUT_EXIT], shortcut::GLOBAL)) isQuitting = true; } } diff --git a/src/imgui/taskbar.h b/src/imgui/taskbar.h index 778683c..fd1468c 100644 --- a/src/imgui/taskbar.h +++ b/src/imgui/taskbar.h @@ -2,7 +2,6 @@ #include "canvas.h" #include "dialog.h" -#include "filesystem_.h" #include "imgui_.h" #include "manager.h" #include "resources.h" @@ -15,21 +14,12 @@ namespace anm2ed::imgui Canvas generate; float generateTime{}; PopupHelper generatePopup{PopupHelper("Generate Animation from Grid")}; - PopupHelper changePopup{PopupHelper("Change All Frame Properties", imgui::POPUP_SMALL_NO_HEIGHT)}; PopupHelper renderPopup{PopupHelper("Render Animation", imgui::POPUP_SMALL_NO_HEIGHT)}; PopupHelper configurePopup{PopupHelper("Configure")}; PopupHelper aboutPopup{PopupHelper("About")}; Settings editSettings{}; int selectedShortcut{-1}; - -#if defined(_WIN32) || defined(__unix__) - bool isAbleToAssociateAnm2 = true; -#else - bool isAbleToAssociateAnm2 = false; -#endif - - bool isAnm2Association = std::filesystem::exists(util::filesystem::path_application_get()); - + int creditsIndex{}; bool isQuittingMode{}; public: @@ -38,4 +28,4 @@ namespace anm2ed::imgui Taskbar(); void update(Manager&, Settings&, Resources&, Dialog&, bool&); }; -}; +}; \ No newline at end of file diff --git a/src/imgui/window/animation_preview.cpp b/src/imgui/window/animation_preview.cpp index 5960528..4a0500e 100644 --- a/src/imgui/window/animation_preview.cpp +++ b/src/imgui/window/animation_preview.cpp @@ -24,14 +24,13 @@ namespace anm2ed::imgui constexpr auto NULL_RECT_SIZE = vec2(100); constexpr auto TRIGGER_TEXT_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.5f); - AnimationPreview::AnimationPreview() : Canvas(vec2()) - { - } + AnimationPreview::AnimationPreview() : Canvas(vec2()) {} void AnimationPreview::tick(Manager& manager, Document& document, Settings& settings) { auto& anm2 = document.anm2; auto& playback = document.playback; + auto& frameTime = document.frameTime; auto& zoom = document.previewZoom; auto& pan = document.previewPan; auto& isRootTransform = settings.previewIsRootTransform; @@ -53,7 +52,7 @@ namespace anm2ed::imgui } } - document.reference.frameTime = playback.time; + frameTime = playback.time; } if (manager.isRecording) @@ -124,6 +123,7 @@ namespace anm2ed::imgui settings.previewIsGrid = false; settings.previewIsAxes = false; settings.timelineIsOnlyShowLayers = true; + settings.onionskinIsEnabled = false; savedZoom = zoom; savedPan = pan; @@ -211,13 +211,13 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(2); - shortcut(settings.shortcutCenterView); + shortcut(manager.chords[SHORTCUT_CENTER_VIEW]); if (ImGui::Button("Center View", widgetSize)) pan = vec2(); set_item_tooltip_shortcut("Centers the view.", settings.shortcutCenterView); ImGui::SameLine(); - shortcut(settings.shortcutFit); + shortcut(manager.chords[SHORTCUT_FIT]); if (ImGui::Button("Fit", widgetSize)) if (animation) set_to_rect(zoom, pan, animation->rect(isRootTransform)); set_item_tooltip_shortcut("Set the view to match the extent of the animation.", settings.shortcutFit); @@ -397,7 +397,7 @@ namespace anm2ed::imgui onionskin_render(time, settings.onionskinAfterCount, 1, settings.onionskinAfterColor); }; - auto frameTime = reference.frameTime > -1 && !playback.isPlaying ? reference.frameTime : playback.time; + auto frameTime = document.frameTime > -1 && !playback.isPlaying ? document.frameTime : playback.time; if (animation) { @@ -408,7 +408,7 @@ namespace anm2ed::imgui render(animation, frameTime); - if (auto overlayAnimation = anm2.animation_get({overlayIndex})) + if (auto overlayAnimation = anm2.animation_get(overlayIndex)) render(overlayAnimation, frameTime, {}, 1.0f - math::uint8_to_float(overlayTransparency)); if (drawOrder == draw_order::ABOVE && isEnabled) onionskins_render(frameTime); @@ -469,8 +469,8 @@ namespace anm2ed::imgui auto isKeyDown = isLeftDown || isRightDown || isUpDown || isDownDown; auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased; - auto isZoomIn = chord_repeating(string_to_chord(settings.shortcutZoomIn)); - auto isZoomOut = chord_repeating(string_to_chord(settings.shortcutZoomOut)); + auto isZoomIn = chord_repeating(manager.chords[SHORTCUT_ZOOM_IN]); + auto isZoomOut = chord_repeating(manager.chords[SHORTCUT_ZOOM_OUT]); auto isBegin = isMouseClicked || isKeyJustPressed; auto isDuring = isMouseDown || isKeyDown; diff --git a/src/imgui/window/animations.cpp b/src/imgui/window/animations.cpp index dcac6c1..4fc6e3d 100644 --- a/src/imgui/window/animations.cpp +++ b/src/imgui/window/animations.cpp @@ -48,7 +48,10 @@ namespace anm2ed::imgui ImGui::SetNextItemSelectionUserData((int)i); if (selectable_input_text(animation.name, std::format("###Document #{} Animation #{}", manager.selected, i), animation.name, selection.contains((int)i))) + { reference = {(int)i}; + document.frames.clear(); + } if (ImGui::IsItemHovered()) hovered = (int)i; ImGui::PopFont(); @@ -155,9 +158,9 @@ namespace anm2ed::imgui DOCUMENT_EDIT(document, "Paste Animation(s)", Document::ANIMATIONS, deserialize()); }; - if (shortcut(settings.shortcutCut, shortcut::FOCUSED)) cut(); - if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); - if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(); + if (shortcut(manager.chords[SHORTCUT_CUT], shortcut::FOCUSED)) cut(); + if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); + if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(); if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { @@ -171,7 +174,7 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(5); - shortcut(settings.shortcutAdd); + shortcut(manager.chords[SHORTCUT_ADD]); if (ImGui::Button("Add", widgetSize)) { auto add = [&]() @@ -204,7 +207,7 @@ namespace anm2ed::imgui ImGui::BeginDisabled(selection.empty()); { - shortcut(settings.shortcutDuplicate); + shortcut(manager.chords[SHORTCUT_DUPLICATE]); if (ImGui::Button("Duplicate", widgetSize)) { auto duplicate = [&]() @@ -225,7 +228,7 @@ namespace anm2ed::imgui ImGui::SameLine(); - if (shortcut(settings.shortcutMerge, shortcut::FOCUSED) && !selection.empty()) + if (shortcut(manager.chords[SHORTCUT_MERGE], shortcut::FOCUSED) && !selection.empty()) { auto merge_quick = [&]() { @@ -270,7 +273,7 @@ namespace anm2ed::imgui ImGui::SameLine(); - shortcut(settings.shortcutRemove); + shortcut(manager.chords[SHORTCUT_REMOVE]); if (ImGui::Button("Remove", widgetSize)) { auto remove = [&]() @@ -285,11 +288,11 @@ namespace anm2ed::imgui DOCUMENT_EDIT(document, "Remove Animation(s)", Document::ANIMATIONS, remove()); } - set_item_tooltip_shortcut("Remove the selected animation(s).", settings.shortcutDuplicate); + set_item_tooltip_shortcut("Remove the selected animation(s).", settings.shortcutRemove); ImGui::SameLine(); - shortcut(settings.shortcutDefault); + shortcut(manager.chords[SHORTCUT_DEFAULT]); ImGui::BeginDisabled(selection.size() != 1); if (ImGui::Button("Default", widgetSize)) { @@ -369,21 +372,25 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(2); - if (ImGui::Button("Merge", widgetSize)) + ImGui::BeginDisabled(mergeSelection.empty()); { - auto merge = [&]() + if (ImGui::Button("Merge", widgetSize)) { - if (mergeSelection.contains(overlayIndex)) overlayIndex = -1; - auto merged = - anm2.animations_merge(mergeReference, mergeSelection, (merge::Type)type, isDeleteAnimationsAfter); + auto merge = [&]() + { + if (mergeSelection.contains(overlayIndex)) overlayIndex = -1; + auto merged = + anm2.animations_merge(mergeReference, mergeSelection, (merge::Type)type, isDeleteAnimationsAfter); - selection = {merged}; - reference = {merged}; - }; + selection = {merged}; + reference = {merged}; + }; - DOCUMENT_EDIT(document, "Merge Animations", Document::ANIMATIONS, merge()); - merge_close(); + DOCUMENT_EDIT(document, "Merge Animations", Document::ANIMATIONS, merge()); + merge_close(); + } } + ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button("Close", widgetSize)) merge_close(); diff --git a/src/imgui/window/events.cpp b/src/imgui/window/events.cpp index e7f5843..f9c652b 100644 --- a/src/imgui/window/events.cpp +++ b/src/imgui/window/events.cpp @@ -73,8 +73,8 @@ namespace anm2ed::imgui toasts.error(std::format("Failed to deserialize event(s): {}", errorString)); }; - if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); - if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); + if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); + if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND); if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { @@ -96,7 +96,7 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(2); - shortcut(settings.shortcutAdd); + shortcut(manager.chords[SHORTCUT_ADD]); if (ImGui::Button("Add", widgetSize)) { auto add = [&]() @@ -112,7 +112,7 @@ namespace anm2ed::imgui set_item_tooltip_shortcut("Add an event.", settings.shortcutAdd); ImGui::SameLine(); - shortcut(settings.shortcutRemove); + shortcut(manager.chords[SHORTCUT_REMOVE]); ImGui::BeginDisabled(unused.empty()); if (ImGui::Button("Remove Unused", widgetSize)) { diff --git a/src/imgui/window/frame_properties.cpp b/src/imgui/window/frame_properties.cpp index 5ffab31..34e275d 100644 --- a/src/imgui/window/frame_properties.cpp +++ b/src/imgui/window/frame_properties.cpp @@ -16,108 +16,217 @@ namespace anm2ed::imgui if (ImGui::Begin("Frame Properties", &settings.windowIsFrameProperties)) { auto& document = *manager.get(); + auto& frames = document.frames.selection; auto& type = document.reference.itemType; - auto frame = document.frame_get(); - auto useFrame = frame ? *frame : anm2::Frame(); - ImGui::BeginDisabled(!frame); + if (frames.size() <= 1) { - if (type == anm2::TRIGGER) + auto frame = document.frame_get(); + auto useFrame = frame ? *frame : anm2::Frame(); + + ImGui::BeginDisabled(!frame); { - if (combo_negative_one_indexed("Event", frame ? &useFrame.eventID : &dummy_value(), - document.event.labels)) - DOCUMENT_EDIT(document, "Trigger Event", Document::FRAMES, frame->eventID = useFrame.eventID); - ImGui::SetItemTooltip("Change the event this trigger uses."); - - if (combo_negative_one_indexed("Sound", frame ? &useFrame.soundID : &dummy_value(), - document.sound.labels)) - DOCUMENT_EDIT(document, "Trigger Sound", Document::FRAMES, frame->soundID = useFrame.soundID); - ImGui::SetItemTooltip("Change the sound this trigger uses."); - - if (ImGui::InputInt("At Frame", frame ? &useFrame.atFrame : &dummy_value(), imgui::STEP, - imgui::STEP_FAST, !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0)) - DOCUMENT_EDIT(document, "Trigger At Frame", Document::FRAMES, frame->atFrame = useFrame.atFrame); - ImGui::SetItemTooltip("Change the frame the trigger will be activated at."); - } - else - { - ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_); + if (type == anm2::TRIGGER) { - if (ImGui::InputFloat2("Crop", frame ? value_ptr(useFrame.crop) : &dummy_value(), - frame ? vec2_format_get(useFrame.crop) : "")) - DOCUMENT_EDIT(document, "Frame Crop", Document::FRAMES, frame->crop = useFrame.crop); - ImGui::SetItemTooltip("Change the crop position the frame uses."); + if (combo_negative_one_indexed("Event", frame ? &useFrame.eventID : &dummy_value_negative(), + document.event.labels)) + DOCUMENT_EDIT(document, "Trigger Event", Document::FRAMES, frame->eventID = useFrame.eventID); + ImGui::SetItemTooltip("Change the event this trigger uses."); - if (ImGui::InputFloat2("Size", frame ? value_ptr(useFrame.size) : &dummy_value(), - frame ? vec2_format_get(useFrame.size) : "")) - DOCUMENT_EDIT(document, "Frame Size", Document::FRAMES, frame->size = useFrame.size); - ImGui::SetItemTooltip("Change the size of the crop the frame uses."); + if (combo_negative_one_indexed("Sound", frame ? &useFrame.soundID : &dummy_value_negative(), + document.sound.labels)) + DOCUMENT_EDIT(document, "Trigger Sound", Document::FRAMES, frame->soundID = useFrame.soundID); + ImGui::SetItemTooltip("Change the sound this trigger uses."); + + if (ImGui::InputInt("At Frame", frame ? &useFrame.atFrame : &dummy_value(), STEP, STEP_FAST, + !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0)) + DOCUMENT_EDIT(document, "Trigger At Frame", Document::FRAMES, frame->atFrame = useFrame.atFrame); + ImGui::SetItemTooltip("Change the frame the trigger will be activated at."); + + if (ImGui::Checkbox("Visible", frame ? &useFrame.isVisible : &dummy_value())) + DOCUMENT_EDIT(document, "Trigger Visibility", Document::FRAMES, frame->isVisible = useFrame.isVisible); + ImGui::SetItemTooltip("Toggle the trigger's visibility."); } - ImGui::EndDisabled(); - - if (ImGui::InputFloat2("Position", frame ? value_ptr(useFrame.position) : &dummy_value(), - frame ? vec2_format_get(useFrame.position) : "")) - DOCUMENT_EDIT(document, "Frame Position", Document::FRAMES, frame->position = useFrame.position); - ImGui::SetItemTooltip("Change the position of the frame."); - - ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_); + else { - if (ImGui::InputFloat2("Pivot", frame ? value_ptr(useFrame.pivot) : &dummy_value(), - frame ? vec2_format_get(useFrame.pivot) : "")) - DOCUMENT_EDIT(document, "Frame Pivot", Document::FRAMES, frame->pivot = useFrame.pivot); - ImGui::SetItemTooltip("Change the pivot of the frame; i.e., where it is centered."); + ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_); + { + if (ImGui::InputFloat2("Crop", frame ? value_ptr(useFrame.crop) : &dummy_value(), + frame ? vec2_format_get(useFrame.crop) : "")) + DOCUMENT_EDIT(document, "Frame Crop", Document::FRAMES, frame->crop = useFrame.crop); + ImGui::SetItemTooltip("Change the crop position the frame uses."); + + if (ImGui::InputFloat2("Size", frame ? value_ptr(useFrame.size) : &dummy_value(), + frame ? vec2_format_get(useFrame.size) : "")) + DOCUMENT_EDIT(document, "Frame Size", Document::FRAMES, frame->size = useFrame.size); + ImGui::SetItemTooltip("Change the size of the crop the frame uses."); + } + ImGui::EndDisabled(); + + if (ImGui::InputFloat2("Position", frame ? value_ptr(useFrame.position) : &dummy_value(), + frame ? vec2_format_get(useFrame.position) : "")) + DOCUMENT_EDIT(document, "Frame Position", Document::FRAMES, frame->position = useFrame.position); + ImGui::SetItemTooltip("Change the position of the frame."); + + ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_); + { + if (ImGui::InputFloat2("Pivot", frame ? value_ptr(useFrame.pivot) : &dummy_value(), + frame ? vec2_format_get(useFrame.pivot) : "")) + DOCUMENT_EDIT(document, "Frame Pivot", Document::FRAMES, frame->pivot = useFrame.pivot); + ImGui::SetItemTooltip("Change the pivot of the frame; i.e., where it is centered."); + } + ImGui::EndDisabled(); + + if (ImGui::InputFloat2("Scale", frame ? value_ptr(useFrame.scale) : &dummy_value(), + frame ? vec2_format_get(useFrame.scale) : "")) + DOCUMENT_EDIT(document, "Frame Scale", Document::FRAMES, frame->scale = useFrame.scale); + ImGui::SetItemTooltip("Change the scale of the frame, in percent."); + + if (ImGui::InputFloat("Rotation", frame ? &useFrame.rotation : &dummy_value(), STEP, STEP_FAST, + frame ? float_format_get(useFrame.rotation) : "")) + DOCUMENT_EDIT(document, "Frame Rotation", Document::FRAMES, frame->rotation = useFrame.rotation); + ImGui::SetItemTooltip("Change the rotation of the frame."); + + if (input_int_range("Duration", frame ? useFrame.duration : dummy_value(), + frame ? anm2::FRAME_DURATION_MIN : 0, anm2::FRAME_DURATION_MAX, STEP, STEP_FAST, + !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0)) + DOCUMENT_EDIT(document, "Frame Duration", Document::FRAMES, frame->duration = useFrame.duration); + ImGui::SetItemTooltip("Change how long the frame lasts."); + + if (ImGui::ColorEdit4("Tint", frame ? value_ptr(useFrame.tint) : &dummy_value())) + DOCUMENT_EDIT(document, "Frame Tint", Document::FRAMES, frame->tint = useFrame.tint); + ImGui::SetItemTooltip("Change the tint of the frame."); + + if (ImGui::ColorEdit3("Color Offset", frame ? value_ptr(useFrame.colorOffset) : &dummy_value())) + DOCUMENT_EDIT(document, "Frame Color Offset", Document::FRAMES, + frame->colorOffset = useFrame.colorOffset); + ImGui::SetItemTooltip("Change the color added onto the frame."); + + if (ImGui::Checkbox("Visible", frame ? &useFrame.isVisible : &dummy_value())) + DOCUMENT_EDIT(document, "Frame Visibility", Document::FRAMES, frame->isVisible = useFrame.isVisible); + ImGui::SetItemTooltip("Toggle the frame's visibility."); + + ImGui::SameLine(); + + if (ImGui::Checkbox("Interpolated", frame ? &useFrame.isInterpolated : &dummy_value())) + DOCUMENT_EDIT(document, "Frame Interpolation", Document::FRAMES, + frame->isInterpolated = useFrame.isInterpolated); + ImGui::SetItemTooltip( + "Toggle the frame interpolating; i.e., blending its values into the next frame based on the time."); + + auto widgetSize = widget_size_with_row_get(2); + + if (ImGui::Button("Flip X", widgetSize)) + DOCUMENT_EDIT(document, "Frame Flip X", Document::FRAMES, frame->scale.x = -frame->scale.x); + ImGui::SetItemTooltip("%s", "Flip the horizontal scale of the frame, to cheat mirroring the frame " + "horizontally.\n(Note: the format does not support mirroring.)"); + ImGui::SameLine(); + if (ImGui::Button("Flip Y", widgetSize)) + DOCUMENT_EDIT(document, "Frame Flip Y", Document::FRAMES, frame->scale.y = -frame->scale.y); + ImGui::SetItemTooltip("%s", "Flip the vertical scale of the frame, to cheat mirroring the frame " + "vertically.\n(Note: the format does not support mirroring.)"); } - ImGui::EndDisabled(); - - if (ImGui::InputFloat2("Scale", frame ? value_ptr(useFrame.scale) : &dummy_value(), - frame ? vec2_format_get(useFrame.scale) : "")) - DOCUMENT_EDIT(document, "Frame Scale", Document::FRAMES, frame->scale = useFrame.scale); - ImGui::SetItemTooltip("Change the scale of the frame, in percent."); - - if (ImGui::InputFloat("Rotation", frame ? &useFrame.rotation : &dummy_value(), imgui::STEP, - imgui::STEP_FAST, frame ? float_format_get(useFrame.rotation) : "")) - DOCUMENT_EDIT(document, "Frame Rotation", Document::FRAMES, frame->rotation = useFrame.rotation); - ImGui::SetItemTooltip("Change the rotation of the frame."); - - if (ImGui::InputInt("Duration", frame ? &useFrame.delay : &dummy_value(), imgui::STEP, imgui::STEP_FAST, - !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0)) - DOCUMENT_EDIT(document, "Frame Duration", Document::FRAMES, frame->delay = useFrame.delay); - ImGui::SetItemTooltip("Change how long the frame lasts."); - - if (ImGui::ColorEdit4("Tint", frame ? value_ptr(useFrame.tint) : &dummy_value())) - DOCUMENT_EDIT(document, "Frame Tint", Document::FRAMES, frame->tint = useFrame.tint); - ImGui::SetItemTooltip("Change the tint of the frame."); - - if (ImGui::ColorEdit3("Color Offset", frame ? value_ptr(useFrame.colorOffset) : &dummy_value())) - DOCUMENT_EDIT(document, "Frame Color Offset", Document::FRAMES, frame->colorOffset = useFrame.colorOffset); - ImGui::SetItemTooltip("Change the color added onto the frame."); - - if (ImGui::Checkbox("Visible", frame ? &useFrame.isVisible : &dummy_value())) - DOCUMENT_EDIT(document, "Frame Visibility", Document::FRAMES, frame->isVisible = useFrame.isVisible); - ImGui::SetItemTooltip("Toggle the frame's visibility."); - - ImGui::SameLine(); - - if (ImGui::Checkbox("Interpolated", frame ? &useFrame.isInterpolated : &dummy_value())) - DOCUMENT_EDIT(document, "Frame Interpolation", Document::FRAMES, - frame->isInterpolated = useFrame.isInterpolated); - ImGui::SetItemTooltip( - "Toggle the frame interpolating; i.e., blending its values into the next frame based on the time."); - - auto widgetSize = imgui::widget_size_with_row_get(2); - - if (ImGui::Button("Flip X", widgetSize)) - DOCUMENT_EDIT(document, "Frame Flip X", Document::FRAMES, frame->scale.x = -frame->scale.x); - ImGui::SetItemTooltip("%s", "Flip the horizontal scale of the frame, to cheat mirroring the frame " - "horizontally.\n(Note: the format does not support mirroring.)"); - ImGui::SameLine(); - if (ImGui::Button("Flip Y", widgetSize)) - DOCUMENT_EDIT(document, "Frame Flip Y", Document::FRAMES, frame->scale.y = -frame->scale.y); - ImGui::SetItemTooltip("%s", "Flip the vertical scale of the frame, to cheat mirroring the frame " - "vertically.\n(Note: the format does not support mirroring.)"); } + ImGui::EndDisabled(); + } + else + { + auto& isCrop = settings.changeIsCrop; + auto& isSize = settings.changeIsSize; + auto& isPosition = settings.changeIsPosition; + auto& isPivot = settings.changeIsPivot; + auto& isScale = settings.changeIsScale; + auto& isRotation = settings.changeIsRotation; + auto& isDelay = settings.changeIsDelay; + auto& isTint = settings.changeIsTint; + auto& isColorOffset = settings.changeIsColorOffset; + auto& isVisibleSet = settings.changeIsVisibleSet; + auto& isInterpolatedSet = settings.changeIsInterpolatedSet; + auto& crop = settings.changeCrop; + auto& size = settings.changeSize; + auto& position = settings.changePosition; + auto& pivot = settings.changePivot; + auto& scale = settings.changeScale; + auto& rotation = settings.changeRotation; + auto& duration = settings.changeDelay; + auto& tint = settings.changeTint; + auto& colorOffset = settings.changeColorOffset; + auto& isVisible = settings.changeIsVisible; + auto& isInterpolated = settings.changeIsInterpolated; + +#define PROPERTIES_WIDGET(body) \ + ImGui::Checkbox(checkboxLabel, &isEnabled); \ + ImGui::SameLine(); \ + ImGui::BeginDisabled(!isEnabled); \ + body; \ + ImGui::EndDisabled(); + + auto bool_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, bool& value) + { PROPERTIES_WIDGET(ImGui::Checkbox(valueLabel, &value)); }; + + auto color3_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec3& value) + { PROPERTIES_WIDGET(ImGui::ColorEdit3(valueLabel, value_ptr(value))); }; + + auto color4_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec4& value) + { PROPERTIES_WIDGET(ImGui::ColorEdit4(valueLabel, value_ptr(value))); }; + + auto float2_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec2& value) + { PROPERTIES_WIDGET(ImGui::InputFloat2(valueLabel, value_ptr(value), vec2_format_get(value))); }; + + auto float_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, float& value) + { PROPERTIES_WIDGET(ImGui::InputFloat(valueLabel, &value, STEP, STEP_FAST, float_format_get(value))); }; + + auto duration_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, int& value) + { + PROPERTIES_WIDGET( + input_int_range(valueLabel, value, anm2::FRAME_DURATION_MIN, anm2::FRAME_DURATION_MAX, STEP, STEP_FAST)); + }; + +#undef PROPERTIES_WIDGET + + float2_value("##Is Crop", "Crop", isCrop, crop); + float2_value("##Is Size", "Size", isSize, size); + float2_value("##Is Position", "Position", isPosition, position); + float2_value("##Is Pivot", "Pivot", isPivot, pivot); + float2_value("##Is Scale", "Scale", isScale, scale); + float_value("##Is Rotation", "Rotation", isRotation, rotation); + duration_value("##Is Delay", "Delay", isDelay, duration); + color4_value("##Is Tint", "Tint", isTint, tint); + color3_value("##Is Color Offset", "Color Offset", isColorOffset, colorOffset); + bool_value("##Is Visible", "Visible", isVisibleSet, isVisible); + ImGui::SameLine(); + bool_value("##Is Interpolated", "Interpolated", isInterpolatedSet, isInterpolated); + + auto frame_change = [&](anm2::ChangeType type) + { + anm2::FrameChange frameChange; + if (isCrop) frameChange.crop = std::make_optional(crop); + if (isSize) frameChange.size = std::make_optional(size); + if (isPosition) frameChange.position = std::make_optional(position); + if (isPivot) frameChange.pivot = std::make_optional(pivot); + if (isScale) frameChange.scale = std::make_optional(scale); + if (isRotation) frameChange.rotation = std::make_optional(rotation); + if (isDelay) frameChange.duration = std::make_optional(duration); + if (isTint) frameChange.tint = std::make_optional(tint); + if (isColorOffset) frameChange.colorOffset = std::make_optional(colorOffset); + if (isVisibleSet) frameChange.isVisible = std::make_optional(isVisible); + if (isInterpolatedSet) frameChange.isInterpolated = std::make_optional(isInterpolated); + + DOCUMENT_EDIT(document, "Change Frame Properties", Document::FRAMES, + document.item_get()->frames_change(frameChange, type, *frames.begin(), (int)frames.size())); + }; + + auto widgetSize = widget_size_with_row_get(3); + + if (ImGui::Button("Adjust", widgetSize)) frame_change(anm2::ADJUST); + ImGui::SetItemTooltip("Set the value of each specified value onto the frame's equivalent."); + ImGui::SameLine(); + if (ImGui::Button("Add", widgetSize)) frame_change(anm2::ADD); + ImGui::SetItemTooltip("Add the specified values onto each frame.\n(Boolean values will simply be set.)"); + ImGui::SameLine(); + if (ImGui::Button("Subtract", widgetSize)) frame_change(anm2::SUBTRACT); + ImGui::SetItemTooltip("Subtract the specified values from each frame.\n(Boolean values will simply be set.)"); } - ImGui::EndDisabled(); } ImGui::End(); } diff --git a/src/imgui/window/layers.cpp b/src/imgui/window/layers.cpp index b09d1e9..90200cb 100644 --- a/src/imgui/window/layers.cpp +++ b/src/imgui/window/layers.cpp @@ -84,8 +84,8 @@ namespace anm2ed::imgui toasts.error(std::format("Failed to deserialize layer(s): {}", errorString)); }; - if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); - if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); + if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); + if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND); if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { @@ -107,12 +107,12 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(2); - shortcut(settings.shortcutAdd); + shortcut(manager.chords[SHORTCUT_ADD]); if (ImGui::Button("Add", widgetSize)) manager.layer_properties_open(); set_item_tooltip_shortcut("Add a layer.", settings.shortcutAdd); ImGui::SameLine(); - shortcut(settings.shortcutRemove); + shortcut(manager.chords[SHORTCUT_REMOVE]); ImGui::BeginDisabled(unused.empty()); if (ImGui::Button("Remove Unused", widgetSize)) { diff --git a/src/imgui/window/nulls.cpp b/src/imgui/window/nulls.cpp index db776f0..641f398 100644 --- a/src/imgui/window/nulls.cpp +++ b/src/imgui/window/nulls.cpp @@ -84,8 +84,8 @@ namespace anm2ed::imgui toasts.error(std::format("Failed to deserialize null(s): {}", errorString)); }; - if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); - if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); + if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); + if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND); if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { @@ -107,12 +107,12 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(2); - shortcut(settings.shortcutAdd); + shortcut(manager.chords[SHORTCUT_ADD]); if (ImGui::Button("Add", widgetSize)) manager.null_properties_open(); set_item_tooltip_shortcut("Add a null.", settings.shortcutAdd); ImGui::SameLine(); - shortcut(settings.shortcutRemove); + shortcut(manager.chords[SHORTCUT_REMOVE]); ImGui::BeginDisabled(unused.empty()); if (ImGui::Button("Remove Unused", widgetSize)) { diff --git a/src/imgui/window/onionskin.cpp b/src/imgui/window/onionskin.cpp index 793d5b1..76704f8 100644 --- a/src/imgui/window/onionskin.cpp +++ b/src/imgui/window/onionskin.cpp @@ -11,7 +11,7 @@ namespace anm2ed::imgui { constexpr auto FRAMES_MAX = 100; - void Onionskin::update(Settings& settings) + void Onionskin::update(Manager& manager, Settings& settings) { auto& isEnabled = settings.onionskinIsEnabled; auto& beforeCount = settings.onionskinBeforeCount; @@ -49,7 +49,7 @@ namespace anm2ed::imgui } ImGui::End(); - if (shortcut(settings.shortcutOnionskin, shortcut::GLOBAL)) isEnabled = !isEnabled; + if (shortcut(manager.chords[SHORTCUT_ONIONSKIN], shortcut::GLOBAL)) isEnabled = !isEnabled; } } diff --git a/src/imgui/window/onionskin.h b/src/imgui/window/onionskin.h index 6b23aed..df3847c 100644 --- a/src/imgui/window/onionskin.h +++ b/src/imgui/window/onionskin.h @@ -1,12 +1,12 @@ #pragma once -#include "settings.h" +#include "manager.h" namespace anm2ed::imgui { class Onionskin { public: - void update(Settings&); + void update(Manager&, Settings&); }; } diff --git a/src/imgui/window/sounds.cpp b/src/imgui/window/sounds.cpp index 35f1cf4..aa6421a 100644 --- a/src/imgui/window/sounds.cpp +++ b/src/imgui/window/sounds.cpp @@ -79,8 +79,8 @@ namespace anm2ed::imgui toasts.error(std::format("Failed to deserialize sound(s): {}", errorString)); }; - if (imgui::shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); - if (imgui::shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); + if (imgui::shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); + if (imgui::shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND); if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { @@ -102,12 +102,12 @@ namespace anm2ed::imgui auto widgetSize = imgui::widget_size_with_row_get(2); - imgui::shortcut(settings.shortcutAdd); + imgui::shortcut(manager.chords[SHORTCUT_ADD]); if (ImGui::Button("Add", widgetSize)) dialog.file_open(dialog::SOUND_OPEN); imgui::set_item_tooltip_shortcut("Add a sound.", settings.shortcutAdd); ImGui::SameLine(); - imgui::shortcut(settings.shortcutRemove); + imgui::shortcut(manager.chords[SHORTCUT_REMOVE]); ImGui::BeginDisabled(unused.empty()); if (ImGui::Button("Remove Unused", widgetSize)) { diff --git a/src/imgui/window/spritesheet_editor.cpp b/src/imgui/window/spritesheet_editor.cpp index 7d91fb5..e7f2b8c 100644 --- a/src/imgui/window/spritesheet_editor.cpp +++ b/src/imgui/window/spritesheet_editor.cpp @@ -17,9 +17,7 @@ namespace anm2ed::imgui { constexpr auto PIVOT_COLOR = color::PINK; - SpritesheetEditor::SpritesheetEditor() : Canvas(vec2()) - { - } + SpritesheetEditor::SpritesheetEditor() : Canvas(vec2()) {} void SpritesheetEditor::update(Manager& manager, Settings& settings, Resources& resources) { @@ -80,13 +78,13 @@ namespace anm2ed::imgui auto widgetSize = ImVec2(imgui::row_widget_width_get(2), 0); - imgui::shortcut(settings.shortcutCenterView); + imgui::shortcut(manager.chords[SHORTCUT_CENTER_VIEW]); if (ImGui::Button("Center View", widgetSize)) center_view(); imgui::set_item_tooltip_shortcut("Centers the view.", settings.shortcutCenterView); ImGui::SameLine(); - imgui::shortcut(settings.shortcutFit); + imgui::shortcut(manager.chords[SHORTCUT_FIT]); if (ImGui::Button("Fit", widgetSize)) if (spritesheet) set_to_rect(zoom, pan, {0, 0, spritesheet->texture.size.x, spritesheet->texture.size.y}); imgui::set_item_tooltip_shortcut("Set the view to match the extent of the spritesheet.", settings.shortcutFit); diff --git a/src/imgui/window/spritesheets.cpp b/src/imgui/window/spritesheets.cpp index 804690a..2e2d0ff 100644 --- a/src/imgui/window/spritesheets.cpp +++ b/src/imgui/window/spritesheets.cpp @@ -52,8 +52,8 @@ namespace anm2ed::imgui toasts.error(std::format("Failed to deserialize spritesheet(s): {}", errorString)); }; - if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); - if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(merge::APPEND); + if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); + if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(merge::APPEND); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); @@ -181,9 +181,9 @@ namespace anm2ed::imgui } ImGui::EndChild(); - auto rowOneWidgetSize = widget_size_with_row_get(4); + auto rowOneWidgetSize = widget_size_with_row_get(3); - shortcut(settings.shortcutAdd); + shortcut(manager.chords[SHORTCUT_ADD]); if (ImGui::Button("Add", rowOneWidgetSize)) dialog.file_open(dialog::SPRITESHEET_OPEN); set_item_tooltip_shortcut("Add a new spritesheet.", settings.shortcutAdd); @@ -238,12 +238,12 @@ namespace anm2ed::imgui dialog.reset(); } - ImGui::SameLine(); + auto rowTwoWidgetSize = widget_size_with_row_get(2); ImGui::BeginDisabled(unused.empty()); { - shortcut(settings.shortcutRemove); - if (ImGui::Button("Remove Unused", rowOneWidgetSize)) + shortcut(manager.chords[SHORTCUT_REMOVE]); + if (ImGui::Button("Remove Unused", rowTwoWidgetSize)) { auto remove_unused = [&]() { @@ -263,26 +263,6 @@ namespace anm2ed::imgui } ImGui::EndDisabled(); - auto rowTwoWidgetSize = widget_size_with_row_get(3); - - shortcut(settings.shortcutSelectAll); - ImGui::BeginDisabled(selection.size() == anm2.content.spritesheets.size()); - { - if (ImGui::Button("Select All", rowTwoWidgetSize)) - for (auto& id : anm2.content.spritesheets | std::views::keys) - selection.insert(id); - } - ImGui::EndDisabled(); - set_item_tooltip_shortcut("Select all spritesheets.", settings.shortcutSelectAll); - - ImGui::SameLine(); - - shortcut(settings.shortcutSelectNone); - ImGui::BeginDisabled(selection.empty()); - if (ImGui::Button("Select None", rowTwoWidgetSize)) selection.clear(); - set_item_tooltip_shortcut("Unselect all spritesheets.", settings.shortcutSelectNone); - ImGui::EndDisabled(); - ImGui::SameLine(); ImGui::BeginDisabled(selection.empty()); diff --git a/src/imgui/window/timeline.cpp b/src/imgui/window/timeline.cpp index 0558eba..eeb0db0 100644 --- a/src/imgui/window/timeline.cpp +++ b/src/imgui/window/timeline.cpp @@ -1,13 +1,17 @@ #include "timeline.h" +#include #include #include #include "toast.h" +#include "vector_.h" + using namespace anm2ed::resource; using namespace anm2ed::types; +using namespace anm2ed::util; using namespace glm; namespace anm2ed::imgui @@ -15,13 +19,18 @@ namespace anm2ed::imgui constexpr auto COLOR_HIDDEN_MULTIPLIER = vec4(0.5f, 0.5f, 0.5f, 1.000f); constexpr auto FRAME_TIMELINE_COLOR = ImVec4(0.106f, 0.184f, 0.278f, 1.000f); constexpr auto FRAME_BORDER_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.15f); + constexpr auto FRAME_BORDER_COLOR_REFERENCED = ImVec4(1.0f, 1.0f, 1.0f, 0.50f); constexpr auto FRAME_MULTIPLE_OVERLAY_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.05f); constexpr auto PLAYHEAD_LINE_THICKNESS = 4.0f; + constexpr auto FRAME_BORDER_THICKNESS = 2.5f; + constexpr auto FRAME_BORDER_THICKNESS_REFERENCED = 5.0f; + constexpr auto TEXT_MULTIPLE_COLOR = to_imvec4(color::WHITE); constexpr auto PLAYHEAD_LINE_COLOR = to_imvec4(color::WHITE); constexpr auto FRAME_MULTIPLE = 5; + constexpr auto FRAME_DRAG_PAYLOAD_ID = "Frame Drag Drop"; constexpr auto HELP_FORMAT = R"(- Press {} to decrement time. - Press {} to increment time. @@ -32,50 +41,69 @@ namespace anm2ed::imgui void Timeline::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) { auto& document = *manager.get(); + auto& anm2 = document.anm2; auto& playback = document.playback; auto& reference = document.reference; + auto& frames = document.frames; auto animation = document.animation_get(); style = ImGui::GetStyle(); + auto frames_delete = [&]() + { + if (auto item = animation->item_get(reference.itemType, reference.itemID); item) + { + for (auto& i : frames.selection | std::views::reverse) + item->frames.erase(item->frames.begin() + i); + + reference.frameIndex = -1; + frames.clear(); + } + }; + auto context_menu = [&]() { - auto& hoveredFrame = document.hoveredFrame; - auto& anm2 = document.anm2; - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); auto copy = [&]() { - if (auto frame = anm2.frame_get(hoveredFrame)) clipboard.set(frame->to_string(hoveredFrame.itemType)); + if (auto item = animation->item_get(reference.itemType, reference.itemID); item) + { + std::string clipboardString{}; + for (auto& i : frames.selection) + { + if (!vector::in_bounds(item->frames, i)) break; + clipboardString += item->frames[i].to_string(reference.itemType); + } + clipboard.set(clipboardString); + } }; auto cut = [&]() { copy(); - auto frames_delete = [&]() - { - if (auto item = anm2.item_get(reference); item) - { - item->frames.erase(item->frames.begin() + reference.frameIndex); - reference.frameIndex = glm::max(-1, --reference.frameIndex); - } - }; - DOCUMENT_EDIT(document, "Cut Frame(s)", Document::FRAMES, frames_delete()); }; auto paste = [&]() { - if (auto item = document.item_get()) + if (auto item = animation->item_get(reference.itemType, reference.itemID)) { document.snapshot("Paste Frame(s)"); std::set indices{}; std::string errorString{}; - auto start = reference.frameIndex + 1; + auto insertIndex = reference.frameIndex == -1 ? item->frames.size() : reference.frameIndex + 1; + auto start = reference.itemType == anm2::TRIGGER ? hoveredTime : insertIndex; if (item->frames_deserialize(clipboard.get(), reference.itemType, start, indices, &errorString)) + { + frames.selection.clear(); + for (auto i : indices) + frames.selection.insert(i); + reference.frameIndex = *indices.begin(); + animation->fit_length(); document.change(Document::FRAMES); + } else toasts.error(std::format("Failed to deserialize frame(s): {}", errorString)); } @@ -83,14 +111,14 @@ namespace anm2ed::imgui toasts.error(std::format("Failed to deserialize frame(s): select an item first!")); }; - if (shortcut(settings.shortcutCut, shortcut::FOCUSED)) cut(); - if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); - if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(); + if (shortcut(manager.chords[SHORTCUT_CUT], shortcut::FOCUSED)) cut(); + if (shortcut(manager.chords[SHORTCUT_COPY], shortcut::FOCUSED)) copy(); + if (shortcut(manager.chords[SHORTCUT_PASTE], shortcut::FOCUSED)) paste(); if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) { - if (ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, hoveredFrame != anm2::Reference{})) cut(); - if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, hoveredFrame != anm2::Reference{})) copy(); + if (ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, !frames.selection.empty())) cut(); + if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, !frames.selection.empty())) copy(); if (ImGui::MenuItem("Paste", nullptr, false, !clipboard.is_empty())) paste(); ImGui::EndPopup(); } @@ -98,9 +126,19 @@ namespace anm2ed::imgui ImGui::PopStyleVar(2); }; + auto item_properties_reset = [&]() + { + addItemName.clear(); + addItemSpritesheetID = {}; + addItemID = -1; + unusedItems = reference.itemType == anm2::LAYER ? anm2.layers_unused(*animation) + : reference.itemType == anm2::NULL_ ? anm2.nulls_unused(*animation) + : std::set{}; + }; + auto item_child = [&](anm2::Type type, int id, int& index) { - auto& anm2 = document.anm2; + ImGui::PushID(index); auto item = animation ? animation->item_get(type, id) : nullptr; auto isVisible = item ? item->isVisible : false; @@ -123,13 +161,24 @@ namespace anm2ed::imgui auto itemSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 2)); - if (ImGui::BeginChild(label.c_str(), itemSize, ImGuiChildFlags_Borders)) + if (ImGui::BeginChild(label.c_str(), itemSize, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollWithMouse)) { + auto isReferenced = reference.itemType == type && reference.itemID == id; + + auto cursorPos = ImGui::GetCursorPos(); + if (type != anm2::NONE) { - anm2::Reference itemReference = {reference.animationIndex, type, id}; + ImGui::SetCursorPos(to_imvec2(to_vec2(cursorPos) - to_vec2(style.ItemSpacing))); - if (ImGui::IsWindowHovered()) + ImGui::SetNextItemAllowOverlap(); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4()); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4()); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4()); + if (ImGui::Selectable("##Item Button", false, ImGuiSelectableFlags_None, itemSize)) + reference = {reference.animationIndex, type, id}; + ImGui::PopStyleColor(3); + if (ImGui::IsItemHovered()) { if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { @@ -144,16 +193,43 @@ namespace anm2ed::imgui break; } } - - if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) reference = itemReference; } + if (type == anm2::LAYER) + { + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + ImGui::SetDragDropPayload("Layer Animation Drag Drop", &id, sizeof(int)); + ImGui::EndDragDropSource(); + } + + if (ImGui::BeginDragDropTarget()) + { + if (auto payload = ImGui::AcceptDragDropPayload("Layer Animation Drag Drop")) + { + auto droppedID = *(int*)payload->Data; + + auto layer_order_move = [&]() + { + int source = vector::find_index(animation->layerOrder, droppedID); + int destination = vector::find_index(animation->layerOrder, id); + + if (source != -1 && destination != -1) vector::move_index(animation->layerOrder, source, destination); + }; + + DOCUMENT_EDIT(document, "Move Layer Animation", Document::ITEMS, layer_order_move()); + } + ImGui::EndDragDropTarget(); + } + } + + ImGui::SetCursorPos(cursorPos); + ImGui::Image(resources.icons[icon].id, icon_size_get()); ImGui::SameLine(); + if (isReferenced) ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(label.c_str()); - - anm2::Item* itemPtr = animation->item_get(type, id); - bool& itemVisible = itemPtr->isVisible; + if (isReferenced) ImGui::PopFont(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); @@ -163,18 +239,15 @@ namespace anm2ed::imgui ImGui::SetCursorPos( ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); - int visibleIcon = itemVisible ? icon::VISIBLE : icon::INVISIBLE; - + int visibleIcon = isVisible ? icon::VISIBLE : icon::INVISIBLE; if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, icon_size_get())) - DOCUMENT_EDIT(document, "Item Visibility", Document::FRAMES, itemVisible = !itemVisible); - ImGui::SetItemTooltip(itemVisible ? "The item is shown. Press to hide." - : "The item is hidden. Press to show."); + DOCUMENT_EDIT(document, "Item Visibility", Document::FRAMES, item->isVisible = !item->isVisible); + ImGui::SetItemTooltip(isVisible ? "The item is shown. Press to hide." : "The item is hidden. Press to show."); if (type == anm2::NULL_) { auto& null = anm2.content.nulls.at(id); auto& isShowRect = null.isShowRect; - auto rectIcon = isShowRect ? icon::SHOW_RECT : icon::HIDE_RECT; ImGui::SetCursorPos( ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, @@ -191,17 +264,15 @@ namespace anm2ed::imgui else { auto cursorPos = ImGui::GetCursorPos(); - auto& isShowUnused = settings.timelineIsShowUnused; - - ImGui::SetCursorPos( - ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, - (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); + ImGui::SetCursorPos( + ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, + (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); + auto& isShowUnused = settings.timelineIsShowUnused; auto unusedIcon = isShowUnused ? icon::SHOW_UNUSED : icon::HIDE_UNUSED; if (ImGui::ImageButton("##Unused Toggle", resources.icons[unusedIcon].id, icon_size_get())) isShowUnused = !isShowUnused; @@ -210,16 +281,13 @@ namespace anm2ed::imgui auto& showLayersOnly = settings.timelineIsOnlyShowLayers; auto layersIcon = showLayersOnly ? icon::SHOW_LAYERS : icon::HIDE_LAYERS; - ImGui::SetCursorPos( ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); - if (ImGui::ImageButton("##Layers Toggle", resources.icons[layersIcon].id, icon_size_get())) showLayersOnly = !showLayersOnly; ImGui::SetItemTooltip(showLayersOnly ? "Only layers are visible. Press to show all items." : "All items are visible. Press to only show layers."); - ImGui::PopStyleVar(); ImGui::PopStyleColor(3); @@ -238,6 +306,8 @@ namespace anm2ed::imgui ImGui::PopStyleColor(); ImGui::PopStyleVar(2); index++; + + ImGui::PopID(); }; auto items_child = [&]() @@ -313,15 +383,19 @@ namespace anm2ed::imgui ImGui::BeginDisabled(!animation); { - shortcut(settings.shortcutAdd); - if (ImGui::Button("Add", widgetSize)) propertiesPopup.open(); + shortcut(manager.chords[SHORTCUT_ADD]); + if (ImGui::Button("Add", widgetSize)) + { + item_properties_reset(); + propertiesPopup.open(); + } set_item_tooltip_shortcut("Add a new item to the animation.", settings.shortcutAdd); ImGui::SameLine(); ImGui::BeginDisabled(!document.item_get() && reference.itemType != anm2::LAYER && reference.itemType != anm2::NULL_); { - shortcut(settings.shortcutRemove); + shortcut(manager.chords[SHORTCUT_REMOVE]); if (ImGui::Button("Remove", widgetSize)) { auto remove = [&]() @@ -345,9 +419,6 @@ namespace anm2ed::imgui auto frame_child = [&](anm2::Type type, int id, int& index, float width) { - auto& anm2 = document.anm2; - auto& hoveredFrame = document.hoveredFrame; - auto item = animation ? animation->item_get(type, id) : nullptr; auto isVisible = item ? item->isVisible : false; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; @@ -358,7 +429,7 @@ namespace anm2ed::imgui auto colorHovered = to_imvec4(anm2::TYPE_COLOR_HOVERED[type]); auto colorHidden = to_imvec4(to_vec4(color) * COLOR_HIDDEN_MULTIPLIER); auto colorActiveHidden = to_imvec4(to_vec4(colorActive) * COLOR_HIDDEN_MULTIPLIER); - auto colorHoveredHidden = to_imvec4(to_vec4(colorHidden) * COLOR_HIDDEN_MULTIPLIER); + auto colorHoveredHidden = to_imvec4(to_vec4(colorHovered) * COLOR_HIDDEN_MULTIPLIER); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); @@ -373,6 +444,8 @@ namespace anm2ed::imgui if (ImGui::BeginChild("##Frames Child", childSize, ImGuiChildFlags_Borders)) { + auto drawList = ImGui::GetWindowDrawList(); + auto clipMax = drawList->GetClipRectMax(); auto length = animation ? animation->frameNum : anm2.animations.length(); auto frameSize = ImVec2(ImGui::GetTextLineHeight(), ImGui::GetContentRegionAvail().y); auto framesSize = ImVec2(frameSize.x * length, frameSize.y); @@ -380,11 +453,8 @@ namespace anm2ed::imgui auto cursorScreenPos = ImGui::GetCursorScreenPos(); auto border = ImGui::GetStyle().FrameBorderSize; auto borderLineLength = frameSize.y / 5; - auto scrollX = ImGui::GetScrollX(); - auto available = ImGui::GetContentRegionAvail(); - auto frameMin = std::max(0, (int)std::floor(scrollX / frameSize.x) - 1); - auto frameMax = std::min(anm2::FRAME_NUM_MAX, (int)std::ceil(scrollX + available.x / frameSize.x) + 1); - auto drawList = ImGui::GetWindowDrawList(); + auto frameMin = std::max(0, (int)std::floor(scroll.x / frameSize.x) - 1); + auto frameMax = std::min(anm2::FRAME_NUM_MAX, (int)std::ceil((scroll.x + clipMax.x) / frameSize.x) + 1); pickerLineDrawList = drawList; if (type == anm2::NONE) @@ -422,16 +492,18 @@ namespace anm2ed::imgui if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseDown(0)) isDragging = true; + auto childPos = ImGui::GetWindowPos(); + auto mousePos = ImGui::GetIO().MousePos; + auto localMousePos = ImVec2(mousePos.x - childPos.x, mousePos.y - childPos.y); + hoveredTime = floorf(localMousePos.x / frameSize.x); + if (isDragging) { - auto childPos = ImGui::GetWindowPos(); - auto mousePos = ImGui::GetIO().MousePos; - auto localMousePos = ImVec2(mousePos.x - childPos.x, mousePos.y - childPos.y); - playback.time = floorf(localMousePos.x / frameSize.x); - reference.frameTime = playback.time; + playback.time = hoveredTime; + document.frameTime = playback.time; } - playback.clamp(settings.playbackIsClampPlayhead ? length : anm2::FRAME_NUM_MAX); + playback.clamp(settings.playbackIsClamp ? length : anm2::FRAME_NUM_MAX); if (ImGui::IsMouseReleased(0)) isDragging = false; @@ -440,47 +512,220 @@ namespace anm2ed::imgui } else if (animation) { - anm2::Reference itemReference{reference.animationIndex, type, id}; - - ImGui::PushStyleColor(ImGuiCol_ButtonActive, isVisible ? colorActive : colorActiveHidden); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, isVisible ? colorHovered : colorHoveredHidden); - - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); - float frameTime{}; + if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && + ImGui::IsMouseReleased(ImGuiMouseButton_Left)) + { + frames.clear(); + reference = {reference.animationIndex, type, id}; + } + + for (int i = frameMin; i < frameMax; i++) + { + auto frameScreenPos = ImVec2(cursorScreenPos.x + frameSize.x * (float)i, cursorScreenPos.y); + auto frameRectMax = ImVec2(frameScreenPos.x + frameSize.x, frameScreenPos.y + frameSize.y); + + drawList->AddRect(frameScreenPos, frameRectMax, ImGui::GetColorU32(FRAME_BORDER_COLOR)); + + if (i % FRAME_MULTIPLE == 0) + drawList->AddRectFilled(frameScreenPos, frameRectMax, ImGui::GetColorU32(FRAME_MULTIPLE_OVERLAY_COLOR)); + } + + frames.selection.start(item->frames.size(), ImGuiMultiSelectFlags_ClearOnEscape); + for (auto [i, frame] : std::views::enumerate(item->frames)) { ImGui::PushID(i); - auto frameReference = - anm2::Reference(itemReference.animationIndex, itemReference.itemType, itemReference.itemID, i); - auto isSelected = reference == frameReference; - vec2 frameMin = {frameTime * frameSize.x, cursorPos.y}; - vec2 frameMax = {frameMin.x + frame.delay * frameSize.x, frameMin.y + frameSize.y}; - auto buttonSize = to_imvec2(frameMax - frameMin); - auto buttonPos = ImVec2(cursorPos.x + frameMin.x, cursorPos.y); + auto frameReference = anm2::Reference{reference.animationIndex, type, id, (int)i}; + auto isFrameVisible = isVisible && frame.isVisible; + auto isReferenced = reference == frameReference; + auto isSelected = + (frames.selection.contains(i) && reference.itemType == type && reference.itemID == id) || isReferenced; + + if (type == anm2::TRIGGER) frameTime = frame.atFrame; + + auto buttonSize = + type == anm2::TRIGGER ? frameSize : to_imvec2(vec2(frameSize.x * frame.duration, frameSize.y)); + auto buttonPos = ImVec2(cursorPos.x + (frameTime * frameSize.x), cursorPos.y); + ImGui::SetCursorPos(buttonPos); - ImGui::PushStyleColor(ImGuiCol_Button, isSelected && isVisible ? colorActive - : isSelected && !isVisible ? colorActiveHidden - : isVisible ? color - : colorHidden); - if (ImGui::Button("##Frame Button", buttonSize)) + ImGui::PushStyleColor(ImGuiCol_Header, isFrameVisible && isSelected ? colorActive + : isSelected ? colorActiveHidden + : isFrameVisible ? color + : colorHidden); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, isFrameVisible ? colorActive : colorActiveHidden); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, isFrameVisible ? colorHovered : colorHoveredHidden); + + ImGui::SetNextItemAllowOverlap(); + ImGui::SetNextItemSelectionUserData((int)i); + if (ImGui::Selectable("##Frame Button", true, ImGuiSelectableFlags_None, buttonSize)) { if (type == anm2::LAYER) { document.spritesheet.reference = anm2.content.layers[id].spritesheetID; document.layer.selection = {id}; } - reference = frameReference; - reference.frameTime = frameTime; + else if (type == anm2::NULL_) + document.null.selection = {id}; - if (ImGui::IsKeyDown(ImGuiMod_Alt)) - DOCUMENT_EDIT(document, "Frame Interpolation", Document::FRAMES, - frame.isInterpolated = !frame.isInterpolated); + if (type != anm2::TRIGGER) + { + if (ImGui::IsKeyDown(ImGuiMod_Alt)) + DOCUMENT_EDIT(document, "Frame Interpolation", Document::FRAMES, + frame.isInterpolated = !frame.isInterpolated); + + document.frameTime = frameTime; + } + + reference = frameReference; } - if (ImGui::IsItemHovered()) hoveredFrame = frameReference; - ImGui::PopStyleColor(); + if (type == anm2::TRIGGER && ImGui::IsItemHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left) && + !draggedTrigger) + { + draggedTrigger = &frame; + draggedTriggerIndex = (int)i; + draggedTriggerAtFrameStart = hoveredTime; + } + + ImGui::PopStyleColor(3); + + if (type != anm2::TRIGGER) + { + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + frameDragDrop = {}; + frameDragDrop.type = type; + frameDragDrop.itemID = id; + frameDragDrop.animationIndex = reference.animationIndex; + + auto append_valid_indices = [&](const auto& container) + { + for (auto idx : container) + if (idx >= 0 && idx < (int)item->frames.size()) frameDragDrop.selection.push_back(idx); + }; + + if (isReferenced) append_valid_indices(frames.selection); + + auto contains_index = [&](const std::vector& container, int index) + { return std::find(container.begin(), container.end(), index) != container.end(); }; + + if ((!contains_index(frameDragDrop.selection, (int)i) || frameDragDrop.selection.size() <= 1) && + frameSelectionSnapshotReference.animationIndex == reference.animationIndex && + frameSelectionSnapshotReference.itemType == type && frameSelectionSnapshotReference.itemID == id && + contains_index(frameSelectionSnapshot, (int)i)) + { + frameDragDrop.selection = frameSelectionSnapshot; + frames.selection.clear(); + for (int idx : frameSelectionSnapshot) + if (idx >= 0 && idx < (int)item->frames.size()) frames.selection.insert(idx); + frameSelectionLocked = frameDragDrop.selection; + isFrameSelectionLocked = true; + } + + if (frameDragDrop.selection.empty()) + { + frameDragDrop.selection.push_back((int)i); + } + + std::sort(frameDragDrop.selection.begin(), frameDragDrop.selection.end()); + frameDragDrop.selection.erase( + std::unique(frameDragDrop.selection.begin(), frameDragDrop.selection.end()), + frameDragDrop.selection.end()); + + ImGui::SetDragDropPayload(FRAME_DRAG_PAYLOAD_ID, &frameDragDrop, sizeof(FrameDragDrop)); + ImGui::EndDragDropSource(); + } + + if (ImGui::BeginDragDropTarget()) + { + if (auto payload = ImGui::AcceptDragDropPayload(FRAME_DRAG_PAYLOAD_ID)) + { + auto source = static_cast(payload->Data); + auto sameAnimation = source && source->animationIndex == reference.animationIndex; + auto sourceItem = + sameAnimation && animation ? animation->item_get(source->type, source->itemID) : nullptr; + auto targetItem = animation ? animation->item_get(type, id) : nullptr; + + auto time_from_index = [&](anm2::Item* target, int index) + { + if (!target || target->frames.empty()) return 0.0f; + index = std::clamp(index, 0, (int)target->frames.size()); + float timeAccum = 0.0f; + for (int n = 0; n < index && n < (int)target->frames.size(); ++n) + timeAccum += target->frames[n].duration; + return timeAccum; + }; + + if (source && sourceItem && targetItem && source->type != anm2::TRIGGER && type != anm2::TRIGGER) + { + std::vector indices = source->selection; + if (indices.empty()) indices.push_back((int)i); + std::sort(indices.begin(), indices.end()); + indices.erase(std::unique(indices.begin(), indices.end()), indices.end()); + + int insertPosResult = -1; + int insertedCount = 0; + DOCUMENT_EDIT(document, "Move Frame(s)", Document::FRAMES, { + std::vector movedFrames; + movedFrames.reserve(indices.size()); + + for (int i : indices) + if (i >= 0 && i < (int)sourceItem->frames.size()) + movedFrames.push_back(std::move(sourceItem->frames[i])); + + for (auto it = indices.rbegin(); it != indices.rend(); ++it) + if (*it >= 0 && *it < (int)sourceItem->frames.size()) + sourceItem->frames.erase(sourceItem->frames.begin() + *it); + + int desired = std::clamp((int)i + 1, 0, (int)targetItem->frames.size()); + if (sourceItem == targetItem) + { + int removedBefore = 0; + for (int i : indices) + if (i < desired) ++removedBefore; + desired -= removedBefore; + } + desired = std::clamp(desired, 0, (int)targetItem->frames.size()); + + insertPosResult = desired; + insertedCount = (int)movedFrames.size(); + targetItem->frames.insert(targetItem->frames.begin() + insertPosResult, + std::make_move_iterator(movedFrames.begin()), + std::make_move_iterator(movedFrames.end())); + }); + + if (insertedCount > 0) + { + frames.selection.clear(); + for (int offset = 0; offset < insertedCount; ++offset) + frames.selection.insert(insertPosResult + offset); + + reference = {reference.animationIndex, type, id, insertPosResult}; + document.frameTime = time_from_index(targetItem, reference.frameIndex); + if (type == anm2::LAYER) + { + document.spritesheet.reference = anm2.content.layers[id].spritesheetID; + document.layer.selection = {id}; + } + else if (type == anm2::NULL_) + document.null.selection = {id}; + } + } + } + + ImGui::EndDragDropTarget(); + } + } + + auto rectMin = ImGui::GetItemRectMin(); + auto rectMax = ImGui::GetItemRectMax(); + auto borderColor = isReferenced ? FRAME_BORDER_COLOR_REFERENCED : FRAME_BORDER_COLOR; + auto borderThickness = isReferenced ? FRAME_BORDER_THICKNESS_REFERENCED : FRAME_BORDER_THICKNESS; + drawList->AddRect(rectMin, rectMax, ImGui::GetColorU32(borderColor), ImGui::GetStyle().FrameRounding, 0, + borderThickness); + auto icon = type == anm2::TRIGGER ? icon::TRIGGER : frame.isInterpolated ? icon::INTERPOLATED : icon::UNINTERPOLATED; @@ -489,15 +734,59 @@ namespace anm2ed::imgui ImGui::SetCursorPos(iconPos); ImGui::Image(resources.icons[icon].id, icon_size_get()); - frameTime += frame.delay; + if (type != anm2::TRIGGER) frameTime += frame.duration; ImGui::PopID(); } - ImGui::PopStyleVar(); - ImGui::PopStyleColor(2); + frames.selection.finish(); + if (isFrameSelectionLocked) + { + frames.selection.clear(); + for (int idx : frameSelectionLocked) + frames.selection.insert(idx); + isFrameSelectionLocked = false; + frameSelectionLocked.clear(); + } + if (reference.itemType == type && reference.itemID == id) + { + frameSelectionSnapshot.assign(frames.selection.begin(), frames.selection.end()); + frameSelectionSnapshotReference = reference; + } + + if (draggedTrigger) + { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + + if (!isDraggedTriggerSnapshot && hoveredTime != draggedTriggerAtFrameStart) + { + isDraggedTriggerSnapshot = true; + document.snapshot("Trigger At Frame"); + } + + draggedTrigger->atFrame = glm::clamp( + hoveredTime, 0, settings.playbackIsClamp ? animation->frameNum - 1 : anm2::FRAME_NUM_MAX - 1); + + for (auto&& [i, trigger] : std::views::enumerate(animation->triggers.frames)) + { + if (i == draggedTriggerIndex) continue; + if (trigger.atFrame == draggedTrigger->atFrame) draggedTrigger->atFrame--; + } + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) + { + document.change(Document::FRAMES); + draggedTrigger = nullptr; + draggedTriggerIndex = -1; + draggedTriggerAtFrameStart = -1; + isDraggedTriggerSnapshot = false; + item->frames_sort_by_at_frame(); + } + } } } + + context_menu(); + ImGui::EndChild(); ImGui::PopStyleVar(); @@ -507,8 +796,6 @@ namespace anm2ed::imgui auto frames_child = [&]() { - auto& anm2 = document.anm2; - auto itemsChildWidth = ImGui::GetTextLineHeightWithSpacing() * 15; auto cursorPos = ImGui::GetCursorPos(); @@ -577,16 +864,16 @@ namespace anm2ed::imgui for (auto& id : animation->layerOrder) { - if (auto itemPtr = animation->item_get(anm2::LAYER, id); itemPtr) - if (!settings.timelineIsShowUnused && itemPtr->frames.empty()) continue; + if (auto item = animation->item_get(anm2::LAYER, id); item) + if (!settings.timelineIsShowUnused && item->frames.empty()) continue; frames_child_row(anm2::LAYER, id); } for (auto& id : animation->nullAnimations | std::views::keys) { - if (auto itemPtr = animation->item_get(anm2::NULL_, id); itemPtr) - if (!settings.timelineIsShowUnused && itemPtr->frames.empty()) continue; + if (auto item = animation->item_get(anm2::NULL_, id); item) + if (!settings.timelineIsShowUnused && item->frames.empty()) continue; frames_child_row(anm2::NULL_, id); } @@ -615,8 +902,6 @@ namespace anm2ed::imgui pickerLineDrawList->PopClipRect(); ImGui::PopStyleVar(); - - context_menu(); } ImGui::EndChild(); ImGui::PopStyleVar(); @@ -633,33 +918,55 @@ namespace anm2ed::imgui auto label = playback.isPlaying ? "Pause" : "Play"; auto tooltip = playback.isPlaying ? "Pause the animation." : "Play the animation."; - shortcut(settings.shortcutPlayPause); + shortcut(manager.chords[SHORTCUT_PLAY_PAUSE]); if (ImGui::Button(label, widgetSize)) playback.toggle(); set_item_tooltip_shortcut(tooltip, settings.shortcutPlayPause); ImGui::SameLine(); - auto itemPtr = document.item_get(); + auto item = animation->item_get(reference.itemType, reference.itemID); - ImGui::BeginDisabled(!itemPtr); + ImGui::BeginDisabled(!item); { - shortcut(settings.shortcutAdd); - if (ImGui::Button("Insert Frame", widgetSize)) + shortcut(manager.chords[SHORTCUT_ADD]); + if (ImGui::Button("Insert", widgetSize)) { auto insert_frame = [&]() { - auto frame = document.frame_get(); - if (frame) + if (reference.itemType == anm2::TRIGGER) { - itemPtr->frames.insert(itemPtr->frames.begin() + reference.frameIndex, *frame); - reference.frameIndex++; + for (auto& trigger : animation->triggers.frames) + if (document.frameTime == trigger.atFrame) return; + + auto addFrame = anm2::Frame(); + addFrame.atFrame = document.frameTime; + item->frames.push_back(addFrame); + item->frames_sort_by_at_frame(); + reference.frameIndex = item->frame_index_from_at_frame_get(addFrame.atFrame); } - else if (!itemPtr->frames.empty()) + else { - auto lastFrame = itemPtr->frames.back(); - itemPtr->frames.emplace_back(lastFrame); - reference.frameIndex = static_cast(itemPtr->frames.size()) - 1; + auto frame = document.frame_get(); + if (frame) + { + auto addFrame = *frame; + item->frames.insert(item->frames.begin() + reference.frameIndex, addFrame); + reference.frameIndex++; + } + else if (!item->frames.empty()) + { + auto addFrame = item->frames.back(); + item->frames.emplace_back(addFrame); + reference.frameIndex = (int)(item->frames.size()) - 1; + } + else + { + item->frames.emplace_back(anm2::Frame()); + reference.frameIndex = 0; + } } + + frames.selection = {reference.frameIndex}; }; DOCUMENT_EDIT(document, "Insert Frame", Document::FRAMES, insert_frame()); @@ -670,17 +977,9 @@ namespace anm2ed::imgui ImGui::BeginDisabled(!document.frame_get()); { - shortcut(settings.shortcutRemove); - if (ImGui::Button("Delete Frame", widgetSize)) - { - auto delete_frame = [&]() - { - itemPtr->frames.erase(itemPtr->frames.begin() + reference.frameIndex); - reference.frameIndex = glm::max(-1, --reference.frameIndex); - }; - - DOCUMENT_EDIT(document, "Delete Frame(s)", Document::FRAMES, delete_frame()); - } + shortcut(manager.chords[SHORTCUT_REMOVE]); + if (ImGui::Button("Delete", widgetSize)) + DOCUMENT_EDIT(document, "Delete Frame(s)", Document::FRAMES, frames_delete()); set_item_tooltip_shortcut("Delete the selected frames.", settings.shortcutRemove); ImGui::SameLine(); @@ -694,36 +993,44 @@ namespace anm2ed::imgui ImGui::SameLine(); - if (ImGui::Button("Fit Animation Length", widgetSize)) animation->frameNum = animation->length(); + if (ImGui::Button("Fit Animation Length", widgetSize)) + DOCUMENT_EDIT(document, "Fit Animation Length", Document::ANIMATIONS, + animation->frameNum = animation->length()); ImGui::SetItemTooltip("The animation length will be set to the effective length of the animation."); ImGui::SameLine(); + auto frameNum = animation ? animation->frameNum : dummy_value(); ImGui::SetNextItemWidth(widgetSize.x); - ImGui::InputInt("Animation Length", animation ? &animation->frameNum : &dummy_value(), STEP, STEP_FAST, - !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0); - if (animation) animation->frameNum = clamp(animation->frameNum, anm2::FRAME_NUM_MIN, anm2::FRAME_NUM_MAX); + if (input_int_range("Animation Length", frameNum, anm2::FRAME_NUM_MIN, anm2::FRAME_NUM_MAX, STEP, STEP_FAST, + !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0)) + DOCUMENT_EDIT(document, "Animation Length", Document::ANIMATIONS, animation->frameNum = frameNum); ImGui::SetItemTooltip("Set the animation's length."); ImGui::SameLine(); + auto isLoop = animation ? animation->isLoop : dummy_value(); ImGui::SetNextItemWidth(widgetSize.x); - ImGui::Checkbox("Loop", animation ? &animation->isLoop : &dummy_value()); + if (ImGui::Checkbox("Loop", &isLoop)) + DOCUMENT_EDIT(document, "Loop", Document::ANIMATIONS, animation->isLoop = isLoop); ImGui::SetItemTooltip("Toggle the animation looping."); } ImGui::EndDisabled(); ImGui::SameLine(); + auto fps = anm2.info.fps; ImGui::SetNextItemWidth(widgetSize.x); - ImGui::InputInt("FPS", &anm2.info.fps, 1, 5); - anm2.info.fps = clamp(anm2.info.fps, anm2::FPS_MIN, anm2::FPS_MAX); + if (input_int_range("FPS", fps, anm2::FPS_MIN, anm2::FPS_MAX)) + DOCUMENT_EDIT(document, "FPS", Document::ANIMATIONS, anm2.info.fps = fps); ImGui::SetItemTooltip("Set the FPS of all animations."); ImGui::SameLine(); + auto createdBy = anm2.info.createdBy; ImGui::SetNextItemWidth(widgetSize.x); - input_text_string("Author", &anm2.info.createdBy); + if (input_text_string("Author", &createdBy)) + DOCUMENT_EDIT(document, "FPS", Document::ANIMATIONS, anm2.info.createdBy = createdBy); ImGui::SetItemTooltip("Set the author of the document."); ImGui::SameLine(); @@ -739,226 +1046,6 @@ namespace anm2ed::imgui ImGui::SetCursorPos(cursorPos); }; - auto popups_fn = [&]() - { - auto item_properties_reset = [&]() - { - addItemName.clear(); - addItemSpritesheetID = {}; - addItemID = -1; - isUnusedItemsSet = false; - }; - - auto& anm2 = document.anm2; - - 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 = footer_size_get(); - auto optionsSize = 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); - { - input_text_string("Name", &addItemName); - ImGui::SetItemTooltip("Set the item's name."); - ImGui::BeginDisabled(type != anm2::LAYER); - { - combo_negative_one_indexed("Spritesheet", &addItemSpritesheetID, document.spritesheet.labels); - 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 = widget_size_with_row_get(2); - - if (ImGui::Button("Add", widgetSize)) - { - anm2::Reference addReference{}; - - document.snapshot("Add Item"); - if (type == anm2::LAYER) - addReference = anm2.layer_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, - addItemSpritesheetID - 1, (locale::Type)locale); - else if (type == anm2::NULL_) - addReference = anm2.null_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, - (locale::Type)locale); - - document.change(Document::ITEMS); - - 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(); - } - - bakePopup.trigger(); - - if (ImGui::BeginPopupModal(bakePopup.label, &bakePopup.isOpen, ImGuiWindowFlags_NoResize)) - { - auto& interval = settings.bakeInterval; - auto& isRoundRotation = settings.bakeIsRoundRotation; - auto& isRoundScale = settings.bakeIsRoundScale; - - auto frame = document.frame_get(); - - input_int_range("Interval", interval, anm2::FRAME_DELAY_MIN, frame ? frame->delay : anm2::FRAME_DELAY_MIN); - ImGui::SetItemTooltip("Set the maximum delay of each frame that will be baked."); - - ImGui::Checkbox("Round Rotation", &isRoundRotation); - ImGui::SetItemTooltip("Rotation will be rounded to the nearest whole number."); - - ImGui::Checkbox("Round Scale", &isRoundScale); - ImGui::SetItemTooltip("Scale will be rounded to the nearest whole number."); - - auto widgetSize = widget_size_with_row_get(2); - - if (ImGui::Button("Bake", widgetSize)) - { - if (auto itemPtr = document.item_get()) - DOCUMENT_EDIT(document, "Bake Frames", Document::FRAMES, - itemPtr->frames_bake(reference.frameIndex, interval, isRoundScale, isRoundRotation)); - bakePopup.close(); - } - ImGui::SetItemTooltip("Bake the selected frame(s) with the options selected."); - - ImGui::SameLine(); - - if (ImGui::Button("Cancel", widgetSize)) bakePopup.close(); - ImGui::SetItemTooltip("Cancel baking frames."); - - ImGui::EndPopup(); - } - }; - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); if (ImGui::Begin("Timeline", &settings.windowIsTimeline)) { @@ -969,27 +1056,223 @@ namespace anm2ed::imgui ImGui::PopStyleVar(); ImGui::End(); - popups_fn(); + propertiesPopup.trigger(); - if (shortcut(settings.shortcutPlayPause, shortcut::GLOBAL)) playback.toggle(); + 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; + + auto footerSize = footer_size_get(); + auto optionsSize = 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); + { + input_text_string("Name", &addItemName); + ImGui::SetItemTooltip("Set the item's name."); + ImGui::BeginDisabled(type != anm2::LAYER); + { + combo_negative_one_indexed("Spritesheet", &addItemSpritesheetID, document.spritesheet.labels); + 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 = widget_size_with_row_get(2); + + if (ImGui::Button("Add", widgetSize)) + { + anm2::Reference addReference{}; + + document.snapshot("Add Item"); + if (type == anm2::LAYER) + addReference = anm2.layer_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, + addItemSpritesheetID - 1, (locale::Type)locale); + else if (type == anm2::NULL_) + addReference = anm2.null_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, + (locale::Type)locale); + + document.change(Document::ITEMS); + + 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(); + } + + bakePopup.trigger(); + + if (ImGui::BeginPopupModal(bakePopup.label, &bakePopup.isOpen, ImGuiWindowFlags_NoResize)) + { + auto& interval = settings.bakeInterval; + auto& isRoundRotation = settings.bakeIsRoundRotation; + auto& isRoundScale = settings.bakeIsRoundScale; + + auto frame = document.frame_get(); + + input_int_range("Interval", interval, anm2::FRAME_DURATION_MIN, + frame ? frame->duration : anm2::FRAME_DURATION_MIN); + ImGui::SetItemTooltip("Set the maximum duration of each frame that will be baked."); + + ImGui::Checkbox("Round Rotation", &isRoundRotation); + ImGui::SetItemTooltip("Rotation will be rounded to the nearest whole number."); + + ImGui::Checkbox("Round Scale", &isRoundScale); + ImGui::SetItemTooltip("Scale will be rounded to the nearest whole number."); + + auto widgetSize = widget_size_with_row_get(2); + + if (ImGui::Button("Bake", widgetSize)) + { + if (auto itemPtr = document.item_get()) + DOCUMENT_EDIT(document, "Bake Frames", Document::FRAMES, + itemPtr->frames_bake(reference.frameIndex, interval, isRoundScale, isRoundRotation)); + bakePopup.close(); + } + ImGui::SetItemTooltip("Bake the selected frame(s) with the options selected."); + + ImGui::SameLine(); + + if (ImGui::Button("Cancel", widgetSize)) bakePopup.close(); + ImGui::SetItemTooltip("Cancel baking frames."); + + ImGui::EndPopup(); + } + + if (shortcut(manager.chords[SHORTCUT_PLAY_PAUSE], shortcut::GLOBAL)) playback.toggle(); if (animation) { - if (chord_repeating(string_to_chord(settings.shortcutPreviousFrame))) + if (chord_repeating(manager.chords[SHORTCUT_PREVIOUS_FRAME])) { - playback.decrement(settings.playbackIsClampPlayhead ? animation->frameNum : anm2::FRAME_NUM_MAX); - reference.frameTime = playback.time; + playback.decrement(settings.playbackIsClamp ? animation->frameNum : anm2::FRAME_NUM_MAX); + document.frameTime = playback.time; } - if (chord_repeating(string_to_chord(settings.shortcutNextFrame))) + if (chord_repeating(manager.chords[SHORTCUT_NEXT_FRAME])) { - playback.increment(settings.playbackIsClampPlayhead ? animation->frameNum : anm2::FRAME_NUM_MAX); - reference.frameTime = playback.time; + playback.increment(settings.playbackIsClamp ? animation->frameNum : anm2::FRAME_NUM_MAX); + document.frameTime = playback.time; } } - if (ImGui::IsKeyChordPressed(string_to_chord(settings.shortcutShortenFrame))) document.snapshot("Shorten Frame"); - if (chord_repeating(string_to_chord(settings.shortcutShortenFrame))) + if (ImGui::IsKeyChordPressed(manager.chords[SHORTCUT_SHORTEN_FRAME])) document.snapshot("Shorten Frame"); + if (chord_repeating(manager.chords[SHORTCUT_SHORTEN_FRAME])) { if (auto frame = document.frame_get()) { @@ -998,8 +1281,8 @@ namespace anm2ed::imgui } } - if (ImGui::IsKeyChordPressed(string_to_chord(settings.shortcutExtendFrame))) document.snapshot("Extend Frame"); - if (chord_repeating(string_to_chord(settings.shortcutExtendFrame))) + if (ImGui::IsKeyChordPressed(manager.chords[SHORTCUT_EXTEND_FRAME])) document.snapshot("Extend Frame"); + if (chord_repeating(manager.chords[SHORTCUT_EXTEND_FRAME])) { if (auto frame = document.frame_get()) { diff --git a/src/imgui/window/timeline.h b/src/imgui/window/timeline.h index e1b41cc..474e3da 100644 --- a/src/imgui/window/timeline.h +++ b/src/imgui/window/timeline.h @@ -1,5 +1,8 @@ #pragma once +#include +#include + #include "clipboard.h" #include "manager.h" #include "resources.h" @@ -7,6 +10,14 @@ namespace anm2ed::imgui { + struct FrameDragDrop + { + anm2::Type type{anm2::NONE}; + int itemID{-1}; + int animationIndex{-1}; + std::vector selection{}; + }; + class Timeline { bool isDragging{}; @@ -18,7 +29,16 @@ namespace anm2ed::imgui bool addItemIsRect{}; int addItemID{-1}; int addItemSpritesheetID{-1}; - bool isUnusedItemsSet{}; + int hoveredTime{}; + anm2::Frame* draggedTrigger{}; + int draggedTriggerIndex{-1}; + int draggedTriggerAtFrameStart{-1}; + bool isDraggedTriggerSnapshot{}; + FrameDragDrop frameDragDrop{}; + std::vector frameSelectionSnapshot{}; + std::vector frameSelectionLocked{}; + bool isFrameSelectionLocked{}; + anm2::Reference frameSelectionSnapshotReference{}; std::set unusedItems{}; glm::vec2 scroll{}; ImDrawList* pickerLineDrawList{}; diff --git a/src/imgui/window/tools.cpp b/src/imgui/window/tools.cpp index 608806d..a9f9603 100644 --- a/src/imgui/window/tools.cpp +++ b/src/imgui/window/tools.cpp @@ -50,9 +50,7 @@ namespace anm2ed::imgui if (isSelected) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); - auto member = SHORTCUT_MEMBERS[info.shortcut]; - - if (shortcut(settings.*member, shortcut::GLOBAL_SET)) tool_use((tool::Type)i); + if (shortcut(manager.chords[info.shortcut], shortcut::GLOBAL_SET)) tool_use((tool::Type)i); if (i == tool::COLOR) { diff --git a/src/loader.cpp b/src/loader.cpp index f5ae413..0284249 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -61,10 +61,7 @@ namespace anm2ed } } - std::string Loader::settings_path() - { - return filesystem::path_preferences_get() + "settings.ini"; - } + std::string Loader::settings_path() { return filesystem::path_preferences_get() + "settings.ini"; } Loader::Loader(int argc, const char** argv) { diff --git a/src/main.cpp b/src/main.cpp index ea7d4da..25b49c6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,7 +7,7 @@ int main(int argc, const char** argv) if (loader.isError) return EXIT_FAILURE; - anm2ed::State state(loader.window, loader.arguments); + anm2ed::State state(loader.window, loader.settings, loader.arguments); while (!state.isQuit) state.loop(loader.window, loader.settings); diff --git a/src/manager.cpp b/src/manager.cpp index 016730e..27e6e53 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -281,5 +281,11 @@ namespace anm2ed autosave_files_write(); } + void Manager::chords_set(Settings& settings) + { + for (int i = 0; i < SHORTCUT_COUNT; i++) + chords[i] = imgui::string_to_chord(settings.*SHORTCUT_MEMBERS[i]); + } + Manager::~Manager() { autosave_files_clear(); } } diff --git a/src/manager.h b/src/manager.h index 485b309..74e623d 100644 --- a/src/manager.h +++ b/src/manager.h @@ -3,6 +3,7 @@ #include #include "document.h" +#include "settings.h" namespace anm2ed { @@ -27,6 +28,8 @@ namespace anm2ed int recordingEnd{}; bool isRecordingRange{}; + ImGuiKeyChord chords[SHORTCUT_COUNT]{}; + anm2::Layer editLayer{}; imgui::PopupHelper layerPropertiesPopup{imgui::PopupHelper("Layer Properties", imgui::POPUP_SMALL_NO_HEIGHT)}; @@ -64,6 +67,8 @@ namespace anm2ed void autosave_files_write(); void autosave_files_clear(); + void chords_set(Settings&); + std::filesystem::path autosave_directory_get(); }; } diff --git a/src/playback.cpp b/src/playback.cpp index 03c2117..cb6ce18 100644 --- a/src/playback.cpp +++ b/src/playback.cpp @@ -11,10 +11,7 @@ namespace anm2ed isPlaying = !isPlaying; } - void Playback::clamp(int length) - { - time = glm::clamp(time, 0.0f, (float)length - 1.0f); - } + void Playback::clamp(int length) { time = glm::clamp(time, 0.0f, (float)length - 1.0f); } void Playback::tick(int fps, int length, bool isLoop) { diff --git a/src/resource/audio.cpp b/src/resource/audio.cpp index af04685..e871d79 100644 --- a/src/resource/audio.cpp +++ b/src/resource/audio.cpp @@ -1,12 +1,26 @@ #include "audio.h" +#include +#include +#include +#include #include +#include +#include namespace anm2ed::resource { + namespace + { + constexpr int XM_SAMPLE_RATE = 44100; + constexpr uint16_t XM_CHUNK_FRAMES = 1024; + constexpr int XM_MAX_SECONDS = 600; + constexpr int XM_CHANNELS = 2; + } + MIX_Mixer* Audio::mixer_get() { - static MIX_Mixer* mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr); + static auto mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr); return mixer; } @@ -15,21 +29,130 @@ namespace anm2ed::resource if (path && *path) internal = MIX_LoadAudio(mixer_get(), path, true); } - void Audio::unload() + Audio::Audio(const unsigned char* data, size_t size) { - if (!internal) return; - MIX_DestroyAudio(internal); - internal = nullptr; + if (!data || size == 0) return; + + auto is_chunk_silent = [](const float* samples, size_t count) + { + constexpr float epsilon = 1e-6f; + for (size_t i = 0; i < count; ++i) + if (std::fabs(samples[i]) > epsilon) return false; + return true; + }; + + auto prescanStorage = std::make_unique(XM_PRESCAN_DATA_SIZE); + auto prescan = (xm_prescan_data_t*)prescanStorage.get(); + if (!xm_prescan_module((const char*)data, (uint32_t)size, prescan)) return; + + auto contextSize = xm_size_for_context(prescan); + auto pool = std::make_unique(contextSize); + auto context = xm_create_context(pool.get(), prescan, (const char*)data, (uint32_t)size); + if (!context) return; + + xm_set_sample_rate(context, (uint16_t)XM_SAMPLE_RATE); + xm_set_max_loop_count(context, 1); + + auto pcm = std::vector{}; + pcm.reserve(XM_CHUNK_FRAMES * XM_CHANNELS * 8); + + auto framesGenerated = (size_t)0; + const auto maxFrames = (size_t)XM_SAMPLE_RATE * XM_MAX_SECONDS; + auto heardAudio = false; + + while (framesGenerated < maxFrames) + { + auto framesThisPass = (uint16_t)std::min(XM_CHUNK_FRAMES, maxFrames - framesGenerated); + auto offset = pcm.size(); + pcm.resize(offset + framesThisPass * XM_CHANNELS); + auto* chunkStart = pcm.data() + offset; + xm_generate_samples(context, chunkStart, framesThisPass); + framesGenerated += framesThisPass; + + auto chunkSamples = (size_t)framesThisPass * XM_CHANNELS; + auto chunkSilent = is_chunk_silent(chunkStart, chunkSamples); + if (!chunkSilent) + { + heardAudio = true; + } + else if (heardAudio) + { + pcm.resize(offset); + break; + } + + if (xm_get_loop_count(context) > 0) break; + } + + if (pcm.empty()) return; + + auto spec = SDL_AudioSpec{}; + spec.freq = XM_SAMPLE_RATE; + spec.format = SDL_AUDIO_F32; + spec.channels = XM_CHANNELS; + + internal = MIX_LoadRawAudio(nullptr, pcm.data(), pcm.size() * sizeof(float), &spec); } - void Audio::play(MIX_Mixer* mixer) + void Audio::unload() { - MIX_PlayAudio(mixer ? mixer : mixer_get(), internal); + if (track) + { + MIX_DestroyTrack(track); + track = nullptr; + } + if (internal) + { + MIX_DestroyAudio(internal); + internal = nullptr; + } } + void Audio::play(bool loop, MIX_Mixer* mixer) + { + if (!internal) return; + auto targetMixer = mixer ? mixer : mixer_get(); + if (!targetMixer) return; + + if (track && MIX_GetTrackMixer(track) != targetMixer) + { + MIX_DestroyTrack(track); + track = nullptr; + } + + if (!track) + { + track = MIX_CreateTrack(targetMixer); + if (!track) return; + } + + MIX_SetTrackAudio(track, internal); + + SDL_PropertiesID options = 0; + if (loop) + { + options = SDL_CreateProperties(); + if (options) SDL_SetNumberProperty(options, MIX_PROP_PLAY_LOOPS_NUMBER, -1); + } + + MIX_PlayTrack(track, options); + + if (options) SDL_DestroyProperties(options); + } + + void Audio::stop(MIX_Mixer* mixer) + { + if (!track) return; + if (mixer && MIX_GetTrackMixer(track) != mixer) return; + MIX_StopTrack(track, 0); + } + + bool Audio::is_playing() const { return track && MIX_TrackPlaying(track); } + Audio::Audio(Audio&& other) noexcept { internal = std::exchange(other.internal, nullptr); + track = std::exchange(other.track, nullptr); } Audio& Audio::operator=(Audio&& other) noexcept @@ -38,17 +161,11 @@ namespace anm2ed::resource { unload(); internal = std::exchange(other.internal, nullptr); + track = std::exchange(other.track, nullptr); } return *this; } - Audio::~Audio() - { - unload(); - } - - bool Audio::is_valid() - { - return internal; - } + Audio::~Audio() { unload(); } + bool Audio::is_valid() { return internal; } } diff --git a/src/resource/audio.h b/src/resource/audio.h index 569ae8a..33de290 100644 --- a/src/resource/audio.h +++ b/src/resource/audio.h @@ -1,17 +1,20 @@ #pragma once #include +#include namespace anm2ed::resource { class Audio { MIX_Audio* internal{nullptr}; + MIX_Track* track{nullptr}; MIX_Mixer* mixer_get(); void unload(); public: Audio(const char*); + Audio(const unsigned char*, size_t); ~Audio(); Audio() = default; Audio(Audio&&) noexcept; @@ -20,6 +23,8 @@ namespace anm2ed::resource Audio& operator=(const Audio&) = delete; bool is_valid(); - void play(MIX_Mixer* = nullptr); + void play(bool loop = false, MIX_Mixer* = nullptr); + void stop(MIX_Mixer* = nullptr); + bool is_playing() const; }; } diff --git a/src/resource/icon.h b/src/resource/icon.h index 784faf3..824d506 100644 --- a/src/resource/icon.h +++ b/src/resource/icon.h @@ -212,18 +212,4 @@ namespace anm2ed::resource::icon SVG_LIST #undef X }; - - constexpr uint8_t PROGRAM[] = { - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x00, 0x40, 0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0x60, 0xb9, 0x55, 0x00, 0x00, 0x00, 0x09, 0x70, - 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, - 0x8b, 0x49, 0x44, 0x41, 0x54, 0x68, 0xde, 0xed, 0xd7, 0x3d, 0x0e, 0x80, 0x20, 0x0c, 0x86, 0x61, 0x7b, 0x53, 0x3c, - 0x99, 0xbd, 0x69, 0x1d, 0x8c, 0x83, 0x4d, 0xd0, 0x44, 0x29, 0x7f, 0xbe, 0x5d, 0x08, 0x30, 0xf4, 0x19, 0x80, 0x2f, - 0xc8, 0xd2, 0xb8, 0x04, 0xc0, 0xb4, 0x80, 0x64, 0xc7, 0xb8, 0xf9, 0x86, 0x02, 0x60, 0x36, 0x80, 0xd9, 0x75, 0xbe, - 0xba, 0x7d, 0x00, 0xf3, 0x00, 0x72, 0x8d, 0x7c, 0x83, 0x73, 0x5d, 0xa5, 0xf0, 0x43, 0x04, 0xa0, 0x1a, 0x20, 0xaa, - 0x11, 0x80, 0xf1, 0x01, 0x1a, 0x14, 0x5b, 0x00, 0xfa, 0x03, 0x24, 0xbb, 0x0f, 0x93, 0xa7, 0xb0, 0xf9, 0x1c, 0x46, - 0x00, 0x00, 0x00, 0xe8, 0x06, 0xa0, 0xa5, 0x3f, 0x2a, 0x99, 0x07, 0x0d, 0x40, 0xbf, 0x80, 0xa8, 0x02, 0x30, 0x0e, - 0x40, 0xdf, 0x1e, 0xb2, 0x56, 0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x07, 0x90, 0x86, 0xff, 0x05, 0xd4, - 0x2e, 0x00, 0x00, 0x00, 0x34, 0x07, 0xec, 0x94, 0x51, 0xac, 0x41, 0x55, 0x6e, 0xe4, 0x26, 0x00, 0x00, 0x00, 0x00, - 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82}; } \ No newline at end of file diff --git a/src/resources.cpp b/src/resources.cpp index d4bfa54..68329a2 100644 --- a/src/resources.cpp +++ b/src/resources.cpp @@ -1,5 +1,7 @@ #include "resources.h" +#include "xm_music.h" + #include using namespace anm2ed::resource; @@ -16,5 +18,7 @@ namespace anm2ed for (auto [i, shader] : std::views::enumerate(shader::SHADERS)) shaders[i] = Shader(shader.vertex, shader.fragment); + + music = Audio(xm::ABOUT, std::size(xm::ABOUT)); }; } \ No newline at end of file diff --git a/src/resources.h b/src/resources.h index d703b4a..c1c5f78 100644 --- a/src/resources.h +++ b/src/resources.h @@ -2,6 +2,7 @@ #include +#include "audio.h" #include "font.h" #include "icon.h" #include "shader.h" @@ -15,6 +16,7 @@ namespace anm2ed resource::Font fonts[resource::font::COUNT]{}; resource::Texture icons[resource::icon::COUNT]{}; resource::Shader shaders[resource::shader::COUNT]{}; + resource::Audio music{}; Resources(); }; diff --git a/src/settings.h b/src/settings.h index db1ac02..9831d82 100644 --- a/src/settings.h +++ b/src/settings.h @@ -4,7 +4,7 @@ #include -#include "anm2/anm2.h" +#include "anm2/anm2_type.h" #include "render.h" #include "types.h" @@ -54,7 +54,7 @@ namespace anm2ed X(VIEW_ZOOM_STEP, viewZoomStep, "Zoom Step", FLOAT, 50.0f) \ \ X(PLAYBACK_IS_LOOP, playbackIsLoop, "Loop", BOOL, true) \ - X(PLAYBACK_IS_CLAMP_PLAYHEAD, playbackIsClampPlayhead, "Clamp Playhead", BOOL, true) \ + X(PLAYBACK_IS_CLAMP, playbackIsClamp, "Clamp", BOOL, true) \ \ X(CHANGE_IS_CROP, changeIsCrop, "##Is Crop", BOOL, false) \ X(CHANGE_IS_SIZE, changeIsSize, "##Is Size", BOOL, false) \ @@ -186,9 +186,7 @@ namespace anm2ed X(SHORTCUT_DUPLICATE, shortcutDuplicate, "Duplicate", STRING, "Ctrl+J") \ X(SHORTCUT_DEFAULT, shortcutDefault, "Default", STRING, "Home") \ X(SHORTCUT_MERGE, shortcutMerge, "Merge", STRING, "Ctrl+E") \ - X(SHORTCUT_PASTE, shortcutPaste, "Paste", STRING, "Ctrl+V") \ - X(SHORTCUT_SELECT_ALL, shortcutSelectAll, "Select All", STRING, "Ctrl+A") \ - X(SHORTCUT_SELECT_NONE, shortcutSelectNone, "Select None", STRING, "Escape") + X(SHORTCUT_PASTE, shortcutPaste, "Paste", STRING, "Ctrl+V") #define SETTINGS_WINDOWS \ /* Symbol / Name / String / Type / Default */ \ @@ -205,6 +203,14 @@ namespace anm2ed X(WINDOW_TIMELINE, windowIsTimeline, "Timeline", BOOL, true) \ X(WINDOW_TOOLS, windowIsTools, "Tools", BOOL, true) + enum ShortcutType + { +#define X(symbol, name, string, type, ...) symbol, + SETTINGS_SHORTCUTS +#undef X + SHORTCUT_COUNT + }; + class Settings { public: @@ -218,14 +224,6 @@ namespace anm2ed void save(const std::string&, const std::string&); }; - enum ShortcutType - { -#define X(symbol, name, string, type, ...) symbol, - SETTINGS_SHORTCUTS -#undef X - SHORTCUT_COUNT - }; - constexpr const char* SHORTCUT_STRINGS[] = { #define X(symbol, name, string, type, ...) string, SETTINGS_SHORTCUTS diff --git a/src/snapshots.h b/src/snapshots.h index ee7382d..a1a05f9 100644 --- a/src/snapshots.h +++ b/src/snapshots.h @@ -17,6 +17,7 @@ namespace anm2ed public: anm2::Anm2 anm2{}; anm2::Reference reference{}; + float frameTime{}; Playback playback{}; Storage animation{}; Storage merge{}; @@ -26,7 +27,7 @@ namespace anm2ed Storage sound{}; Storage spritesheet{}; Storage items{}; - std::map frames{}; + Storage frames{}; std::string message = snapshots::ACTION; }; diff --git a/src/state.cpp b/src/state.cpp index 545b835..998738d 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -17,12 +17,14 @@ namespace anm2ed constexpr auto UPDATE_RATE = 120; constexpr auto UPDATE_INTERVAL = (1000 / UPDATE_RATE); - State::State(SDL_Window*& window, std::vector& arguments) + State::State(SDL_Window*& window, Settings& settings, std::vector& arguments) { dialog = Dialog(window); for (auto argument : arguments) manager.open(argument); + + manager.chords_set(settings); } void State::tick(Settings& settings) diff --git a/src/state.h b/src/state.h index 07cc3f0..d36270e 100644 --- a/src/state.h +++ b/src/state.h @@ -27,7 +27,7 @@ namespace anm2ed uint64_t previousTick{}; uint64_t previousUpdate{}; - State(SDL_Window*&, std::vector&); + State(SDL_Window*&, Settings& settings, std::vector&); void loop(SDL_Window*&, Settings&); }; diff --git a/src/storage.cpp b/src/storage.cpp index 95b9e79..7f147d8 100644 --- a/src/storage.cpp +++ b/src/storage.cpp @@ -2,6 +2,8 @@ namespace anm2ed { + void Storage::clear() { *this = Storage(); } + void Storage::labels_set(std::vector labels) { labelsString = labels; diff --git a/src/storage.h b/src/storage.h index db43cf3..f3cda62 100644 --- a/src/storage.h +++ b/src/storage.h @@ -1,6 +1,5 @@ #pragma once -#include "anm2/anm2.h" #include "imgui_.h" namespace anm2ed @@ -15,14 +14,7 @@ namespace anm2ed std::vector labels{}; imgui::MultiSelectStorage selection{}; + void clear(); void labels_set(std::vector); }; - - class FrameStorage - { - public: - anm2::Type referenceType{anm2::NONE}; - int referenceID{-1}; - int referenceFrameIndex{-1}; - }; } \ No newline at end of file diff --git a/src/types.h b/src/types.h index 80b58e7..ca9edc2 100644 --- a/src/types.h +++ b/src/types.h @@ -70,33 +70,24 @@ namespace anm2ed::types { constexpr auto ID_NONE = -1; - constexpr ImVec2 to_imvec2(const glm::vec2& v) noexcept - { - return {v.x, v.y}; - } - constexpr glm::vec2 to_vec2(const ImVec2& v) noexcept - { - return {v.x, v.y}; - } + constexpr ImVec2 to_imvec2(const glm::vec2& v) noexcept { return {v.x, v.y}; } + constexpr glm::vec2 to_vec2(const ImVec2& v) noexcept { return {v.x, v.y}; } - constexpr glm::ivec2 to_ivec2(const ImVec2& v) noexcept - { - return {v.x, v.y}; - } + constexpr glm::ivec2 to_ivec2(const ImVec2& v) noexcept { return {v.x, v.y}; } - constexpr ImVec4 to_imvec4(const glm::vec4& v) noexcept - { - return {v.x, v.y, v.z, v.w}; - } + constexpr ImVec4 to_imvec4(const glm::vec4& v) noexcept { return {v.x, v.y, v.z, v.w}; } - constexpr glm::vec4 to_vec4(const ImVec4& v) noexcept - { - return {v.x, v.y, v.z, v.w}; - } + constexpr glm::vec4 to_vec4(const ImVec4& v) noexcept { return {v.x, v.y, v.z, v.w}; } template constexpr T& dummy_value() { static T value{}; return value; } + + template constexpr T& dummy_value_negative() + { + static T value{-1}; + return value; + } } \ No newline at end of file diff --git a/src/util/filesystem_.cpp b/src/util/filesystem_.cpp index 251df42..51db318 100644 --- a/src/util/filesystem_.cpp +++ b/src/util/filesystem_.cpp @@ -8,37 +8,14 @@ namespace anm2ed::util::filesystem { - std::string path_pref_get(const char* org, const char* app) + std::string path_preferences_get() { - auto path = SDL_GetPrefPath(org, app); + auto path = SDL_GetPrefPath(nullptr, "anm2ed"); std::string string = path; SDL_free(path); return string; } - std::string path_preferences_get() { return path_pref_get(nullptr, "anm2ed"); } - std::string path_base_get() { return std::string(SDL_GetBasePath()); } - std::string path_executable_get() { return std::filesystem::path(path_base_get()) / "anm2ed"; } - -#ifdef __unix__ - std::string path_application_get() - { - return std::filesystem::path(path_pref_get(nullptr, "applications")) / "anm2ed.desktop"; - } - - std::string path_mime_get() - { - return std::filesystem::path(path_pref_get(nullptr, "mime/application")) / "x-anm2+xml.xml"; - } - - std::string path_icon_get() { return std::filesystem::path(path_preferences_get()) / "anm2ed.png"; } - std::string path_icon_file_get() - { - return std::filesystem::path(path_preferences_get()) / "application-x-anm2+xml.png"; - } - -#endif - bool path_is_exist(const std::string& path) { std::error_code errorCode; diff --git a/src/util/filesystem_.h b/src/util/filesystem_.h index f9bf2e7..cd7fd8f 100644 --- a/src/util/filesystem_.h +++ b/src/util/filesystem_.h @@ -5,17 +5,7 @@ namespace anm2ed::util::filesystem { -#ifdef __unix__ - std::string path_application_get(); - std::string path_mime_get(); - std::string path_icon_get(); - std::string path_icon_file_get(); -#endif - - std::string path_pref_get(); std::string path_preferences_get(); - std::string path_base_get(); - std::string path_executable_get(); bool path_is_exist(const std::string&); bool path_is_extension(const std::string&, const std::string&); diff --git a/src/util/vector_.h b/src/util/vector_.h index eba0dd9..dcb419e 100644 --- a/src/util/vector_.h +++ b/src/util/vector_.h @@ -12,15 +12,40 @@ namespace anm2ed::util::vector return index >= 0 && index < (int)v.size() ? &v[index] : nullptr; } - template bool in_bounds(std::vector& v, int index) + template int find_index(std::vector& v, T& value) { - return index >= 0 && index < (int)v.size(); + auto it = std::find(v.begin(), v.end(), value); + if (it == v.end()) return -1; + return (int)(std::distance(v.begin(), it)); } + + template bool in_bounds(std::vector& v, int index) { return index >= 0 && index < (int)v.size(); } template void clamp_in_bounds(std::vector& v, int& index) { index = std::clamp(index, 0, (int)v.size() - 1); } + template int move_index(std::vector& v, int source, int dest) + { + auto size = (int)(v.size()); + if (source < 0 || source >= size) return -1; + dest = std::clamp(dest, 0, size - 1); + + if (source == dest) return dest; + + auto isInsertAfter = source < dest; + + T item = std::move(v[source]); + v.erase(v.begin() + source); + + if (dest > source) --dest; // destination shifts when removing earlier slot + if (isInsertAfter) ++dest; // drop after original target + + dest = std::clamp(dest, 0, (int)(v.size())); + v.insert(v.begin() + dest, std::move(item)); + return dest; + } + template std::set move_indices(std::vector& v, std::vector& indices, int index) { if (indices.empty()) return {}; @@ -60,5 +85,4 @@ namespace anm2ed::util::vector return moveIndices; } - }