#include "timeline.hpp" #include #include #include #include #include #include #include #include "actions.hpp" #include "log.hpp" #include "toast.hpp" #include "util/imgui/draw.hpp" #include "util/imgui/input.hpp" #include "util/imgui/layout.hpp" #include "util/imgui/shortcut.hpp" #include "util/imgui/tooltip.hpp" #include "vector.hpp" using namespace anm2ed::resource; using namespace anm2ed::types; using namespace anm2ed::util; using namespace glm; namespace anm2ed::imgui { constexpr auto WIDTH_MULTIPLIER = 1.25f; constexpr auto COLOR_HIDDEN_MULTIPLIER = vec4(0.5f, 0.5f, 0.5f, 1.000f); constexpr auto FRAME_BORDER_COLOR_DARK = ImVec4(1.0f, 1.0f, 1.0f, 0.15f); constexpr auto FRAME_BORDER_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 0.25f); constexpr auto FRAME_BORDER_COLOR_REFERENCED_DARK = ImVec4(1.0f, 1.0f, 1.0f, 0.50f); constexpr auto FRAME_BORDER_COLOR_REFERENCED_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 0.60f); constexpr auto FRAME_BORDER_THICKNESS = 1.0f; constexpr auto FRAME_BORDER_THICKNESS_REFERENCED = 2.0f; constexpr auto FRAME_MULTIPLE_OVERLAY_COLOR_DARK = ImVec4(1.0f, 1.0f, 1.0f, 0.05f); constexpr auto FRAME_MULTIPLE_OVERLAY_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 0.1f); constexpr auto FRAME_ROUNDING = 3.0f; constexpr auto ICON_TINT_DEFAULT_DARK = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); constexpr auto ICON_TINT_DEFAULT_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); constexpr auto ITEM_TEXT_COLOR_DARK = to_imvec4(color::WHITE); constexpr auto ITEM_TEXT_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); constexpr auto PLAYHEAD_ICON_TINT_LIGHT = ImVec4(0.3922f, 0.7843f, 0.5882f, 1.0f); constexpr auto PLAYHEAD_LINE_COLOR_DARK = to_imvec4(color::WHITE); constexpr auto PLAYHEAD_LINE_COLOR_LIGHT = ImVec4(0.1961f, 0.5882f, 0.3922f, 1.0f); constexpr auto PLAYHEAD_LINE_THICKNESS = 4.0f; constexpr auto TEXT_MULTIPLE_COLOR_DARK = to_imvec4(color::WHITE); constexpr auto TEXT_MULTIPLE_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); constexpr auto TIMELINE_BACKGROUND_COLOR_LIGHT = ImVec4(0.5490f, 0.5490f, 0.5882f, 1.0f); constexpr auto TIMELINE_PLAYHEAD_RECT_COLOR_DARK = ImVec4(0.60f, 0.45f, 0.30f, 1.0f); constexpr auto TIMELINE_PLAYHEAD_RECT_COLOR_LIGHT = ImVec4(0.8353f, 0.8353f, 0.7294f, 1.0f); constexpr auto TIMELINE_TICK_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); constexpr auto TIMELINE_CHILD_BG_COLOR_LIGHT = ImVec4(0.5490f, 0.5490f, 0.5882f, 1.0f); constexpr auto TIMELINE_TEXT_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); enum class DropZone { BEFORE, INSIDE, AFTER }; constexpr glm::vec4 FRAME_COLOR_LIGHT_BASE[] = {{0.80f, 0.80f, 0.80f, 1.0f}, {0.5216f, 0.7333f, 1.0f, 1.0f}, {1.0f, 0.9961f, 0.5882f, 1.0f}, {0.6157f, 1.0f, 0.5882f, 1.0f}, {1.0f, 0.5882f, 0.8314f, 1.0f}}; constexpr glm::vec4 FRAME_COLOR_LIGHT_ACTIVE[] = {{0.74f, 0.74f, 0.74f, 1.0f}, {0.0980f, 0.3765f, 0.6431f, 1.0f}, {1.0f, 0.5255f, 0.3333f, 1.0f}, {0.3686f, 0.5765f, 0.2353f, 1.0f}, {0.6118f, 0.2039f, 0.2745f, 1.0f}}; constexpr glm::vec4 FRAME_COLOR_LIGHT_HOVERED[] = {{0.84f, 0.84f, 0.84f, 1.0f}, {0.0752f, 0.2887f, 0.4931f, 1.0f}, {0.85f, 0.4467f, 0.2833f, 1.0f}, {0.2727f, 0.4265f, 0.1741f, 1.0f}, {0.4618f, 0.1539f, 0.2072f, 1.0f}}; constexpr glm::vec4 ITEM_COLOR_LIGHT_BASE[] = {{0.3059f, 0.3255f, 0.5412f, 1.0f}, {0.3333f, 0.5725f, 0.8392f, 1.0f}, {1.0f, 0.5412f, 0.3412f, 1.0f}, {0.5255f, 0.8471f, 0.4588f, 1.0f}, {0.7961f, 0.3882f, 0.5412f, 1.0f}}; constexpr glm::vec4 ITEM_COLOR_LIGHT_ACTIVE[] = {{0.3459f, 0.3655f, 0.5812f, 1.0f}, {0.3733f, 0.6125f, 0.8792f, 1.0f}, {0.9106f, 0.4949f, 0.2753f, 1.0f}, {0.5655f, 0.8871f, 0.4988f, 1.0f}, {0.8361f, 0.4282f, 0.5812f, 1.0f}}; constexpr glm::vec4 ITEM_COLOR_LIGHT_SELECTED[] = {{0.74f, 0.74f, 0.74f, 1.0f}, {0.2039f, 0.4549f, 0.7176f, 1.0f}, {0.8745f, 0.4392f, 0.2275f, 1.0f}, {0.3765f, 0.6784f, 0.2980f, 1.0f}, {0.6353f, 0.2235f, 0.3647f, 1.0f}}; constexpr auto FRAME_MULTIPLE = 5; constexpr auto FRAME_TOOLTIP_HOVER_DELAY = 0.75f; #define ITEM_CHILD_WIDTH ImGui::GetTextLineHeightWithSpacing() * 12.5 struct TimelineItemRow { int type{NONE}; int id{-1}; int index{-1}; int groupId{-1}; int depth{}; bool isGroup{}; }; 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& region = document.region; auto animation = anm2.element_get(ElementType::ANIMATION, reference.animationIndex); auto rowFrameChildHeight = ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().WindowPadding.y * 1.5f; style = ImGui::GetStyle(); auto isLightTheme = settings.theme == theme::LIGHT; bool isTextPushed = false; if (isLightTheme) { ImGui::PushStyleColor(ImGuiCol_Text, TIMELINE_TEXT_COLOR_LIGHT); isTextPushed = true; } auto type_index = [](int type) { return std::clamp(type, 0, (int)TRIGGER); }; auto item_type_get = [](int type) { return static_cast(type); }; auto item_get = [&](int type, int id = -1) { return animation ? animation_item_get(*animation, item_type_get(type), id) : nullptr; }; auto frame_get = [&]() { return anm2.element_get(reference.animationIndex, item_type_get(reference.itemType), reference.frameIndex, reference.itemID); }; auto selected_item_get = [&]() { return anm2.element_get(reference.animationIndex, item_type_get(reference.itemType), reference.itemID); }; auto item_frames_count = [](const Element* item) { return item ? (int)item->children.size() : 0; }; auto layer_get = [&](int id) { return anm2.element_get(ElementType::LAYER_ELEMENT, id); }; auto null_get = [&](int id) { auto nulls = anm2.element_get(ElementType::NULLS); return nulls ? element_child_id_get(*nulls, ElementType::NULL_ELEMENT, id) : nullptr; }; auto spritesheet_get = [&](int id) { return anm2.element_get(ElementType::SPRITESHEET, id); }; auto info_get = [&]() { return element_first_get(anm2.root, ElementType::INFO); }; auto container_type_get = [](int type) { if (type == LAYER) return ElementType::LAYER_ANIMATIONS; if (type == NULL_) return ElementType::NULL_ANIMATIONS; return ElementType::UNKNOWN; }; auto track_type_get = [](int type) { if (type == LAYER) return ElementType::LAYER_ANIMATION; if (type == NULL_) return ElementType::NULL_ANIMATION; return ElementType::UNKNOWN; }; auto track_id_get = [](const Element& track, int type) { if (type == LAYER) return track.layerId; if (type == NULL_) return track.nullId; return -1; }; auto track_container_get = [&](int type) { return animation ? element_child_first_get(*animation, container_type_get(type)) : nullptr; }; auto track_group_get = [&](int type, int groupId) { auto container = track_container_get(type); return container ? element_child_id_get(*container, ElementType::GROUP, groupId) : nullptr; }; auto is_track_group_visible = [&](int type, int groupId) { if (groupId == -1) return true; auto group = track_group_get(type, groupId); return !group || group->isVisible; }; auto row_group_get = [&](const TimelineItemRow& row) -> Element* { return row.isGroup ? track_group_get(row.type, row.id) : nullptr; }; auto group_items_count_get = [&](int type, int groupId) { auto container = track_container_get(type); auto trackType = track_type_get(type); int count{}; if (!container) return count; for (auto& item : container->children) if (item.type == trackType && item.groupId == groupId) ++count; return count; }; auto command_animation_get = [](Document& document, int animationIndex) { return document.anm2.element_get(ElementType::ANIMATION, animationIndex); }; auto command_item_get = [](Document& document, int animationIndex, int type, int id) { auto animation = document.anm2.element_get(ElementType::ANIMATION, animationIndex); return animation ? animation_item_get(*animation, static_cast(type), id) : nullptr; }; auto command_frame_get = [](Document& document, const Reference& targetReference) { return document.anm2.element_get(targetReference.animationIndex, static_cast(targetReference.itemType), targetReference.frameIndex, targetReference.itemID); }; auto command_layer_get = [](Document& document, int id) { return document.anm2.element_get(ElementType::LAYER_ELEMENT, id); }; auto command_spritesheet_get = [](Document& document, int id) { return document.anm2.element_get(ElementType::SPRITESHEET, id); }; auto command_info_get = [](Document& document) { return element_first_get(document.anm2.root, ElementType::INFO); }; auto item_reference_get = [&](int type, int id) { return Reference{reference.animationIndex, type, id}; }; auto item_reference_from_frame_get = [](Reference frameReference) { frameReference.frameIndex = -1; return frameReference; }; auto is_same_item = [](const Reference& left, const Reference& right) { return left.animationIndex == right.animationIndex && left.itemType == right.itemType && left.itemID == right.itemID; }; auto group_selection_reset_for = [this](Document& targetDocument) { targetDocument.groupReferences.clear(); isRowSelectionAnchorSet = false; }; auto frame_references_for_current_get = [&]() { std::set result = frames.references; if (result.empty()) for (auto frameIndex : frames.selection) result.insert({reference.animationIndex, reference.itemType, reference.itemID, frameIndex}); return result; }; auto item_references_for_current_get = [&]() { std::set result = document.items.references; if (result.empty() && reference.itemType != NONE) result.insert(item_reference_get(reference.itemType, reference.itemID)); return result; }; auto frames_selection_sync_for = [&](Document& targetDocument) { targetDocument.frames.selection.clear(); for (const auto& frameReference : targetDocument.frames.references) if (is_same_item(frameReference, targetDocument.reference) && frameReference.frameIndex >= 0) targetDocument.frames.selection.insert(frameReference.frameIndex); frameSelectionSnapshot.assign(targetDocument.frames.selection.begin(), targetDocument.frames.selection.end()); frameSelectionSnapshotReference = targetDocument.reference; }; auto item_selection_set_for = [&](Document& targetDocument, Reference itemReference) { itemReference.frameIndex = -1; targetDocument.items.references = {itemReference}; }; auto frame_selection_set_for = [&](Document& targetDocument, Reference frameReference) { group_selection_reset_for(targetDocument); targetDocument.frames.references = {frameReference}; targetDocument.reference = frameReference; item_selection_set_for(targetDocument, item_reference_from_frame_get(frameReference)); frames_selection_sync_for(targetDocument); }; auto frame_selection_toggle_for = [&](Document& targetDocument, Reference frameReference) { group_selection_reset_for(targetDocument); auto& selection = targetDocument.frames.references; auto itemReference = item_reference_from_frame_get(frameReference); if (selection.contains(frameReference)) { if (selection.size() > 1) selection.erase(frameReference); bool isItemStillSelected{}; for (const auto& selectedFrame : selection) if (is_same_item(selectedFrame, itemReference)) isItemStillSelected = true; if (!isItemStillSelected && targetDocument.items.references.size() > 1) targetDocument.items.references.erase(itemReference); } else { selection.insert(frameReference); targetDocument.items.references.insert(itemReference); } targetDocument.reference = frameReference; frames_selection_sync_for(targetDocument); }; auto frame_selection_range_set_for = [&](Document& targetDocument, Reference firstReference, Reference lastReference, bool isAdditive) -> bool { group_selection_reset_for(targetDocument); if (!is_same_item(firstReference, lastReference) || firstReference.frameIndex < 0 || lastReference.frameIndex < 0) return false; auto item = command_item_get(targetDocument, lastReference.animationIndex, lastReference.itemType, lastReference.itemID); if (!item) return false; if (firstReference.frameIndex >= (int)item->children.size() || lastReference.frameIndex >= (int)item->children.size()) return false; auto firstIndex = firstReference.frameIndex; auto lastIndex = lastReference.frameIndex; if (firstIndex > lastIndex) std::swap(firstIndex, lastIndex); auto itemReference = item_reference_from_frame_get(lastReference); if (!isAdditive) { targetDocument.frames.references.clear(); item_selection_set_for(targetDocument, itemReference); } else targetDocument.items.references.insert(itemReference); for (int i = firstIndex; i <= lastIndex; ++i) targetDocument.frames.references.insert( {lastReference.animationIndex, lastReference.itemType, lastReference.itemID, i}); targetDocument.reference = lastReference; frames_selection_sync_for(targetDocument); return true; }; auto all_frame_references_for_item_get = [&](const Reference& itemReference) { std::set result{}; auto item = item_get(itemReference.itemType, itemReference.itemID); if (!item) return result; for (int i = 0; i < (int)item->children.size(); ++i) result.insert({itemReference.animationIndex, itemReference.itemType, itemReference.itemID, i}); return result; }; auto all_frame_references_for_items_get = [&]() { std::set result{}; auto itemReferences = item_references_for_current_get(); if (itemReferences.empty()) itemReferences.insert(item_reference_get(reference.itemType, reference.itemID)); for (auto itemReference : itemReferences) { auto itemFrames = all_frame_references_for_item_get(itemReference); result.insert(itemFrames.begin(), itemFrames.end()); } return result; }; auto frames_selection_reset_for = [this](Document& targetDocument) { targetDocument.frames.selection.clear(); targetDocument.frames.references.clear(); frameSelectionSnapshot.clear(); frameSelectionLocked.clear(); isFrameSelectionLocked = false; frameFocusRequested = false; frameFocusIndex = -1; frameSelectionSnapshotReference = targetDocument.reference; }; auto frames_selection_set_reference_for = [this](Document& targetDocument) { auto& targetFrames = targetDocument.frames; auto& targetReference = targetDocument.reference; targetFrames.selection.clear(); targetFrames.references.clear(); if (targetReference.frameIndex >= 0) { targetFrames.selection.insert(targetReference.frameIndex); targetFrames.references.insert(targetReference); } frameSelectionSnapshot.assign(targetFrames.selection.begin(), targetFrames.selection.end()); frameSelectionSnapshotReference = targetReference; frameSelectionLocked.clear(); isFrameSelectionLocked = false; frameFocusIndex = targetReference.frameIndex; frameFocusRequested = targetReference.frameIndex >= 0; }; auto reference_clear_for = [=](Document& targetDocument) { targetDocument.reference = {targetDocument.reference.animationIndex}; frames_selection_reset_for(targetDocument); targetDocument.items.references.clear(); }; auto reference_set_item_for = [=](Document& targetDocument, int type, int id) { targetDocument.reference = {targetDocument.reference.animationIndex, type, id}; frames_selection_reset_for(targetDocument); item_selection_set_for(targetDocument, targetDocument.reference); }; auto reference_set_timeline_item_for = [=](Document& targetDocument, int type, int id) { if (type == LAYER) if (auto layer = command_layer_get(targetDocument, id)) targetDocument.spritesheet.reference = layer->spritesheetId; reference_set_item_for(targetDocument, type, id); }; auto command_push = [&](auto run) { manager.command_push({manager.selected, [run](Manager& manager, Document& document) mutable { run(manager, document); }}); }; auto snapshot_command_push = [&](StringType messageType) { auto message = std::string(localize.get(messageType)); manager.command_push( {manager.selected, [message](Manager&, Document& document) { document.snapshot(message); }}); }; auto edit_command_push = [&](StringType messageType, Document::ChangeType changeType, auto run) { auto message = std::string(localize.get(messageType)); manager.command_push({manager.selected, [=](Manager& manager, Document& document) mutable { document.snapshot(message); run(manager, document); document.anm2_change(changeType); }}); }; auto type_color_base_vec = [&](int type) { return isLightTheme ? FRAME_COLOR_LIGHT_BASE[type_index(type)] : TYPE_COLOR[type]; }; auto type_color_active_vec = [&](int type) { if (isLightTheme) return FRAME_COLOR_LIGHT_ACTIVE[type_index(type)]; return TYPE_COLOR_ACTIVE[type]; }; auto type_color_hovered_vec = [&](int type) { if (isLightTheme) return FRAME_COLOR_LIGHT_HOVERED[type_index(type)]; return TYPE_COLOR_HOVERED[type]; }; auto item_color_vec = [&](int type) { if (!isLightTheme) return TYPE_COLOR[type]; return ITEM_COLOR_LIGHT_BASE[type_index(type)]; }; auto item_color_active_vec = [&](int type) { if (!isLightTheme) return TYPE_COLOR_ACTIVE[type]; return ITEM_COLOR_LIGHT_ACTIVE[type_index(type)]; }; auto iconTintDefault = isLightTheme ? ICON_TINT_DEFAULT_LIGHT : ICON_TINT_DEFAULT_DARK; auto itemIconTint = isLightTheme ? ICON_TINT_DEFAULT_LIGHT : iconTintDefault; auto frameBorderColor = isLightTheme ? FRAME_BORDER_COLOR_LIGHT : FRAME_BORDER_COLOR_DARK; auto frameBorderColorReferenced = isLightTheme ? FRAME_BORDER_COLOR_REFERENCED_LIGHT : FRAME_BORDER_COLOR_REFERENCED_DARK; auto frameMultipleOverlayColor = isLightTheme ? FRAME_MULTIPLE_OVERLAY_COLOR_LIGHT : FRAME_MULTIPLE_OVERLAY_COLOR_DARK; auto textMultipleColor = isLightTheme ? TEXT_MULTIPLE_COLOR_LIGHT : TEXT_MULTIPLE_COLOR_DARK; auto playheadLineColor = isLightTheme ? PLAYHEAD_LINE_COLOR_LIGHT : PLAYHEAD_LINE_COLOR_DARK; auto playheadIconTint = isLightTheme ? PLAYHEAD_ICON_TINT_LIGHT : iconTintDefault; auto timelineBackgroundColor = isLightTheme ? TIMELINE_BACKGROUND_COLOR_LIGHT : ImGui::GetStyleColorVec4(ImGuiCol_Header); auto timelinePlayheadRectColor = isLightTheme ? TIMELINE_PLAYHEAD_RECT_COLOR_LIGHT : TIMELINE_PLAYHEAD_RECT_COLOR_DARK; auto timelineTickColor = isLightTheme ? TIMELINE_TICK_COLOR_LIGHT : frameBorderColor; auto itemTextColor = isLightTheme ? ITEM_TEXT_COLOR_LIGHT : ITEM_TEXT_COLOR_DARK; auto overlay_icon = [&](GLuint textureId, ImVec4 tint, bool isForced = false) { if (!isForced && !isLightTheme) return; auto min = ImGui::GetItemRectMin(); auto max = ImGui::GetItemRectMax(); ImGui::GetWindowDrawList()->AddImage((ImTextureID)(intptr_t)textureId, min, max, ImVec2(0, 0), ImVec2(1, 1), ImGui::GetColorU32(tint)); }; auto frames_selection_set_reference = [&]() { frames_selection_set_reference_for(document); }; auto playback_stop = [&]() { playback.isPlaying = false; playback.isFinished = false; playback.timing_reset(); }; auto frame_insert = [&]() { auto targetReference = reference; auto targetFrameTime = document.frameTime; if (!animation || !command_item_get(document, targetReference.animationIndex, targetReference.itemType, targetReference.itemID)) return; edit_command_push(EDIT_INSERT_FRAME, Document::FRAMES, [=, this](Manager&, Document& document) mutable { auto animation = command_animation_get(document, targetReference.animationIndex); auto item = command_item_get(document, targetReference.animationIndex, targetReference.itemType, targetReference.itemID); if (!animation || !item) return; auto newReference = targetReference; if (targetReference.itemType == TRIGGER) { for (auto& trigger : item->children) if (targetFrameTime == trigger.atFrame) return; auto addFrame = element_make(ElementType::TRIGGER); addFrame.atFrame = targetFrameTime; item->children.push_back(addFrame); frames_sort_by_at_frame(*item); newReference.frameIndex = frame_index_from_at_frame_get(*item, addFrame.atFrame); } else { auto frame = command_frame_get(document, targetReference); if (frame) { auto addFrame = *frame; auto insertIndex = std::clamp(targetReference.frameIndex + 1, 0, (int)item->children.size()); item->children.insert(item->children.begin() + insertIndex, addFrame); newReference.frameIndex = insertIndex; } else if (!item->children.empty()) { auto addFrame = item->children.back(); item->children.emplace_back(addFrame); newReference.frameIndex = (int)item->children.size() - 1; } else { item->children.emplace_back(element_make(ElementType::FRAME)); newReference.frameIndex = 0; } } document.reference = newReference; frames_selection_set_reference_for(document); if (newReference.itemType != TRIGGER) document.frameTime = frame_time_from_index_get(*item, newReference.frameIndex); }); }; auto frames_delete_for = [=](Document& document, std::set selectedFrames) mutable { std::map> groupedFrames{}; for (auto frameReference : selectedFrames) { auto itemReference = item_reference_from_frame_get(frameReference); groupedFrames[itemReference].insert(frameReference.frameIndex); } for (auto& [itemReference, indices] : groupedFrames) { auto item = command_item_get(document, itemReference.animationIndex, itemReference.itemType, itemReference.itemID); if (!item) continue; for (auto it = indices.rbegin(); it != indices.rend(); ++it) { auto i = *it; if (i >= 0 && i < (int)item->children.size()) item->children.erase(item->children.begin() + i); } } auto item = command_item_get(document, document.reference.animationIndex, document.reference.itemType, document.reference.itemID); if (!item || item->children.empty()) { document.reference.frameIndex = -1; frames_selection_reset_for(document); return; } document.reference.frameIndex = glm::clamp(document.reference.frameIndex, 0, (int)item->children.size() - 1); frames_selection_set_reference_for(document); }; auto frames_delete_action = [&]() { auto selectedFrames = frame_references_for_current_get(); if (selectedFrames.empty()) return; edit_command_push(EDIT_DELETE_FRAMES, Document::FRAMES, [=](Manager&, Document& document) mutable { frames_delete_for(document, selectedFrames); }); }; auto frames_bake = [&]() { auto selectedFrames = frame_references_for_current_get(); auto bakeInterval = settings.bakeInterval; auto isRoundScale = settings.bakeIsRoundScale; auto isRoundRotation = settings.bakeIsRoundRotation; std::erase_if(selectedFrames, [](const Reference& frameReference) { return frameReference.itemType == TRIGGER; }); if (selectedFrames.empty()) return; edit_command_push(EDIT_BAKE_FRAMES, Document::FRAMES, [=, this](Manager&, Document& document) mutable { std::map> groupedFrames{}; for (auto frameReference : selectedFrames) { auto itemReference = item_reference_from_frame_get(frameReference); groupedFrames[itemReference].insert(frameReference.frameIndex); } std::set bakedSelection{}; for (auto& [itemReference, indices] : groupedFrames) { auto item = command_item_get(document, itemReference.animationIndex, itemReference.itemType, itemReference.itemID); if (!item) continue; int insertedBefore = 0; for (auto originalIndex : indices) { auto i = originalIndex + insertedBefore; if (!vector::in_bounds(item->children, i)) continue; auto originalDuration = item->children[i].duration; frame_bake(*item, i, bakeInterval, isRoundScale, isRoundRotation); auto bakedCount = originalDuration <= FRAME_DURATION_MIN ? 1 : (int)std::ceil((float)originalDuration / bakeInterval); for (int offset = 0; offset < bakedCount; ++offset) bakedSelection.insert( {itemReference.animationIndex, itemReference.itemType, itemReference.itemID, i + offset}); insertedBefore += bakedCount - 1; } } document.frames.references = std::move(bakedSelection); if (!document.frames.references.empty()) { document.reference = *document.frames.references.begin(); frames_selection_sync_for(document); frameSelectionSnapshotReference = document.reference; frameSelectionLocked.clear(); isFrameSelectionLocked = false; frameFocusIndex = document.reference.frameIndex; frameFocusRequested = true; } else frames_selection_reset_for(document); }); }; auto frame_split = [&]() { auto selectedFrames = frame_references_for_current_get(); if (selectedFrames.size() != 1) return; auto targetReference = *selectedFrames.begin(); auto playheadTime = (int)std::floor(playback.time); if (targetReference.itemType == TRIGGER) return; edit_command_push(EDIT_SPLIT_FRAME, Document::FRAMES, [=](Manager&, Document& document) mutable { if (targetReference.itemType == TRIGGER) return; auto item = command_item_get(document, targetReference.animationIndex, targetReference.itemType, targetReference.itemID); auto frame = command_frame_get(document, targetReference); if (!item || !frame) return; auto originalDuration = frame->duration; if (originalDuration <= 1) return; auto frameStartTime = frame_time_from_index_get(*item, targetReference.frameIndex); int frameStart = (int)std::round(frameStartTime); int firstDuration = playheadTime - frameStart + 1; if (firstDuration <= 0 || firstDuration >= originalDuration) return; int secondDuration = originalDuration - firstDuration; auto splitFrame = *frame; splitFrame.duration = secondDuration; auto nextFrame = track_frame_get(*item, targetReference.frameIndex + 1); if (frame->interpolation != Interpolation::NONE && nextFrame) { float interpolation = (float)firstDuration / (float)originalDuration; switch (frame->interpolation) { case Interpolation::EASE_IN: interpolation *= interpolation; break; case Interpolation::EASE_OUT: interpolation = 1.0f - ((1.0f - interpolation) * (1.0f - interpolation)); break; case Interpolation::EASE_IN_OUT: interpolation = interpolation < 0.5f ? (2.0f * interpolation * interpolation) : (1.0f - std::pow(-2.0f * interpolation + 2.0f, 2.0f) * 0.5f); break; case Interpolation::LINEAR: case Interpolation::NONE: default: break; } splitFrame.rotation = glm::mix(frame->rotation, nextFrame->rotation, interpolation); splitFrame.position = glm::mix(frame->position, nextFrame->position, interpolation); splitFrame.scale = glm::mix(frame->scale, nextFrame->scale, interpolation); splitFrame.colorOffset = glm::mix(frame->colorOffset, nextFrame->colorOffset, interpolation); splitFrame.tint = glm::mix(frame->tint, nextFrame->tint, interpolation); } frame->duration = firstDuration; item->children.insert(item->children.begin() + targetReference.frameIndex + 1, splitFrame); document.reference = targetReference; frames_selection_set_reference_for(document); }); }; auto reference_clear = [&]() { group_selection_reset_for(document); reference_clear_for(document); }; auto reference_set_timeline_item = [&](int type, int id) { group_selection_reset_for(document); reference_set_timeline_item_for(document, type, id); }; auto timeline_item_rows_get = [&]() { std::vector rows{}; if (!animation) return rows; auto track_row_push = [&](const Element& item, int type, int index, int depth = 0) { rows.push_back({.type = type, .id = track_id_get(item, type), .index = index, .groupId = item.groupId, .depth = depth}); }; auto group_row_push = [&](const Element& group, int type, int index) { rows.push_back({.type = type, .id = group.id, .index = index, .isGroup = true}); }; auto group_ids_get = [](const Element& container) { std::set result{}; for (const auto& item : container.children) if (item.type == ElementType::GROUP) result.insert(item.id); return result; }; rows.push_back({.type = ROOT}); if (auto layerAnimations = element_child_first_get(*animation, ElementType::LAYER_ANIMATIONS)) { auto groupIds = group_ids_get(*layerAnimations); auto layer_track_push = [&](int groupId, int depth) { for (int j = (int)layerAnimations->children.size() - 1; j >= 0; --j) { auto& item = layerAnimations->children[j]; if (item.type != ElementType::LAYER_ANIMATION || item.groupId != groupId) continue; if (settings.timelineIsShowUnused || !item.children.empty()) track_row_push(item, LAYER, j, depth); } }; for (int i = (int)layerAnimations->children.size() - 1; i >= 0; --i) { auto& item = layerAnimations->children[i]; if (item.type == ElementType::GROUP) { group_row_push(item, LAYER, i); if (item.isExpanded) layer_track_push(item.id, 1); } else if (item.type == ElementType::LAYER_ANIMATION && !groupIds.contains(item.groupId) && (settings.timelineIsShowUnused || !item.children.empty())) track_row_push(item, LAYER, i); } } if (auto nullAnimations = element_child_first_get(*animation, ElementType::NULL_ANIMATIONS)) { auto groupIds = group_ids_get(*nullAnimations); auto null_track_push = [&](int groupId, int depth) { for (int j = 0; j < (int)nullAnimations->children.size(); ++j) { auto& item = nullAnimations->children[j]; if (item.type != ElementType::NULL_ANIMATION || item.groupId != groupId) continue; if (settings.timelineIsShowUnused || !item.children.empty()) track_row_push(item, NULL_, j, depth); } }; for (int i = 0; i < (int)nullAnimations->children.size(); ++i) { auto& item = nullAnimations->children[i]; if (item.type == ElementType::GROUP) { group_row_push(item, NULL_, i); if (item.isExpanded) null_track_push(item.id, 1); } else if (item.type == ElementType::NULL_ANIMATION && !groupIds.contains(item.groupId) && (settings.timelineIsShowUnused || !item.children.empty())) track_row_push(item, NULL_, i); } } rows.push_back({.type = TRIGGER}); return rows; }; auto timeline_item_references_get = [&]() { std::vector itemReferences; for (const auto& row : timeline_item_rows_get()) { if (row.isGroup) continue; itemReferences.push_back({reference.animationIndex, row.type, row.id}); } return itemReferences; }; auto group_reference_get = [&](const TimelineItemRow& row) { return Reference{reference.animationIndex, row.type, row.id}; }; auto row_reference_get = [&](const TimelineItemRow& row) { return TimelineRowReference{manager.selected, reference.animationIndex, row.type, row.id, row.index, row.isGroup}; }; auto row_item_reference_get = [](const TimelineRowReference& row) { return Reference{row.animationIndex, row.type, row.id}; }; auto timeline_row_references_get = [&]() { std::vector rowReferences; for (const auto& row : timeline_item_rows_get()) { if (row.type == NONE) continue; rowReferences.push_back(row_reference_get(row)); } return rowReferences; }; auto is_group_selected = [&](const TimelineItemRow& row) { return document.groupReferences.contains(group_reference_get(row)); }; auto is_row_selected = [&](const TimelineItemRow& row) { if (row.isGroup) return is_group_selected(row); auto itemReference = item_reference_get(row.type, row.id); auto isReferenced = reference.itemType == row.type && reference.itemID == row.id; return document.items.references.contains(itemReference) || (document.items.references.empty() && document.groupReferences.empty() && isReferenced); }; auto row_selection_clear = [&]() { document.items.references.clear(); document.groupReferences.clear(); }; auto row_selection_insert = [&](const TimelineRowReference& row) { if (row.isGroup) document.groupReferences.insert({row.animationIndex, row.type, row.id}); else document.items.references.insert(row_item_reference_get(row)); }; auto row_selection_erase = [&](const TimelineRowReference& row) { if (row.isGroup) document.groupReferences.erase({row.animationIndex, row.type, row.id}); else document.items.references.erase(row_item_reference_get(row)); }; auto is_row_reference_selected = [&](const TimelineRowReference& row) { if (row.isGroup) return document.groupReferences.contains({row.animationIndex, row.type, row.id}); return document.items.references.contains(row_item_reference_get(row)); }; auto row_selection_count_get = [&]() { return document.items.references.size() + document.groupReferences.size(); }; auto row_selection_set = [&](const TimelineItemRow& row) { auto rowReference = row_reference_get(row); auto isCtrlDown = ImGui::IsKeyDown(ImGuiMod_Ctrl); auto isShiftDown = ImGui::IsKeyDown(ImGuiMod_Shift); if (row.isGroup) reference = {reference.animationIndex}; else { if (row.type == LAYER) if (auto layer = layer_get(row.id)) document.spritesheet.reference = layer->spritesheetId; reference = row_item_reference_get(rowReference); } frames_selection_reset_for(document); if (isShiftDown) { auto rowSelection = timeline_row_references_get(); auto anchor = isRowSelectionAnchorSet ? rowSelectionAnchor : rowReference; auto first = std::find(rowSelection.begin(), rowSelection.end(), anchor); auto last = std::find(rowSelection.begin(), rowSelection.end(), rowReference); if (first == rowSelection.end() || last == rowSelection.end()) { if (!isCtrlDown) row_selection_clear(); row_selection_insert(rowReference); rowSelectionAnchor = rowReference; } else { auto firstIndex = (int)std::distance(rowSelection.begin(), first); auto lastIndex = (int)std::distance(rowSelection.begin(), last); if (firstIndex > lastIndex) std::swap(firstIndex, lastIndex); if (!isCtrlDown) row_selection_clear(); for (int i = firstIndex; i <= lastIndex; ++i) row_selection_insert(rowSelection[i]); if (!isRowSelectionAnchorSet) rowSelectionAnchor = rowReference; } } else if (isCtrlDown) { if (is_row_reference_selected(rowReference) && row_selection_count_get() > 1) row_selection_erase(rowReference); else row_selection_insert(rowReference); rowSelectionAnchor = rowReference; } else { row_selection_clear(); row_selection_insert(rowReference); rowSelectionAnchor = rowReference; } isRowSelectionAnchorSet = true; }; auto reference_set_adjacent_item = [&](int direction) { auto itemReferences = timeline_item_references_get(); if (itemReferences.empty()) return; auto it = std::find_if( itemReferences.begin(), itemReferences.end(), [&](const Reference& itemReference) { return itemReference.itemType == reference.itemType && itemReference.itemID == reference.itemID; }); int index = direction > 0 ? 0 : (int)itemReferences.size() - 1; if (it != itemReferences.end()) index = (int)std::distance(itemReferences.begin(), it) + direction; index = std::clamp(index, 0, (int)itemReferences.size() - 1); auto& itemReference = itemReferences[index]; reference_set_timeline_item(itemReference.itemType, itemReference.itemID); }; auto selected_row_references_get = [&]() { std::vector result{}; for (const auto& row : timeline_item_rows_get()) if (is_row_selected(row)) result.push_back(row_reference_get(row)); return result; }; auto row_drag_references_get = [&](const TimelineItemRow& row) { auto clicked = row_reference_get(row); if (clicked.type != LAYER && clicked.type != NULL_) return std::vector{clicked}; auto rows = selected_row_references_get(); if (!is_row_reference_selected(clicked)) rows = {clicked}; if (rows.empty()) rows = {clicked}; std::erase_if(rows, [&](const TimelineRowReference& rowReference) { return rowReference.type != clicked.type; }); if (rows.empty()) rows = {clicked}; return rows; }; auto row_label_get = [&](const TimelineRowReference& row) -> std::string { if (row.isGroup) { TimelineItemRow groupRow{.type = row.type, .id = row.id, .index = row.index, .isGroup = true}; auto group = row_group_get(groupRow); if (!group || group->name.empty()) return localize.get(TEXT_NEW_GROUP); return group->name; } if (row.type == LAYER) { auto layer = layer_get(row.id); if (!layer) return localize.get(TYPE_STRINGS[row.type]); return std::vformat(localize.get(FORMAT_LAYER), std::make_format_args(layer->id, layer->name, layer->spritesheetId)); } if (row.type == NULL_) { auto null = null_get(row.id); if (!null) return localize.get(TYPE_STRINGS[row.type]); return std::vformat(localize.get(FORMAT_NULL), std::make_format_args(null->id, null->name)); } return localize.get(TYPE_STRINGS[type_index(row.type)]); }; auto row_drag_tooltip_draw = [&](const std::vector& rows) { for (auto row : rows) { auto label = row_label_get(row); ImGui::TextUnformatted(label.c_str()); } }; auto item_remove = [&]() { auto targetRows = selected_row_references_get(); std::map> targetIds{}; std::map> targetGroupIds{}; for (auto row : targetRows) { if (row.type != LAYER && row.type != NULL_) continue; if (row.isGroup) targetGroupIds[row.type].insert(row.id); else targetIds[row.type].insert(row.id); } auto animationIndex = reference.animationIndex; if (!animation) return; if (targetIds.empty() && targetGroupIds.empty()) return; edit_command_push(EDIT_REMOVE_ITEMS, Document::ITEMS, [=, this](Manager&, Document& document) mutable { auto animation = command_animation_get(document, animationIndex); if (!animation) return; for (auto targetType : {LAYER, NULL_}) { auto containerType = container_type_get(targetType); auto targetTrackType = track_type_get(targetType); auto container = element_child_first_get(*animation, containerType); if (!container) continue; auto ids = targetIds.contains(targetType) ? targetIds.at(targetType) : std::set{}; auto groupIds = targetGroupIds.contains(targetType) ? targetGroupIds.at(targetType) : std::set{}; for (int i = (int)container->children.size() - 1; i >= 0; --i) { auto& item = container->children[i]; if (item.type == ElementType::GROUP && groupIds.contains(item.id)) { container->children.erase(container->children.begin() + i); continue; } if (item.type == targetTrackType && (ids.contains(track_id_get(item, targetType)) || groupIds.contains(item.groupId))) container->children.erase(container->children.begin() + i); } } reference_clear_for(document); group_selection_reset_for(document); }); }; auto item_references_groupable_get = [&]() { if (!document.groupReferences.empty()) return std::vector{}; auto selectedItems = item_references_for_current_get(); std::erase_if(selectedItems, [](const Reference& itemReference) { return itemReference.itemType != LAYER && itemReference.itemType != NULL_; }); if (selectedItems.empty()) return std::vector{}; auto itemType = selectedItems.begin()->itemType; for (const auto& itemReference : selectedItems) if (itemReference.itemType != itemType) return std::vector{}; std::vector result{}; for (const auto& row : timeline_item_rows_get()) { if (row.isGroup || row.type != itemType) continue; auto itemReference = item_reference_get(row.type, row.id); if (!selectedItems.contains(itemReference)) continue; if (row.groupId != -1) return std::vector{}; result.push_back(itemReference); } return result; }; auto item_group = [&]() { auto targetReferences = item_references_groupable_get(); if (targetReferences.empty()) return; auto targetType = targetReferences.front().itemType; auto animationIndex = reference.animationIndex; auto groupName = std::string(localize.get(TEXT_NEW_GROUP)); edit_command_push(EDIT_GROUP_ITEMS, Document::ITEMS, [=](Manager&, Document& document) mutable { auto animation = command_animation_get(document, animationIndex); if (!animation) return; auto containerType = container_type_get(targetType); auto targetTrackType = track_type_get(targetType); auto container = element_child_first_get(*animation, containerType); if (!container) return; std::set targetIds{}; for (auto itemReference : targetReferences) targetIds.insert(itemReference.itemID); auto group = element_make(ElementType::GROUP); group.id = element_child_next_id_get(*container, ElementType::GROUP); group.name = groupName; group.isExpanded = true; int insertIndex = (int)container->children.size(); for (int i = 0; i < (int)container->children.size(); ++i) { auto& item = container->children[i]; if (item.type == targetTrackType && targetIds.contains(track_id_get(item, targetType))) { insertIndex = std::min(insertIndex, i); item.groupId = group.id; } } if (insertIndex == (int)container->children.size()) return; insertIndex = std::clamp(insertIndex, 0, (int)container->children.size()); container->children.insert(container->children.begin() + insertIndex, group); document.items.references.clear(); for (auto itemReference : targetReferences) document.items.references.insert(itemReference); document.reference = targetReferences.front(); frames_selection_reset_for(document); }); }; auto rows_move_to_row = [&](std::vector draggedRows, TimelineItemRow targetRow, bool isDropAfter, bool isDropIntoGroup = false) { if (draggedRows.empty()) return; auto targetType = draggedRows.front().type; if (targetType != LAYER && targetType != NULL_) return; for (const auto& draggedRow : draggedRows) if (draggedRow.type != targetType) return; if (targetRow.isGroup && targetRow.type != targetType) return; if (!targetRow.isGroup && targetRow.type != targetType && targetRow.type != NONE && targetRow.type != ROOT && targetRow.type != TRIGGER) return; auto animationIndex = reference.animationIndex; edit_command_push(EDIT_MOVE_ITEMS, Document::ITEMS, [=, this](Manager&, Document& document) mutable { auto animation = command_animation_get(document, animationIndex); if (!animation) return; auto container = element_child_first_get(*animation, container_type_get(targetType)); if (!container) return; auto targetTrackType = track_type_get(targetType); std::set draggedIds{}; std::set draggedGroupIds{}; for (const auto& draggedRow : draggedRows) { if (draggedRow.isGroup) draggedGroupIds.insert(draggedRow.id); else draggedIds.insert(draggedRow.id); } if (targetRow.groupId != -1 && draggedGroupIds.contains(targetRow.groupId)) return; std::vector movedItems{}; for (const auto& item : container->children) { auto isDraggedGroup = item.type == ElementType::GROUP && draggedGroupIds.contains(item.id); auto isDraggedTrack = item.type == targetTrackType && (draggedIds.contains(track_id_get(item, targetType)) || draggedGroupIds.contains(item.groupId)); if (isDraggedGroup || isDraggedTrack) movedItems.push_back(item); } if (movedItems.empty()) return; if (targetRow.isGroup && draggedGroupIds.contains(targetRow.id)) return; auto row_index_get = [&](const TimelineItemRow& row) { for (int i = 0; i < (int)container->children.size(); ++i) { auto& item = container->children[i]; if (row.isGroup && item.type == ElementType::GROUP && item.id == row.id) return i; if (!row.isGroup && row.type == targetType && item.type == targetTrackType && track_id_get(item, targetType) == row.id) return i; } return -1; }; int targetIndex = (int)container->children.size(); if (targetRow.isGroup || targetRow.type == targetType) { auto rowIndex = row_index_get(targetRow); if (rowIndex == -1) return; targetIndex = rowIndex + (targetType == LAYER ? !isDropAfter : isDropAfter); } int removedBeforeTarget = 0; for (int i = (int)container->children.size() - 1; i >= 0; --i) { auto& item = container->children[i]; auto isDraggedGroup = item.type == ElementType::GROUP && draggedGroupIds.contains(item.id); auto isDraggedTrack = item.type == targetTrackType && (draggedIds.contains(track_id_get(item, targetType)) || draggedGroupIds.contains(item.groupId)); if (isDraggedGroup || isDraggedTrack) { if (i < targetIndex) ++removedBeforeTarget; container->children.erase(container->children.begin() + i); } } auto targetGroupId = -1; if (targetRow.isGroup && isDropIntoGroup) targetGroupId = targetRow.id; else if (targetRow.type == targetType) targetGroupId = targetRow.groupId; for (auto& item : movedItems) if (item.type == targetTrackType && !draggedGroupIds.contains(item.groupId)) item.groupId = targetGroupId; targetIndex -= removedBeforeTarget; targetIndex = std::clamp(targetIndex, 0, (int)container->children.size()); container->children.insert(container->children.begin() + targetIndex, movedItems.begin(), movedItems.end()); document.items.references.clear(); document.groupReferences.clear(); for (const auto& draggedRow : draggedRows) { if (draggedRow.isGroup) document.groupReferences.insert( {draggedRow.animationIndex, draggedRow.type, draggedRow.id}); else document.items.references.insert(row_item_reference_get(draggedRow)); } if (!document.items.references.empty()) document.reference = *document.items.references.begin(); else document.reference = {animationIndex}; frames_selection_reset_for(document); }); }; auto fit_animation_length = [&]() { if (!animation) return; auto animationIndex = reference.animationIndex; edit_command_push(EDIT_FIT_ANIMATION_LENGTH, Document::ANIMATIONS, [=](Manager&, Document& document) { auto animation = command_animation_get(document, animationIndex); if (!animation) return; animation->frameNum = animation_length_get(*animation); }); }; auto context_menu = [&]() { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); auto copy = [&]() { if (!animation) return; auto selectedFrames = frame_references_for_current_get(); if (selectedFrames.empty()) return; std::string clipboardString{}; for (auto frameReference : selectedFrames) { auto item = item_get(frameReference.itemType, frameReference.itemID); auto frame = item ? track_frame_get(*item, frameReference.frameIndex) : nullptr; if (!frame) continue; auto parentType = item_type_to_track_type_get(item_type_get(frameReference.itemType)); clipboardString += element_to_string(*frame, parentType); } if (!clipboardString.empty()) clipboard.set(clipboardString); }; auto cut = [&]() { copy(); auto selectedFrames = frame_references_for_current_get(); edit_command_push(EDIT_CUT_FRAMES, Document::FRAMES, [=](Manager&, Document& document) mutable { frames_delete_for(document, selectedFrames); }); }; auto paste = [&]() { if (clipboard.is_empty()) return; auto targetReference = reference; auto selectedFrames = frame_references_for_current_get(); auto clipboardString = clipboard.get(); auto targetHoveredTime = hoveredTime; auto message = std::string(localize.get(EDIT_PASTE_FRAMES)); command_push([=](Manager&, Document& document) mutable { document.snapshot(message); auto animation = command_animation_get(document, targetReference.animationIndex); if (!animation) return; if (auto item = command_item_get(document, targetReference.animationIndex, targetReference.itemType, targetReference.itemID)) { std::set indices{}; std::string errorString{}; int insertIndex = (int)item->children.size(); std::set selectedIndices{}; for (auto frameReference : selectedFrames) if (is_same_item(frameReference, targetReference)) selectedIndices.insert(frameReference.frameIndex); if (!selectedIndices.empty()) insertIndex = std::min((int)item->children.size(), *selectedIndices.rbegin() + 1); else if (targetReference.frameIndex >= 0 && targetReference.frameIndex < (int)item->children.size()) insertIndex = targetReference.frameIndex + 1; auto start = targetReference.itemType == TRIGGER ? targetHoveredTime : insertIndex; if (frames_deserialize(*item, clipboardString, start, indices, &errorString)) { if (targetReference.itemType != LAYER) { for (auto i : indices) if (auto frame = track_frame_get(*item, i); frame) { frame->regionId = -1; frame->crop = {}; frame->size = {}; frame->pivot = {}; } } else if (targetReference.itemID != -1) { auto layer = command_layer_get(document, targetReference.itemID); auto spritesheet = layer ? command_spritesheet_get(document, layer->spritesheetId) : nullptr; for (auto i : indices) { auto frame = track_frame_get(*item, i); if (!frame || frame->regionId == -1) continue; auto region = spritesheet ? element_child_id_get(*spritesheet, ElementType::REGION, frame->regionId) : nullptr; if (!region) frame->regionId = -1; } } document.reference = targetReference; document.frames.selection.clear(); document.frames.references.clear(); for (auto i : indices) { document.frames.selection.insert(i); document.frames.references.insert( {targetReference.animationIndex, targetReference.itemType, targetReference.itemID, i}); } document.reference.frameIndex = *indices.begin(); document.anm2_change(Document::FRAMES); } else { toasts.push(std::format("{} {}", localize.get(TOAST_DESERIALIZE_FRAMES_FAILED), errorString)); logger.error(std::format("{} {}", localize.get(TOAST_DESERIALIZE_FRAMES_FAILED, anm2ed::ENGLISH), errorString)); } } else { toasts.push(localize.get(TOAST_DESERIALIZE_FRAMES_NO_SELECTION)); logger.warning(localize.get(TOAST_DESERIALIZE_FRAMES_NO_SELECTION, anm2ed::ENGLISH)); } }); }; 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 (shortcut(manager.chords[SHORTCUT_SPLIT], shortcut::FOCUSED)) frame_split(); if (shortcut(manager.chords[SHORTCUT_BAKE], shortcut::FOCUSED)) frames_bake(); if (shortcut(manager.chords[SHORTCUT_FIT], shortcut::FOCUSED)) fit_animation_length(); auto make_region = [&]() { auto targetReference = reference; auto frame = frame_get(); if (!frame || targetReference.itemType != LAYER || targetReference.itemID == -1) return; if (frame->regionId != -1) return; auto layer = layer_get(targetReference.itemID); if (!layer) return; auto spritesheetID = layer->spritesheetId; if (!spritesheet_get(spritesheetID)) return; auto settingsPtr = &settings; command_push([=](Manager& manager, Document& document) { auto frame = command_frame_get(document, targetReference); if (!frame || frame->regionId != -1) return; auto layer = command_layer_get(document, targetReference.itemID); if (!layer) return; auto spritesheetID = layer->spritesheetId; if (!command_spritesheet_get(document, spritesheetID)) return; auto region = element_make(ElementType::REGION); region.crop = frame->crop; region.size = frame->size; region.pivot = frame->pivot; region.origin = Origin::CUSTOM; document.spritesheet.reference = spritesheetID; settingsPtr->windowIsRegions = true; manager.makeRegionSpritesheetId = spritesheetID; manager.makeRegion = region; manager.isMakeRegionRequested = true; }); }; auto item = item_get(reference.itemType, reference.itemID); auto frame = frame_get(); auto selectedFrames = frame_references_for_current_get(); auto selectedBakeFrames = selectedFrames; std::erase_if(selectedBakeFrames, [](const Reference& frameReference) { return frameReference.itemType == TRIGGER; }); bool isMakeRegion = frame && reference.itemType == LAYER && reference.itemID != -1 && frame->regionId == -1 && layer_get(reference.itemID) && spritesheet_get(layer_get(reference.itemID)->spritesheetId); Actions actions{}; actions_undo_redo_add(actions, manager, document); actions.separator(); actions.add({.label = playback.isPlaying ? LABEL_PAUSE : LABEL_PLAY, .shortcut = SHORTCUT_PLAY_PAUSE, .isEnabled = []() { return true; }, .run = [&]() { playback.toggle(); }}); actions.add({.label = LABEL_INSERT, .shortcut = SHORTCUT_INSERT_FRAME, .isEnabled = [&]() { return item; }, .run = [&]() { frame_insert(); }}); actions.add({.label = LABEL_DELETE, .shortcut = SHORTCUT_REMOVE, .isEnabled = [=]() { return !selectedFrames.empty(); }, .run = [&]() { frames_delete_action(); }}); actions.add({.label = LABEL_BAKE, .shortcut = SHORTCUT_BAKE, .isEnabled = [=]() { return !selectedBakeFrames.empty(); }, .run = [&]() { frames_bake(); }}); actions.add({.label = LABEL_FIT_ANIMATION_LENGTH, .shortcut = SHORTCUT_FIT, .isEnabled = [&]() { return animation && animation->frameNum != animation_length_get(*animation); }, .run = [&]() { fit_animation_length(); }}); actions.add({.label = LABEL_SPLIT, .shortcut = SHORTCUT_SPLIT, .isEnabled = [=]() { return selectedBakeFrames.size() == 1; }, .run = [&]() { frame_split(); }}); actions.add({.label = LABEL_MAKE_REGION, .shortcut = -1, .isEnabled = [&]() { return isMakeRegion; }, .run = [&]() { make_region(); }}); actions.separator(); actions.add(ACTION_CUT, [=]() { return !selectedFrames.empty(); }, cut); actions.add(ACTION_COPY, [=]() { return !selectedFrames.empty(); }, copy); actions.add(ACTION_PASTE, [&]() { return !clipboard.is_empty(); }, paste); actions_context_window_draw("##Context Menu", actions, settings); ImGui::PopStyleVar(2); }; auto item_base_properties_open = [&](int type, int id) { switch (type) { case LAYER: manager.layer_properties_open(id); break; case NULL_: manager.null_properties_open(id); default: break; }; }; auto group_properties_close = [&]() { groupName.clear(); groupAnimationIndex = -1; groupType = NONE; groupId = -1; groupPropertiesPopup.close(); }; auto group_properties_open = [&](const TimelineItemRow& row, const Element& group) { groupName = group.name.empty() ? std::string(localize.get(TEXT_NEW_GROUP)) : group.name; groupAnimationIndex = reference.animationIndex; groupType = row.type; groupId = row.id; groupPropertiesPopup.open(); }; auto group_properties_update = [&]() { groupPropertiesPopup.trigger(); if (ImGui::BeginPopupModal(groupPropertiesPopup.label(), &groupPropertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) { auto childSize = child_size_get(1); if (ImGui::BeginChild("##Group Properties Child", childSize, ImGuiChildFlags_Borders)) { if (groupPropertiesPopup.isJustOpened) ImGui::SetKeyboardFocusHere(); input_text_string(localize.get(BASIC_NAME), &groupName); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ITEM_NAME)); } ImGui::EndChild(); auto widgetSize = widget_size_with_row_get(2); shortcut(manager.chords[SHORTCUT_CONFIRM]); if (ImGui::Button(localize.get(BASIC_CONFIRM), widgetSize)) { auto targetName = groupName; auto targetAnimationIndex = groupAnimationIndex; auto targetType = groupType; auto targetId = groupId; edit_command_push(EDIT_RENAME_GROUP, Document::ITEMS, [=](Manager&, Document& document) { auto animation = command_animation_get(document, targetAnimationIndex); auto container = animation ? element_child_first_get(*animation, container_type_get(targetType)) : nullptr; auto group = container ? element_child_id_get(*container, ElementType::GROUP, targetId) : nullptr; if (!group) return; group->name = targetName; }); group_properties_close(); } ImGui::SameLine(); shortcut(manager.chords[SHORTCUT_CANCEL]); if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) group_properties_close(); ImGui::EndPopup(); } groupPropertiesPopup.end(); }; auto item_context_menu = [&]() { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); auto& type = reference.itemType; auto& id = reference.itemID; auto item = selected_item_get(); auto selectedItems = item_references_for_current_get(); auto selectedRows = selected_row_references_get(); auto selectedGroupableItems = item_references_groupable_get(); TimelineItemRow selectedGroupRow{}; Element* selectedGroup{}; if (selectedRows.size() == 1 && selectedRows.front().isGroup) { auto row = selectedRows.front(); selectedGroupRow = {.type = row.type, .id = row.id, .index = row.index, .isGroup = true}; selectedGroup = row_group_get(selectedGroupRow); } auto isRemoveAvailable = std::ranges::any_of(selectedRows, [](const TimelineRowReference& row) { return row.type == LAYER || row.type == NULL_; }); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow) && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) ImGui::OpenPopup("##Items Context Menu"); Actions actions{}; actions_undo_redo_add(actions, manager, document); actions.separator(); actions.add(ACTION_PROPERTIES, [&]() { return selectedGroup || (item && (type == LAYER || type == NULL_)); }, [&]() { if (selectedGroup) group_properties_open(selectedGroupRow, *selectedGroup); else item_base_properties_open(type, id); }); actions.add(ACTION_ADD, [&]() { return animation; }, [&]() { itemProperties.open(); }); actions.add(ACTION_REMOVE, [=]() { return isRemoveAvailable; }, [&]() { item_remove(); }); actions.add(ACTION_GROUP, [=]() { return !selectedGroupableItems.empty(); }, [&]() { item_group(); }); actions_popup_draw("##Items Context Menu", actions, settings); ImGui::PopStyleVar(2); }; auto item_child = [&](const TimelineItemRow& row, int index) { ImGui::PushID(index); auto type = row.type; auto id = row.id; if (row.isGroup) { auto group = row_group_get(row); if (!group) { ImGui::PopID(); return; } auto label = group->name.empty() ? std::string(localize.get(TEXT_NEW_GROUP)) : group->name; auto itemSize = ImVec2(ImGui::GetContentRegionAvail().x, rowFrameChildHeight); auto isGroupVisible = group->isVisible; auto colorVec = item_color_vec(type); if (is_group_selected(row)) { if (isLightTheme) colorVec = ITEM_COLOR_LIGHT_SELECTED[type_index(type)]; else colorVec = item_color_active_vec(type); } auto color = to_imvec4(isGroupVisible ? colorVec : colorVec * COLOR_HIDDEN_MULTIPLIER); ImGui::PushStyleColor(ImGuiCol_ChildBg, color); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); if (ImGui::BeginChild("##Group Child", itemSize, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollWithMouse)) { auto cursorPos = ImGui::GetCursorPos(); auto groupChildMin = ImGui::GetWindowPos(); auto groupChildMax = ImVec2(groupChildMin.x + ImGui::GetWindowSize().x, groupChildMin.y + ImGui::GetWindowSize().y); auto toggle_group = [&]() { auto targetRow = row; auto targetAnimationIndex = reference.animationIndex; edit_command_push(EDIT_TOGGLE_GROUP_EXPANDED, Document::ITEMS, [=](Manager&, Document& document) mutable { auto animation = command_animation_get(document, targetAnimationIndex); auto container = animation ? element_child_first_get(*animation, container_type_get(targetRow.type)) : nullptr; auto group = container ? element_child_id_get(*container, ElementType::GROUP, targetRow.id) : nullptr; if (!group) return; group->isExpanded = !group->isExpanded; }); }; ImGui::SetCursorPos(to_imvec2(to_vec2(cursorPos) - to_vec2(style.ItemSpacing))); ImGui::SetNextItemAllowOverlap(); ImGui::InvisibleButton("##Group Button", itemSize); auto groupButtonMin = ImGui::GetItemRectMin(); auto groupButtonMax = ImGui::GetItemRectMax(); auto mousePos = ImGui::GetIO().MousePos; auto is_mouse_in_rect = [](ImVec2 mouse, ImVec2 min, ImVec2 max) { return mouse.x >= min.x && mouse.x < max.x && mouse.y >= min.y && mouse.y < max.y; }; auto isGroupHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && is_mouse_in_rect(mousePos, groupButtonMin, groupButtonMax); bool isGroupTooltipDelayed{}; if (isGroupHovered) { auto& imguiStyle = ImGui::GetStyle(); auto previousTooltipDelay = imguiStyle.HoverDelayNormal; imguiStyle.HoverDelayNormal = FRAME_TOOLTIP_HOVER_DELAY; isGroupTooltipDelayed = ImGui::IsItemHovered(ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_NoSharedDelay); imguiStyle.HoverDelayNormal = previousTooltipDelay; } if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { rowDragReferences = row_drag_references_get(row); ImGui::SetDragDropPayload("Timeline Row Drag Drop", rowDragReferences.data(), (int)rowDragReferences.size() * (int)sizeof(TimelineRowReference)); row_drag_tooltip_draw(rowDragReferences); ImGui::EndDragDropSource(); } if (ImGui::BeginDragDropTarget()) { if (auto payload = ImGui::AcceptDragDropPayload( "Timeline Row Drag Drop", ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect)) { auto groupDropThird = (groupButtonMax.y - groupButtonMin.y) / 3.0f; auto dropZone = DropZone::BEFORE; if (mousePos.y >= groupButtonMax.y - groupDropThird) dropZone = DropZone::AFTER; else if (mousePos.y >= groupButtonMin.y + groupDropThird) dropZone = DropZone::INSIDE; auto isDropAfter = dropZone != DropZone::BEFORE; auto payloadRows = (TimelineRowReference*)payload->Data; auto payloadCount = payload->DataSize / sizeof(TimelineRowReference); std::vector draggedRows(payloadRows, payloadRows + payloadCount); auto isDropIntoGroup = dropZone == DropZone::INSIDE && (row.type == LAYER || row.type == NULL_); for (auto draggedRow : draggedRows) if (draggedRow.isGroup || draggedRow.type != row.type) isDropIntoGroup = false; if (isDropIntoGroup) drop_box_draw(ImGui::GetWindowDrawList(), groupChildMin, groupChildMax); else drop_line_draw(ImGui::GetWindowDrawList(), groupChildMin, groupChildMax, isDropAfter); if (payload->IsDelivery()) rows_move_to_row(draggedRows, row, isDropAfter, isDropIntoGroup); } ImGui::EndDragDropTarget(); } ImGui::SetCursorPos(cursorPos); auto folderIcon = group->isExpanded ? icon::FOLDER_OPEN : icon::FOLDER; ImGui::Image(resources.icons[folderIcon].id, icon_size_get()); auto iconMin = ImGui::GetItemRectMin(); auto iconMax = ImGui::GetItemRectMax(); overlay_icon(resources.icons[folderIcon].id, itemIconTint); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, itemTextColor); ImGui::TextUnformatted(label.c_str()); ImGui::PopStyleColor(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::SetCursorPos( ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); int visibleIcon = isGroupVisible ? icon::VISIBLE : icon::INVISIBLE; if (ImGui::ImageButton("##Group Visible Toggle", resources.icons[visibleIcon].id, icon_size_get())) { auto targetAnimationIndex = reference.animationIndex; auto targetType = type; auto targetGroupId = group->id; auto targetVisible = !isGroupVisible; edit_command_push(EDIT_TOGGLE_ITEM_VISIBILITY, Document::FRAMES, [=](Manager&, Document& document) { auto animation = command_animation_get(document, targetAnimationIndex); auto container = animation ? element_child_first_get(*animation, container_type_get(targetType)) : nullptr; if (!container) return; auto group = element_child_id_get(*container, ElementType::GROUP, targetGroupId); if (!group) return; group->isVisible = targetVisible; }); } auto visibleButtonMin = ImGui::GetItemRectMin(); auto visibleButtonMax = ImGui::GetItemRectMax(); overlay_icon(resources.icons[visibleIcon].id, itemIconTint); ImGui::SetItemTooltip("%s", isGroupVisible ? localize.get(TOOLTIP_ITEM_VISIBILITY_SHOWN) : localize.get(TOOLTIP_ITEM_VISIBILITY_HIDDEN)); ImGui::PopStyleVar(2); ImGui::PopStyleColor(3); auto isIconHovered = isGroupHovered && is_mouse_in_rect(mousePos, iconMin, iconMax); auto isVisibleButtonHovered = isGroupHovered && is_mouse_in_rect(mousePos, visibleButtonMin, visibleButtonMax); if (isGroupTooltipDelayed && !isVisibleButtonHovered) { ImGui::BeginTooltip(); ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(label.c_str()); ImGui::PopFont(); ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(group->id)).c_str()); auto groupItemsCount = group_items_count_get(type, group->id); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_ITEMS_COUNT), std::make_format_args(groupItemsCount)).c_str()); ImGui::EndTooltip(); } if (isIconHovered && ImGui::IsMouseReleased(ImGuiMouseButton_Left) && !ImGui::IsMouseDragPastThreshold(ImGuiMouseButton_Left)) toggle_group(); else if (isGroupHovered && !isIconHovered && !isVisibleButtonHovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) group_properties_open(row, *group); else if (isGroupHovered && !isIconHovered && !isVisibleButtonHovered && ImGui::IsMouseReleased(ImGuiMouseButton_Left) && !ImGui::IsMouseDragPastThreshold(ImGuiMouseButton_Left)) row_selection_set(row); } ImGui::EndChild(); ImGui::PopStyleVar(2); ImGui::PopStyleColor(); ImGui::PopID(); return; } auto item = item_get(type, id); if (type != NONE && !item) { ImGui::PopID(); return; } auto isItemVisible = item ? item->isVisible : false; auto isVisible = item ? item->isVisible && is_track_group_visible(type, row.groupId) : false; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; if (isOnlyShowLayers && type != LAYER) isVisible = false; auto isReferenced = reference.itemType == type && reference.itemID == id; auto isItemSelected = is_row_selected(row); auto label = [&]() -> std::string { if (type == LAYER) { auto layer = layer_get(id); if (!layer) return localize.get(TYPE_STRINGS[type]); return std::vformat(localize.get(FORMAT_LAYER), std::make_format_args(id, layer->name, layer->spritesheetId)); } if (type == NULL_) { auto null = null_get(id); if (!null) return localize.get(TYPE_STRINGS[type]); return std::vformat(localize.get(FORMAT_NULL), std::make_format_args(id, null->name)); } return localize.get(TYPE_STRINGS[type]); }(); auto icon = TYPE_ICONS[type]; auto iconTintCurrent = isLightTheme && type == NONE ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : itemIconTint; auto baseColorVec = item_color_vec(type); auto activeColorVec = item_color_active_vec(type); auto colorVec = baseColorVec; if (isItemSelected && type != NONE) { if (isLightTheme) colorVec = ITEM_COLOR_LIGHT_SELECTED[type_index(type)]; else colorVec = activeColorVec; } auto color = to_imvec4(colorVec); color = !isVisible ? to_imvec4(colorVec * COLOR_HIDDEN_MULTIPLIER) : color; ImGui::PushStyleColor(ImGuiCol_ChildBg, color); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); auto itemSize = ImVec2(ImGui::GetContentRegionAvail().x, rowFrameChildHeight); if (ImGui::BeginChild(label.c_str(), itemSize, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollWithMouse)) { auto cursorPos = ImGui::GetCursorPos(); auto itemChildMin = ImGui::GetWindowPos(); auto itemChildMax = ImVec2(itemChildMin.x + ImGui::GetWindowSize().x, itemChildMin.y + ImGui::GetWindowSize().y); if (type != NONE) { ImGui::SetCursorPos(to_imvec2(to_vec2(cursorPos) - to_vec2(style.ItemSpacing))); ImGui::SetNextItemAllowOverlap(); ImGui::SetNextItemStorageID(id); ImGui::InvisibleButton("##Item Button", itemSize); auto itemButtonMin = ImGui::GetItemRectMin(); auto itemButtonMax = ImGui::GetItemRectMax(); auto isItemButtonHovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); if (type == LAYER || type == NULL_) { if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { rowDragReferences = row_drag_references_get(row); ImGui::SetDragDropPayload("Timeline Row Drag Drop", rowDragReferences.data(), (int)rowDragReferences.size() * (int)sizeof(TimelineRowReference)); row_drag_tooltip_draw(rowDragReferences); ImGui::EndDragDropSource(); } if (ImGui::BeginDragDropTarget()) { if (auto payload = ImGui::AcceptDragDropPayload( "Timeline Row Drag Drop", ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect)) { auto isDropAfter = is_drop_after(itemButtonMin, itemButtonMax); drop_line_draw(ImGui::GetWindowDrawList(), itemChildMin, itemChildMax, isDropAfter); auto payloadRows = (TimelineRowReference*)payload->Data; auto payloadCount = payload->DataSize / sizeof(TimelineRowReference); std::vector draggedRows(payloadRows, payloadRows + payloadCount); if (payload->IsDelivery()) rows_move_to_row(draggedRows, row, isDropAfter); } ImGui::EndDragDropTarget(); } } if (isItemButtonHovered) { if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) item_base_properties_open(type, id); else if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) && !ImGui::IsMouseDragPastThreshold(ImGuiMouseButton_Left)) row_selection_set(row); auto& imguiStyle = ImGui::GetStyle(); auto previousTooltipFlags = imguiStyle.HoverFlagsForTooltipMouse; auto previousTooltipDelay = imguiStyle.HoverDelayNormal; imguiStyle.HoverFlagsForTooltipMouse = ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_NoSharedDelay; imguiStyle.HoverDelayNormal = FRAME_TOOLTIP_HOVER_DELAY; bool showItemTooltip = ImGui::BeginItemTooltip(); imguiStyle.HoverFlagsForTooltipMouse = previousTooltipFlags; imguiStyle.HoverDelayNormal = previousTooltipDelay; if (showItemTooltip) { auto yesNoLabel = [&](bool value) { return value ? localize.get(BASIC_YES) : localize.get(BASIC_NO); }; auto visibleLabel = yesNoLabel(isVisible); auto framesCount = item_frames_count(item); switch (type) { case ROOT: { ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(localize.get(BASIC_ROOT)); ImGui::PopFont(); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_VISIBLE), std::make_format_args(visibleLabel)).c_str()); auto transformLabel = yesNoLabel(settings.previewIsRootTransform); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_TRANSFORM), std::make_format_args(transformLabel)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_FRAMES_COUNT), std::make_format_args(framesCount)).c_str()); break; } case LAYER: { auto layer = layer_get(id); if (!layer) break; ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(layer->name.c_str()); ImGui::PopFont(); ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_SPRITESHEET_ID), std::make_format_args(layer->spritesheetId)) .c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_VISIBLE), std::make_format_args(visibleLabel)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_FRAMES_COUNT), std::make_format_args(framesCount)).c_str()); break; } case NULL_: { auto nullInfo = null_get(id); if (!nullInfo) break; ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(nullInfo->name.c_str()); ImGui::PopFont(); auto rectLabel = yesNoLabel(nullInfo->isShowRect); ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_ID), std::make_format_args(id)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_RECT), std::make_format_args(rectLabel)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_VISIBLE), std::make_format_args(visibleLabel)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_FRAMES_COUNT), std::make_format_args(framesCount)).c_str()); break; } case TRIGGER: { ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE); ImGui::TextUnformatted(localize.get(BASIC_TRIGGERS)); ImGui::PopFont(); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_VISIBLE), std::make_format_args(visibleLabel)).c_str()); ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_TRIGGERS_COUNT), std::make_format_args(framesCount)).c_str()); break; } default: break; } ImGui::EndTooltip(); } } auto contentCursorPos = cursorPos; contentCursorPos.x += (float)row.depth * ImGui::GetTextLineHeightWithSpacing(); ImGui::SetCursorPos(contentCursorPos); ImGui::Image(resources.icons[icon].id, icon_size_get()); overlay_icon(resources.icons[icon].id, iconTintCurrent); ImGui::SameLine(); if (isReferenced) ImGui::PushFont(resources.fonts[font::ITALICS].get(), font::SIZE); ImGui::PushStyleColor(ImGuiCol_Text, itemTextColor); ImGui::TextUnformatted(label.c_str()); ImGui::PopStyleColor(); if (isReferenced) ImGui::PopFont(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::SetCursorPos( ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); int visibleIcon = isItemVisible ? icon::VISIBLE : icon::INVISIBLE; if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, icon_size_get())) { auto animationIndex = reference.animationIndex; auto targetType = type; auto targetID = id; edit_command_push(EDIT_TOGGLE_ITEM_VISIBILITY, Document::FRAMES, [=](Manager&, Document& document) { auto item = command_item_get(document, animationIndex, targetType, targetID); if (!item) return; item->isVisible = !item->isVisible; }); } overlay_icon(resources.icons[visibleIcon].id, iconTintCurrent); ImGui::SetItemTooltip("%s", isItemVisible ? localize.get(TOOLTIP_ITEM_VISIBILITY_SHOWN) : localize.get(TOOLTIP_ITEM_VISIBILITY_HIDDEN)); if (type == NULL_) { if (auto null = null_get(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, (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); if (ImGui::ImageButton("##Rect Toggle", resources.icons[rectIcon].id, icon_size_get())) { auto nullID = id; edit_command_push(EDIT_TOGGLE_NULL_RECT, Document::FRAMES, [=](Manager&, Document& document) { auto nulls = document.anm2.element_get(ElementType::NULLS); auto null = nulls ? element_child_id_get(*nulls, ElementType::NULL_ELEMENT, nullID) : nullptr; if (!null) return; null->isShowRect = !null->isShowRect; }); } overlay_icon(resources.icons[rectIcon].id, iconTintCurrent); ImGui::SetItemTooltip("%s", isShowRect ? localize.get(TOOLTIP_NULL_RECT_SHOWN) : localize.get(TOOLTIP_NULL_RECT_HIDDEN)); } } ImGui::PopStyleVar(2); ImGui::PopStyleColor(3); } else { auto cursorPos = ImGui::GetCursorPos(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); 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; overlay_icon(resources.icons[unusedIcon].id, iconTintCurrent); ImGui::SetItemTooltip("%s", isShowUnused ? localize.get(TOOLTIP_UNUSED_ITEMS_SHOWN) : localize.get(TOOLTIP_UNUSED_ITEMS_HIDDEN)); 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; overlay_icon(resources.icons[layersIcon].id, iconTintCurrent); ImGui::SetItemTooltip("%s", showLayersOnly ? localize.get(TOOLTIP_ONLY_LAYERS_VISIBLE) : localize.get(TOOLTIP_ALL_ITEMS_VISIBLE)); ImGui::PopStyleVar(2); ImGui::PopStyleColor(3); ImGui::SetCursorPos(cursorPos); ImGui::BeginDisabled(); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::Text("(?)"); ImGui::PopStyleColor(); auto tooltipShortcuts = std::vformat( localize.get(TOOLTIP_TIMELINE_SHORTCUTS), std::make_format_args(settings.shortcutMovePlayheadBack, settings.shortcutMovePlayheadForward, settings.shortcutShortenFrame, settings.shortcutExtendFrame, settings.shortcutPreviousFrame, settings.shortcutNextFrame, settings.shortcutPreviousItem, settings.shortcutNextItem)); ImGui::SetItemTooltip("%s", tooltipShortcuts.c_str()); ImGui::EndDisabled(); } } ImGui::EndChild(); ImGui::PopStyleColor(); ImGui::PopStyleVar(2); ImGui::PopID(); }; auto items_child = [&]() { auto itemsChildSize = ImVec2(ITEM_CHILD_WIDTH, ImGui::GetContentRegionAvail().y); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); bool isItemsChildOpen = ImGui::BeginChild("##Items Child", itemsChildSize, ImGuiChildFlags_Borders); ImGui::PopStyleVar(); if (isItemsChildOpen) { auto itemsListChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.y * 2); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); if (ImGui::BeginChild("##Items List Child", itemsListChildSize, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar)) { if (animation && ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused)) { auto rowReferences = timeline_row_references_get(); row_selection_clear(); for (auto rowReference : rowReferences) row_selection_insert(rowReference); if (!rowReferences.empty()) { rowSelectionAnchor = rowReferences.front(); isRowSelectionAnchorSet = true; } frames_selection_reset_for(document); } if (animation && shortcut(manager.chords[SHORTCUT_GROUP], shortcut::FOCUSED) && !item_references_groupable_get().empty()) item_group(); ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2()); ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 0.0f); if (ImGui::BeginTable("##Item Table", 1, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY)) { ImGui::GetCurrentWindow()->Flags |= ImGuiWindowFlags_NoScrollWithMouse; ImGui::SetScrollY(scroll.y); ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableSetupColumn("##Items"); auto item_child_row = [&](const TimelineItemRow& row, int index) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); item_child(row, index); }; int index{}; item_child_row({.type = NONE}, index++); if (animation) for (const auto& row : timeline_item_rows_get()) item_child_row(row, index++); if (isHorizontalScroll && ImGui::GetCurrentWindow()->ScrollbarY) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Dummy(ImVec2(0, style.ScrollbarSize)); } ImGui::EndTable(); } ImGui::PopStyleVar(2); item_context_menu(); } ImGui::PopStyleVar(2); ImGui::EndChild(); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + style.WindowPadding.x, ImGui::GetCursorPosY())); auto widgetSize = widget_size_with_row_get(2, ImGui::GetContentRegionAvail().x - style.WindowPadding.x); ImGui::BeginDisabled(!animation); { shortcut(manager.chords[SHORTCUT_ADD]); if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) itemProperties.open(); set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_ITEM), settings.shortcutAdd); ImGui::SameLine(); auto selectedRows = selected_row_references_get(); auto isRemoveAvailable = std::ranges::any_of(selectedRows, [](const TimelineRowReference& row) { return row.type == LAYER || row.type == NULL_; }); ImGui::BeginDisabled(!isRemoveAvailable); shortcut(manager.chords[SHORTCUT_REMOVE]); if (ImGui::Button(localize.get(BASIC_REMOVE), widgetSize)) item_remove(); set_item_tooltip_shortcut(localize.get(TOOLTIP_REMOVE_ITEMS), settings.shortcutRemove); ImGui::EndDisabled(); } ImGui::EndDisabled(); ImGui::PopStyleVar(); } ImGui::EndChild(); }; int frameMoveDropType = NONE; int frameMoveDropItemID = -1; int frameMoveDropIndex = -1; bool isFrameMoveDropTarget = false; auto frame_move_drag_clear = [&]() { frameMoveDrag = {}; frameSelectionLocked.clear(); }; auto frames_move_to = [&](int targetType, int targetID, int insertIndex) { if (!frameMoveDrag.isActive || !animation || frameMoveDrag.animationIndex != reference.animationIndex) return; if (targetType == TRIGGER) return; auto drag = frameMoveDrag; std::erase_if(drag.references, [](const Reference& frameReference) { return frameReference.itemType == TRIGGER || frameReference.frameIndex < 0; }); if (drag.references.empty() && drag.frameIndex >= 0) drag.references.push_back({drag.animationIndex, drag.type, drag.itemID, drag.frameIndex}); std::erase_if(drag.references, [](const Reference& frameReference) { return frameReference.itemType == TRIGGER || frameReference.frameIndex < 0; }); if (drag.references.empty()) return; edit_command_push(EDIT_MOVE_FRAMES, Document::FRAMES, [=, this](Manager&, Document& document) mutable { auto targetItem = command_item_get(document, drag.animationIndex, targetType, targetID); if (!targetItem) return; std::map> groupedFrames{}; for (auto frameReference : drag.references) { auto itemReference = item_reference_from_frame_get(frameReference); groupedFrames[itemReference].insert(frameReference.frameIndex); } int removedBeforeTarget = 0; std::vector movedFrames; for (auto& [itemReference, indices] : groupedFrames) { auto sourceItem = command_item_get(document, itemReference.animationIndex, itemReference.itemType, itemReference.itemID); if (!sourceItem) continue; for (auto i : indices) { if (i < 0 || i >= (int)sourceItem->children.size()) continue; movedFrames.push_back(std::move(sourceItem->children[i])); if (itemReference.itemType == targetType && itemReference.itemID == targetID && i < insertIndex) ++removedBeforeTarget; } for (auto it = indices.rbegin(); it != indices.rend(); ++it) { auto i = *it; if (i >= 0 && i < (int)sourceItem->children.size()) sourceItem->children.erase(sourceItem->children.begin() + i); } } if (movedFrames.empty()) return; int desired = std::clamp(insertIndex, 0, (int)targetItem->children.size()); desired -= removedBeforeTarget; desired = std::clamp(desired, 0, (int)targetItem->children.size()); auto insertPosResult = desired; auto insertedCount = (int)movedFrames.size(); targetItem->children.insert(targetItem->children.begin() + insertPosResult, std::make_move_iterator(movedFrames.begin()), std::make_move_iterator(movedFrames.end())); if (insertedCount <= 0) return; document.frames.selection.clear(); document.frames.references.clear(); for (int offset = 0; offset < insertedCount; ++offset) { document.frames.selection.insert(insertPosResult + offset); document.frames.references.insert( {drag.animationIndex, targetType, targetID, insertPosResult + offset}); } document.reference = {drag.animationIndex, targetType, targetID, insertPosResult}; document.frameTime = frame_time_from_index_get(*targetItem, document.reference.frameIndex); frameSelectionSnapshot.assign(document.frames.selection.begin(), document.frames.selection.end()); frameSelectionSnapshotReference = document.reference; frameSelectionLocked.clear(); isFrameSelectionLocked = false; frameFocusIndex = document.reference.frameIndex; frameFocusRequested = true; if (targetType == LAYER) if (auto layer = command_layer_get(document, targetID)) document.spritesheet.reference = layer->spritesheetId; }); }; float playheadLineCenterX{}; float playheadLineTopY{}; bool isPlayheadLineSet{}; ImVec2 frameBoxClipMin{}; ImVec2 frameBoxClipMax{}; bool isFrameBoxClipSet{}; auto frame_box_content_point_get = [&]() { auto mousePos = ImGui::GetIO().MousePos; return ImVec2(mousePos.x + scroll.x - frameBoxClipMin.x, mousePos.y + scroll.y - frameBoxClipMin.y); }; auto frame_box_screen_point_get = [&](ImVec2 point) { return ImVec2(point.x - scroll.x + frameBoxClipMin.x, point.y - scroll.y + frameBoxClipMin.y); }; auto is_frame_box_overlapping = [](ImVec2 leftMin, ImVec2 leftMax, ImVec2 rightMin, ImVec2 rightMax) { return leftMin.x <= rightMax.x && leftMax.x >= rightMin.x && leftMin.y <= rightMax.y && leftMax.y >= rightMin.y; }; auto frame_overlay_draw = [&](ImDrawList* drawList, ImVec2 clipMin, ImVec2 clipMax) { if (isFrameBoxSelecting && isFrameBoxClipSet) { auto boxContentMin = ImVec2(std::min(frameBoxStart.x, frameBoxEnd.x), std::min(frameBoxStart.y, frameBoxEnd.y)); auto boxContentMax = ImVec2(std::max(frameBoxStart.x, frameBoxEnd.x), std::max(frameBoxStart.y, frameBoxEnd.y)); auto boxMin = frame_box_screen_point_get(boxContentMin); auto boxMax = frame_box_screen_point_get(boxContentMax); auto boxClipMin = clipMin; if (isPlayheadLineSet) boxClipMin.y = std::max(boxClipMin.y, playheadLineTopY); drawList->PushClipRect(boxClipMin, clipMax, true); drawList->AddRectFilled(boxMin, boxMax, ImGui::GetColorU32(ImGuiCol_DragDropTargetBg)); drawList->AddRect(boxMin, boxMax, ImGui::GetColorU32(ImGuiCol_DragDropTarget)); drawList->PopClipRect(); } if (isPlayheadLineSet) { auto lineTopY = std::max(clipMin.y, playheadLineTopY); if (clipMax.y > lineTopY) { auto linePos = ImVec2(playheadLineCenterX - (PLAYHEAD_LINE_THICKNESS * 0.5f), lineTopY); auto lineSize = ImVec2(PLAYHEAD_LINE_THICKNESS * 0.5f, clipMax.y - lineTopY); drawList->PushClipRect(clipMin, clipMax, true); drawList->AddRectFilled(linePos, ImVec2(linePos.x + lineSize.x, linePos.y + lineSize.y), ImGui::GetColorU32(playheadLineColor)); drawList->PopClipRect(); } } }; auto frame_child = [&](const TimelineItemRow& row, int& index, float width) { auto type = row.type; auto id = row.id; auto childSize = ImVec2(width, rowFrameChildHeight); if (row.isGroup) { auto group = row_group_get(row); if (!group) return; ImGui::PushID(index); ImGui::BeginChild("##Frames Group Child", childSize, ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar); ImGui::EndChild(); index++; ImGui::PopID(); return; } auto item = item_get(type, id); if (type != NONE && !item) return; auto isVisible = item ? item->isVisible && is_track_group_visible(type, row.groupId) : false; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; if (isOnlyShowLayers && type != LAYER) isVisible = false; auto colorVec = type_color_base_vec(type); auto colorActiveVec = type_color_active_vec(type); auto colorHoveredVec = type_color_hovered_vec(type); auto color = to_imvec4(colorVec); auto colorActive = to_imvec4(colorActiveVec); auto colorHovered = to_imvec4(colorHoveredVec); auto colorHidden = to_imvec4(colorVec * COLOR_HIDDEN_MULTIPLIER); auto colorActiveHidden = to_imvec4(colorActiveVec * COLOR_HIDDEN_MULTIPLIER); auto colorHoveredHidden = to_imvec4(colorHoveredVec * COLOR_HIDDEN_MULTIPLIER); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::PopStyleVar(2); ImGui::PushID(index); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); bool isDefaultChild = type == NONE; if (isLightTheme && isDefaultChild) ImGui::PushStyleColor(ImGuiCol_ChildBg, TIMELINE_CHILD_BG_COLOR_LIGHT); bool isFramesChildVisible = ImGui::BeginChild("##Frames Child", childSize, ImGuiChildFlags_Borders); auto drawList = ImGui::GetWindowDrawList(); auto clipMax = drawList->GetClipRectMax(); auto length = animation ? animation->frameNum : 0; auto frameSize = ImVec2(ImGui::GetTextLineHeight(), ImGui::GetContentRegionAvail().y); auto framesSize = ImVec2(frameSize.x * length, frameSize.y); auto cursorPos = ImGui::GetCursorPos(); auto cursorScreenPos = ImGui::GetCursorScreenPos(); auto border = glm::max(0.5f, ImGui::GetStyle().FrameBorderSize * 0.5f); auto borderLineLength = frameSize.y / 5; auto frameMin = std::max(0, (int)std::floor(scroll.x / frameSize.x) - 1); auto frameMax = std::min(FRAME_NUM_MAX, (int)std::ceil((scroll.x + clipMax.x) / frameSize.x) + 1); if (isFrameBoxSelecting && isFrameBoxClipSet && type != NONE && animation && item) { auto boxMin = ImVec2(std::min(frameBoxStart.x, frameBoxEnd.x), std::min(frameBoxStart.y, frameBoxEnd.y)); auto boxMax = ImVec2(std::max(frameBoxStart.x, frameBoxEnd.x), std::max(frameBoxStart.y, frameBoxEnd.y)); auto rowMinY = cursorScreenPos.y + scroll.y - frameBoxClipMin.y; auto rowMaxY = rowMinY + childSize.y; float selectionFrameTime{}; for (auto [i, frame] : std::views::enumerate(item->children)) { auto frameReference = Reference{reference.animationIndex, type, id, (int)i}; auto frameStart = type == TRIGGER ? frame.atFrame : selectionFrameTime; auto frameEnd = type == TRIGGER ? frameStart + 1.0f : frameStart + frame.duration; auto frameContentMin = ImVec2(frameStart * frameSize.x, rowMinY); auto frameContentMax = ImVec2(frameEnd * frameSize.x, rowMaxY); if (is_frame_box_overlapping(frameContentMin, frameContentMax, boxMin, boxMax)) frameBoxSelection.insert(frameReference); if (type != TRIGGER) selectionFrameTime += frame.duration; } } if (isFramesChildVisible) { if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && ImGui::Shortcut(ImGuiKey_Escape, ImGuiInputFlags_RouteFocused)) { if (!frames.references.empty() || !frames.selection.empty()) { reference.frameIndex = -1; frames_selection_reset_for(document); } else if (reference.itemType != NONE || reference.itemID != -1) reference_clear(); } if (type == NONE) { if (length > 0) { if (isLightTheme) { auto totalMax = ImVec2(cursorScreenPos.x + framesSize.x, cursorScreenPos.y + framesSize.y); drawList->AddRectFilled(cursorScreenPos, totalMax, ImGui::GetColorU32(timelineBackgroundColor)); float animationWidth = std::min(framesSize.x, frameSize.x * (float)length); drawList->AddRectFilled(cursorScreenPos, ImVec2(cursorScreenPos.x + animationWidth, cursorScreenPos.y + framesSize.y), ImGui::GetColorU32(timelinePlayheadRectColor)); } else { drawList->AddRectFilled(cursorScreenPos, ImVec2(cursorScreenPos.x + framesSize.x, cursorScreenPos.y + framesSize.y), ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_Header))); } } for (int i = frameMin; i < frameMax; i++) { auto frameScreenPos = ImVec2(cursorScreenPos.x + frameSize.x * (float)i, cursorScreenPos.y); drawList->AddRect(frameScreenPos, ImVec2(frameScreenPos.x + border, frameScreenPos.y + borderLineLength), ImGui::GetColorU32(timelineTickColor), 0, 0, 0.5f); drawList->AddRect(ImVec2(frameScreenPos.x, frameScreenPos.y + frameSize.y - borderLineLength), ImVec2(frameScreenPos.x + border, frameScreenPos.y + frameSize.y), ImGui::GetColorU32(timelineTickColor), 0, 0, 0.5); if (i % FRAME_MULTIPLE == 0) { auto string = std::to_string(i); auto textSize = ImGui::CalcTextSize(string.c_str()); auto textPos = ImVec2(frameScreenPos.x + (frameSize.x - textSize.x) / 2, frameScreenPos.y + (frameSize.y - textSize.y) / 2); drawList->AddRectFilled(frameScreenPos, ImVec2(frameScreenPos.x + frameSize.x, frameScreenPos.y + frameSize.y), ImGui::GetColorU32(frameMultipleOverlayColor)); drawList->AddText(textPos, ImGui::GetColorU32(textMultipleColor), string.c_str()); } } if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { playback_stop(); 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) { playback.time = hoveredTime; playback.clamp(settings.playbackIsClamp ? length : FRAME_NUM_MAX); document.frameTime = playback.time; } if (!playback.isPlaying) playback.clamp(settings.playbackIsClamp ? length : FRAME_NUM_MAX); if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) isDragging = false; if (length > 0) { ImGui::SetCursorPos(ImVec2(cursorPos.x + frameSize.x * floorf(playback.time), cursorPos.y)); ImGui::Image(resources.icons[icon::PLAYHEAD].id, frameSize); auto playheadMin = ImGui::GetItemRectMin(); auto playheadMax = ImGui::GetItemRectMax(); playheadLineCenterX = (playheadMin.x + playheadMax.x) * 0.5f; playheadLineTopY = playheadMax.y; isPlayheadLineSet = true; overlay_icon(resources.icons[icon::PLAYHEAD].id, playheadIconTint, true); } } else if (animation) { float frameTime{}; 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(frameBorderColor)); if (i % FRAME_MULTIPLE == 0) drawList->AddRectFilled(frameScreenPos, frameRectMax, ImGui::GetColorU32(frameMultipleOverlayColor)); } if (!frameMoveDrag.isActive && !ImGui::IsKeyDown(ImGuiMod_Shift) && ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGui::IsAnyItemHovered()) { isFrameBoxPending = true; isFrameBoxAdditive = ImGui::IsKeyDown(ImGuiMod_Ctrl); frameBoxStart = frame_box_content_point_get(); frameBoxEnd = frameBoxStart; frameBoxSelection.clear(); } bool isFrameMovePreview = false; ImVec2 frameMovePreviewMin{}; ImVec2 frameMovePreviewMax{}; bool isFrameMoveHoveredFrame = false; ImVec2 frameMoveHoveredFrameMin{}; ImVec2 frameMoveHoveredFrameMax{}; if (frameMoveDrag.isActive && type != TRIGGER) { auto mousePos = ImGui::GetIO().MousePos; auto rowMin = cursorScreenPos; auto rowMax = ImVec2(cursorScreenPos.x + width, cursorScreenPos.y + frameSize.y); if (mousePos.x >= rowMin.x && mousePos.x < rowMax.x && mousePos.y >= rowMin.y && mousePos.y < rowMax.y) { auto mouseX = mousePos.x - cursorScreenPos.x; auto targetTime = glm::max(0.0f, mouseX / frameSize.x); int dropIndex = (int)item->children.size(); float dropFrameTime{}; float frameTime{}; for (auto [i, frame] : std::views::enumerate(item->children)) { auto frameStart = frameTime; auto frameEnd = frameStart + frame.duration; if (!isFrameMoveHoveredFrame && targetTime >= frameStart && targetTime < frameEnd) { isFrameMoveHoveredFrame = true; frameMoveHoveredFrameMin = ImVec2(cursorScreenPos.x + frameStart * frameSize.x, cursorScreenPos.y); frameMoveHoveredFrameMax = ImVec2(cursorScreenPos.x + frameEnd * frameSize.x, cursorScreenPos.y + frameSize.y); } auto midpoint = frameStart + ((float)frame.duration * 0.5f); if (targetTime < midpoint) { dropIndex = (int)i; dropFrameTime = frameStart; break; } frameTime = frameEnd; dropFrameTime = frameTime; } frameMoveDropType = type; frameMoveDropItemID = id; frameMoveDropIndex = dropIndex; isFrameMoveDropTarget = true; auto dropX = cursorScreenPos.x + dropFrameTime * frameSize.x; auto previewWidth = glm::max(frameSize.x, (float)frameMoveDrag.duration * frameSize.x); frameMovePreviewMin = ImVec2(dropX, cursorScreenPos.y); frameMovePreviewMax = ImVec2(dropX + previewWidth, cursorScreenPos.y + frameSize.y); isFrameMovePreview = true; } } for (auto [i, frame] : std::views::enumerate(item->children)) { ImGui::PushID((int)i); auto frameReference = Reference{reference.animationIndex, type, id, (int)i}; auto isFrameVisible = isVisible && frame.isVisible; auto isReferenced = reference == frameReference; auto isSelected = frames.references.contains(frameReference) || (frames.references.empty() && frames.selection.contains((int)i) && reference.itemType == type && reference.itemID == id); if (type == TRIGGER) frameTime = frame.atFrame; auto buttonSize = type == TRIGGER ? frameSize : to_imvec2(vec2(frameSize.x * frame.duration, frameSize.y)); auto frameStart = type == TRIGGER ? frame.atFrame : frameTime; auto frameEnd = type == TRIGGER ? frameStart + 1.0f : frameStart + frame.duration; if (frameEnd <= (float)frameMin || frameStart >= (float)frameMax) { if (type != TRIGGER) frameTime += frame.duration; ImGui::PopID(); continue; } auto buttonPos = ImVec2(cursorPos.x + (frameTime * frameSize.x), cursorPos.y); if (frameFocusRequested && frameFocusIndex == (int)i && reference == frameReference) frameFocusRequested = false; ImGui::SetCursorPos(buttonPos); auto buttonScreenPos = ImGui::GetCursorScreenPos(); auto fillColor = isSelected ? (isFrameVisible ? colorActive : colorActiveHidden) : (isFrameVisible ? color : colorHidden); drawList->AddRectFilled(buttonScreenPos, ImVec2(buttonScreenPos.x + buttonSize.x, buttonScreenPos.y + buttonSize.y), ImGui::GetColorU32(fillColor), FRAME_ROUNDING); ImGui::PushStyleColor(ImGuiCol_Header, isFrameVisible ? colorActive : colorActiveHidden); ImGui::PushStyleColor(ImGuiCol_HeaderActive, isFrameVisible ? colorActive : colorActiveHidden); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, isFrameVisible ? colorHovered : colorHoveredHidden); ImGui::PushStyleColor(ImGuiCol_NavCursor, isFrameVisible ? colorHovered : colorHoveredHidden); ImGui::SetNextItemAllowOverlap(); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, FRAME_ROUNDING); ImGui::SetNextItemSelectionUserData((int)i); bool isDifferentItem = reference.itemType != type || reference.itemID != id; if (ImGui::Selectable("##Frame Button", isSelected, ImGuiSelectableFlags_None, buttonSize)) { if (type == LAYER) if (auto layer = layer_get(id)) document.spritesheet.reference = layer->spritesheetId; if (type != TRIGGER) { if (ImGui::IsKeyDown(ImGuiMod_Alt)) { auto targetReference = frameReference; edit_command_push(EDIT_FRAME_INTERPOLATION, Document::FRAMES, [=](Manager&, Document& document) { auto frame = command_frame_get(document, targetReference); if (!frame) return; frame->interpolation = frame->interpolation == Interpolation::NONE ? Interpolation::LINEAR : Interpolation::NONE; }); } document.frameTime = frameTime; } auto isCtrlDown = ImGui::IsKeyDown(ImGuiMod_Ctrl); auto isShiftDown = ImGui::IsKeyDown(ImGuiMod_Shift); if (isShiftDown) { auto isHadAnchor = isFrameSelectionAnchorSet; auto anchorReference = isHadAnchor ? frameSelectionAnchor : frameReference; auto isRangeSelected = frame_selection_range_set_for(document, anchorReference, frameReference, isCtrlDown); if (!isRangeSelected) frame_selection_set_for(document, frameReference); if (!isHadAnchor || !isRangeSelected) frameSelectionAnchor = frameReference; isFrameSelectionAnchorSet = true; } else if (isCtrlDown) { frame_selection_toggle_for(document, frameReference); frameSelectionAnchor = frameReference; isFrameSelectionAnchorSet = true; } else { frame_selection_set_for(document, frameReference); frameSelectionAnchor = frameReference; isFrameSelectionAnchorSet = true; } reference = frameReference; isReferenced = true; region.reference = -1; region.selection.clear(); if (isDifferentItem) frames_selection_sync_for(document); } ImGui::PopStyleVar(); ImGui::PopStyleColor(4); if (!isDraggedFrameActive && ImGui::IsItemHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { if (type == TRIGGER || ImGui::IsKeyDown(ImGuiMod_Ctrl)) { isDraggedFrameActive = true; draggedFrameReference = frameReference; draggedFrameType = type; draggedFrameIndex = (int)i; draggedFrameStart = hoveredTime; if (type != TRIGGER) draggedFrameStartDuration = frame.duration; draggedFrameStartMouseX = ImGui::GetIO().MousePos.x; draggedFrameWidth = frameSize.x; } } } if (type != TRIGGER) { if (!isDraggedFrameActive && !frameMoveDrag.isActive && ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { auto selectedReferences = frame_references_for_current_get(); if (!selectedReferences.contains(frameReference)) selectedReferences = {frameReference}; std::erase_if(selectedReferences, [](const Reference& selectedReference) { return selectedReference.itemType == TRIGGER; }); if (selectedReferences.empty()) selectedReferences = {frameReference}; int dragDuration = 0; for (auto selectedReference : selectedReferences) { auto selectedItem = item_get(selectedReference.itemType, selectedReference.itemID); auto selectedFrame = selectedItem ? track_frame_get(*selectedItem, selectedReference.frameIndex) : nullptr; if (selectedFrame) dragDuration += selectedFrame->duration; } dragDuration = glm::max(1, dragDuration); frameMoveDrag = { .type = type, .itemID = id, .animationIndex = reference.animationIndex, .frameIndex = (int)i, .duration = dragDuration, .indices = {}, .references = {selectedReferences.begin(), selectedReferences.end()}, .isActive = true, }; } } auto rectMin = ImGui::GetItemRectMin(); auto rectMax = ImGui::GetItemRectMax(); auto borderColor = isReferenced ? frameBorderColorReferenced : frameBorderColor; auto borderThickness = isReferenced ? FRAME_BORDER_THICKNESS_REFERENCED : FRAME_BORDER_THICKNESS; drawList->AddRect(rectMin, rectMax, ImGui::GetColorU32(borderColor), FRAME_ROUNDING, 0, borderThickness); auto icon = icon::UNINTERPOLATED; if (type == TRIGGER) icon = icon::TRIGGER; else { switch (frame.interpolation) { case Interpolation::NONE: icon = icon::UNINTERPOLATED; break; case Interpolation::LINEAR: icon = icon::INTERPOLATED; break; case Interpolation::EASE_IN: icon = icon::EASE_IN; break; case Interpolation::EASE_OUT: icon = icon::EASE_OUT; break; case Interpolation::EASE_IN_OUT: icon = icon::EASE_IN_OUT; break; default: icon = icon::UNINTERPOLATED; break; } } auto iconPos = ImVec2(cursorPos.x + (frameTime * frameSize.x), cursorPos.y + (frameSize.y / 2) - (icon_size_get().y / 2)); ImGui::SetCursorPos(iconPos); ImGui::Image(resources.icons[icon].id, icon_size_get()); overlay_icon(resources.icons[icon].id, iconTintDefault); if (type != TRIGGER) frameTime += frame.duration; ImGui::PopID(); } if (isFrameMovePreview) { drawList->AddRectFilled(frameMovePreviewMin, frameMovePreviewMax, ImGui::GetColorU32(ImGuiCol_DragDropTargetBg), FRAME_ROUNDING); drawList->AddRect(frameMovePreviewMin, frameMovePreviewMax, ImGui::GetColorU32(ImGuiCol_DragDropTarget), FRAME_ROUNDING, 0, ImGui::GetStyle().DragDropTargetBorderSize); } if (isFrameMoveHoveredFrame) drawList->AddRect(frameMoveHoveredFrameMin, frameMoveHoveredFrameMax, ImGui::GetColorU32(ImGuiCol_DragDropTarget), FRAME_ROUNDING, 0, ImGui::GetStyle().DragDropTargetBorderSize * 1.5f); if (!isFrameBoxSelecting && !frameMoveDrag.isActive && !isDraggedFrameActive && ImGui::IsWindowHovered() && (ImGui::IsMouseReleased(ImGuiMouseButton_Left) || ImGui::IsMouseReleased(ImGuiMouseButton_Right)) && !ImGui::IsAnyItemHovered()) row_selection_set(row); if (isFrameSelectionLocked) { frames.selection.clear(); frames.references.clear(); for (int idx : frameSelectionLocked) { frames.selection.insert(idx); frames.references.insert({reference.animationIndex, type, id, idx}); } isFrameSelectionLocked = false; frameSelectionLocked.clear(); } if (reference.itemType == type && reference.itemID == id) { frameSelectionSnapshot.assign(frames.selection.begin(), frames.selection.end()); frameSelectionSnapshotReference = reference; } } } if (isDraggedFrameActive) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); auto durationDelta = draggedFrameWidth > 0.0f ? static_cast((ImGui::GetIO().MousePos.x - draggedFrameStartMouseX) / draggedFrameWidth) : hoveredTime - draggedFrameStart; auto isDraggedFrameChanged = draggedFrameType == TRIGGER ? hoveredTime != draggedFrameStart : durationDelta != 0; if (!isDraggedFrameSnapshot && isDraggedFrameChanged) { isDraggedFrameSnapshot = true; snapshot_command_push(draggedFrameType == TRIGGER ? EDIT_TRIGGER_AT_FRAME : EDIT_FRAME_DURATION); } if (isDraggedFrameSnapshot) { auto targetReference = draggedFrameReference; auto targetType = draggedFrameType; auto targetIndex = draggedFrameIndex; auto targetStartDuration = draggedFrameStartDuration; auto targetHoveredTime = hoveredTime; auto targetDurationDelta = durationDelta; auto isPlaybackClamp = settings.playbackIsClamp; auto animationLength = animation ? animation->frameNum : FRAME_NUM_MAX; command_push([=](Manager&, Document& document) { auto item = command_item_get(document, targetReference.animationIndex, targetReference.itemType, targetReference.itemID); auto frame = command_frame_get(document, targetReference); if (!item || !frame) return; if (targetType == TRIGGER) { frame->atFrame = glm::clamp(targetHoveredTime, 0, isPlaybackClamp ? animationLength - 1 : FRAME_NUM_MAX - 1); for (auto [i, trigger] : std::views::enumerate(item->children)) { if ((int)i == targetIndex) continue; if (trigger.atFrame == frame->atFrame) frame->atFrame--; } } else { frame->duration = glm::clamp(targetStartDuration + targetDurationDelta, FRAME_DURATION_MIN, FRAME_DURATION_MAX); } }); } if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { auto targetReference = draggedFrameReference; auto targetType = draggedFrameType; command_push([=](Manager&, Document& document) { auto item = command_item_get(document, targetReference.animationIndex, targetReference.itemType, targetReference.itemID); if (targetType == TRIGGER && item) frames_sort_by_at_frame(*item); document.anm2_change(Document::FRAMES); }); isDraggedFrameActive = false; draggedFrameReference = {}; draggedFrameType = NONE; draggedFrameIndex = -1; draggedFrameStart = -1; draggedFrameStartDuration = -1; draggedFrameStartMouseX = 0.0f; draggedFrameWidth = 0.0f; isDraggedFrameSnapshot = false; frameSelectionLocked.clear(); } } context_menu(); ImGui::EndChild(); if (isLightTheme && isDefaultChild) ImGui::PopStyleColor(); ImGui::PopStyleVar(); index++; ImGui::PopID(); }; auto frames_child = [&]() { auto cursorPos = ImGui::GetCursorPos(); ImGui::SetCursorPos(ImVec2(cursorPos.x + ITEM_CHILD_WIDTH, cursorPos.y)); auto framesChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y); if (ImGui::BeginChild("##Frames Child", framesChildSize, ImGuiChildFlags_Borders)) { auto viewListChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - ImGui::GetTextLineHeightWithSpacing() - style.ItemSpacing.y * 2); auto animationsLength = [&]() { int length{}; if (auto animations = anm2.element_get(ElementType::ANIMATIONS)) for (auto& item : animations->children) if (item.type == ElementType::ANIMATION) length = std::max(length, animation_length_get(item)); return length; }(); auto childWidth = animationsLength * ImGui::GetTextLineHeight(); if (animation && animation->frameNum > animationsLength) childWidth = animation->frameNum * ImGui::GetTextLineHeight(); childWidth = std::max(childWidth, ImGui::GetContentRegionAvail().x); childWidth *= WIDTH_MULTIPLIER; ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); if (ImGui::BeginChild("##Frames List Child", viewListChildSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { playheadLineCenterX = 0.0f; playheadLineTopY = 0.0f; isPlayheadLineSet = false; isFrameBoxClipSet = false; if (animation && ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, ImGuiInputFlags_RouteFocused)) { group_selection_reset_for(document); document.frames.references = all_frame_references_for_items_get(); if (!document.frames.references.empty()) { reference = *document.frames.references.begin(); frames_selection_sync_for(document); } } ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2()); if (ImGui::BeginTable("##Frames List Table", 1, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) { ImGuiWindow* window = ImGui::GetCurrentWindow(); window->Flags |= ImGuiWindowFlags_NoScrollWithMouse; scroll.x = ImGui::GetScrollX(); scroll.y = ImGui::GetScrollY(); isHorizontalScroll = window->ScrollbarX; if (isWindowHovered) { auto& io = ImGui::GetIO(); auto lineHeight = ImGui::GetTextLineHeightWithSpacing() * 2; scroll.x -= io.MouseWheelH * lineHeight; scroll.y -= io.MouseWheel * lineHeight; } ImGui::SetScrollX(scroll.x); ImGui::SetScrollY(scroll.y); auto frameBoxDrawList = ImGui::GetWindowDrawList(); frameBoxClipMin = frameBoxDrawList->GetClipRectMin(); frameBoxClipMax = frameBoxDrawList->GetClipRectMax(); isFrameBoxClipSet = true; if (isFrameBoxPending || isFrameBoxSelecting) { auto& io = ImGui::GetIO(); auto threshold = ImGui::GetTextLineHeightWithSpacing() * 0.125f; frameBoxEnd = frame_box_content_point_get(); auto distance = ImVec2(frameBoxEnd.x - frameBoxStart.x, frameBoxEnd.y - frameBoxStart.y); if (isFrameBoxPending && distance.x * distance.x + distance.y * distance.y >= threshold * threshold) { isFrameBoxPending = false; isFrameBoxSelecting = true; } auto edgeSize = ImGui::GetTextLineHeightWithSpacing(); auto scrollStep = edgeSize * 0.5f; if (isFrameBoxSelecting) { if (io.MousePos.x < frameBoxClipMin.x + edgeSize) scroll.x -= scrollStep; else if (io.MousePos.x > frameBoxClipMax.x - edgeSize) scroll.x += scrollStep; if (io.MousePos.y < frameBoxClipMin.y + edgeSize) scroll.y -= scrollStep; else if (io.MousePos.y > frameBoxClipMax.y - edgeSize) scroll.y += scrollStep; ImGui::SetScrollX(scroll.x); ImGui::SetScrollY(scroll.y); frameBoxEnd = frame_box_content_point_get(); frameBoxSelection.clear(); } } int index{}; ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableSetupColumn("##Frames"); auto frames_child_row = [&](const TimelineItemRow& row) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); frame_child(row, index, childWidth); }; frames_child_row({.type = NONE}); if (animation) { ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); for (const auto& row : timeline_item_rows_get()) frames_child_row(row); ImGui::PopStyleVar(); } ImGui::EndTable(); } if (isFrameBoxClipSet) { auto overlayCursor = ImGui::GetCursorScreenPos(); auto overlayPos = ImGui::GetWindowPos(); auto overlaySize = ImGui::GetWindowSize(); ImGui::SetCursorScreenPos(overlayPos); if (ImGui::BeginChild("##Frames Overlay Child", overlaySize, ImGuiChildFlags_None, ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoNavInputs)) frame_overlay_draw(ImGui::GetWindowDrawList(), frameBoxClipMin, frameBoxClipMax); ImGui::EndChild(); ImGui::SetCursorScreenPos(overlayCursor); } if (isFrameBoxPending || isFrameBoxSelecting) { if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { if (isFrameBoxSelecting) { group_selection_reset_for(document); if (isFrameBoxAdditive) document.frames.references.insert(frameBoxSelection.begin(), frameBoxSelection.end()); else document.frames.references = frameBoxSelection; document.items.references.clear(); for (auto frameReference : document.frames.references) document.items.references.insert(item_reference_from_frame_get(frameReference)); if (!document.frames.references.empty()) reference = *document.frames.references.begin(); frames_selection_sync_for(document); } isFrameBoxPending = false; isFrameBoxSelecting = false; frameBoxSelection.clear(); } } ImGui::PopStyleVar(); } ImGui::EndChild(); ImGui::PopStyleVar(); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); ImGui::SetCursorPos( ImVec2(ImGui::GetStyle().WindowPadding.x, ImGui::GetCursorPos().y + ImGui::GetStyle().ItemSpacing.y)); auto widgetSize = widget_size_with_row_get(10); ImGui::BeginDisabled(!animation); { auto label = playback.isPlaying ? localize.get(LABEL_PAUSE) : localize.get(LABEL_PLAY); auto tooltip = playback.isPlaying ? localize.get(TOOLTIP_PAUSE_ANIMATION) : localize.get(TOOLTIP_PLAY_ANIMATION); shortcut(manager.chords[SHORTCUT_PLAY_PAUSE]); if (ImGui::Button(label, widgetSize)) playback.toggle(); set_item_tooltip_shortcut(tooltip, settings.shortcutPlayPause); ImGui::SameLine(); auto item = selected_item_get(); auto selectedFrames = frame_references_for_current_get(); auto selectedBakeFrames = selectedFrames; std::erase_if(selectedBakeFrames, [](const Reference& frameReference) { return frameReference.itemType == TRIGGER; }); ImGui::BeginDisabled(!item); { shortcut(manager.chords[SHORTCUT_INSERT_FRAME]); if (ImGui::Button(localize.get(LABEL_INSERT), widgetSize)) frame_insert(); set_item_tooltip_shortcut(localize.get(TOOLTIP_INSERT_FRAME), settings.shortcutInsertFrame); ImGui::SameLine(); ImGui::BeginDisabled(selectedFrames.empty()); { shortcut(manager.chords[SHORTCUT_REMOVE]); if (ImGui::Button(localize.get(LABEL_DELETE), widgetSize)) frames_delete_action(); set_item_tooltip_shortcut(localize.get(TOOLTIP_DELETE_FRAMES), settings.shortcutRemove); ImGui::SameLine(); ImGui::BeginDisabled(selectedBakeFrames.empty()); if (ImGui::Button(localize.get(LABEL_BAKE), widgetSize)) bakePopup.open(); set_item_tooltip_shortcut(localize.get(TOOLTIP_BAKE_FRAMES), settings.shortcutBake); ImGui::EndDisabled(); } ImGui::EndDisabled(); } ImGui::EndDisabled(); ImGui::SameLine(); ImGui::BeginDisabled(!animation || animation->frameNum == animation_length_get(*animation)); shortcut(manager.chords[SHORTCUT_FIT]); if (ImGui::Button(localize.get(LABEL_FIT_ANIMATION_LENGTH), widgetSize)) fit_animation_length(); set_item_tooltip_shortcut(localize.get(TOOLTIP_FIT_ANIMATION_LENGTH), settings.shortcutFit); ImGui::EndDisabled(); ImGui::SameLine(); auto frameNum = animation ? animation->frameNum : dummy_value(); ImGui::SetNextItemWidth(widgetSize.x); if (input_int_range(localize.get(LABEL_ANIMATION_LENGTH), frameNum, FRAME_NUM_MIN, FRAME_NUM_MAX, STEP, STEP_FAST, !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0) && animation) { auto animationIndex = reference.animationIndex; edit_command_push(EDIT_ANIMATION_LENGTH, Document::ANIMATIONS, [=](Manager&, Document& document) { auto animation = command_animation_get(document, animationIndex); if (!animation) return; animation->frameNum = frameNum; }); } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ANIMATION_LENGTH)); ImGui::SameLine(); auto isLoop = animation ? animation->isLoop : dummy_value(); ImGui::SetNextItemWidth(widgetSize.x); if (ImGui::Checkbox(localize.get(LABEL_LOOP), &isLoop) && animation) { auto animationIndex = reference.animationIndex; edit_command_push(EDIT_LOOP, Document::ANIMATIONS, [=](Manager&, Document& document) { auto animation = command_animation_get(document, animationIndex); if (!animation) return; animation->isLoop = isLoop; }); } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LOOP_ANIMATION)); } ImGui::EndDisabled(); ImGui::SameLine(); auto info = info_get(); auto fps = info ? info->fps : 30; ImGui::SetNextItemWidth(widgetSize.x); if (input_int_range(localize.get(LABEL_FPS), fps, FPS_MIN, FPS_MAX)) { edit_command_push(EDIT_FPS, Document::INFO, [=](Manager&, Document& document) { auto info = command_info_get(document); if (!info) { document.anm2.root.children.push_back(element_make(ElementType::INFO)); info = &document.anm2.root.children.back(); } info->fps = fps; }); } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FPS)); ImGui::SameLine(); info = info_get(); auto createdBy = info ? info->createdBy : std::string{}; ImGui::SetNextItemWidth(widgetSize.x); if (input_text_string(localize.get(LABEL_AUTHOR), &createdBy)) { edit_command_push(EDIT_AUTHOR, Document::INFO, [=](Manager&, Document& document) { auto info = command_info_get(document); if (!info) { document.anm2.root.children.push_back(element_make(ElementType::INFO)); info = &document.anm2.root.children.back(); } info->createdBy = createdBy; }); } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_AUTHOR)); ImGui::SameLine(); ImGui::SetNextItemWidth(widgetSize.x); ImGui::Checkbox(localize.get(LABEL_SOUND), &settings.timelineIsSound); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SOUND)); ImGui::PopStyleVar(); } ImGui::EndChild(); ImGui::SetCursorPos(cursorPos); }; ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); if (ImGui::Begin(localize.get(LABEL_TIMELINE_WINDOW), &settings.windowIsTimeline)) { isWindowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); frames_child(); if (frameMoveDrag.isActive && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { if (isFrameMoveDropTarget) frames_move_to(frameMoveDropType, frameMoveDropItemID, frameMoveDropIndex); frame_move_drag_clear(); } items_child(); } ImGui::PopStyleVar(); ImGui::End(); if (itemProperties.update(manager, settings, document, reference)) group_selection_reset_for(document); group_properties_update(); 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 = frame_get(); input_int_range(localize.get(LABEL_INTERVAL), interval, FRAME_DURATION_MIN, frame ? frame->duration : FRAME_DURATION_MIN); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_INTERVAL)); ImGui::Checkbox(localize.get(LABEL_ROUND_ROTATION), &isRoundRotation); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ROUND_ROTATION)); ImGui::Checkbox(localize.get(LABEL_ROUND_SCALE), &isRoundScale); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ROUND_SCALE)); auto widgetSize = widget_size_with_row_get(2); shortcut(manager.chords[SHORTCUT_CONFIRM]); if (ImGui::Button(localize.get(LABEL_BAKE), widgetSize)) { frames_bake(); bakePopup.close(); } ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_BAKE_FRAMES_OPTIONS)); ImGui::SameLine(); shortcut(manager.chords[SHORTCUT_CANCEL]); if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) bakePopup.close(); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CANCEL_BAKE_FRAMES)); ImGui::EndPopup(); } if (animation) { if (shortcut(manager.chords[SHORTCUT_PLAY_PAUSE], shortcut::GLOBAL)) playback.toggle(); if (shortcut(manager.chords[SHORTCUT_MOVE_PLAYHEAD_BACK], shortcut::GLOBAL)) { playback_stop(); playback.decrement(settings.playbackIsClamp ? animation->frameNum : FRAME_NUM_MAX); document.frameTime = playback.time; } if (shortcut(manager.chords[SHORTCUT_MOVE_PLAYHEAD_FORWARD], shortcut::GLOBAL)) { playback_stop(); playback.increment(settings.playbackIsClamp ? animation->frameNum : FRAME_NUM_MAX); document.frameTime = playback.time; } static bool isShortenChordHeld = false; auto isShortenFrame = shortcut(manager.chords[SHORTCUT_SHORTEN_FRAME], shortcut::GLOBAL); if (isShortenFrame && !isShortenChordHeld) snapshot_command_push(EDIT_SHORTEN_FRAME); if (isShortenFrame) { auto selectedFrames = frame_references_for_current_get(); std::erase_if(selectedFrames, [](const Reference& frameReference) { return frameReference.itemType == TRIGGER; }); if (!selectedFrames.empty()) { command_push([=](Manager&, Document& document) { for (auto frameReference : selectedFrames) { auto frame = command_frame_get(document, frameReference); if (!frame) continue; frame->duration = std::max(FRAME_DURATION_MIN, frame->duration - 1); } document.anm2_change(Document::FRAMES); }); } } isShortenChordHeld = isShortenFrame; static bool isExtendChordHeld = false; auto isExtendFrame = shortcut(manager.chords[SHORTCUT_EXTEND_FRAME], shortcut::GLOBAL); if (isExtendFrame && !isExtendChordHeld) snapshot_command_push(EDIT_EXTEND_FRAME); if (isExtendFrame) { auto selectedFrames = frame_references_for_current_get(); std::erase_if(selectedFrames, [](const Reference& frameReference) { return frameReference.itemType == TRIGGER; }); if (!selectedFrames.empty()) { command_push([=](Manager&, Document& document) { for (auto frameReference : selectedFrames) { auto frame = command_frame_get(document, frameReference); if (!frame) continue; frame->duration = std::min(FRAME_DURATION_MAX, frame->duration + 1); } document.anm2_change(Document::FRAMES); }); } } isExtendChordHeld = isExtendFrame; auto isPreviousFrame = shortcut(manager.chords[SHORTCUT_PREVIOUS_FRAME], shortcut::GLOBAL); auto isNextFrame = shortcut(manager.chords[SHORTCUT_NEXT_FRAME], shortcut::GLOBAL); auto isPreviousItem = shortcut(manager.chords[SHORTCUT_PREVIOUS_ITEM], shortcut::GLOBAL); auto isNextItem = shortcut(manager.chords[SHORTCUT_NEXT_ITEM], shortcut::GLOBAL); if (isPreviousFrame) if (auto item = selected_item_get(); item && !item->children.empty()) reference.frameIndex = glm::clamp(--reference.frameIndex, 0, (int)item->children.size() - 1); if (isNextFrame) if (auto item = selected_item_get(); item && !item->children.empty()) reference.frameIndex = glm::clamp(++reference.frameIndex, 0, (int)item->children.size() - 1); if (isPreviousFrame || isNextFrame) { if (auto item = selected_item_get(); item && !item->children.empty()) { frames_selection_set_reference(); document.frameTime = frame_time_from_index_get(*item, reference.frameIndex); } } if (isPreviousItem) reference_set_adjacent_item(-1); if (isNextItem) reference_set_adjacent_item(1); } if (isTextPushed) ImGui::PopStyleColor(); } }