3455 lines
158 KiB
C++
3455 lines
158 KiB
C++
#include "timeline.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <format>
|
|
#include <map>
|
|
#include <set>
|
|
|
|
#include <imgui_internal.h>
|
|
|
|
#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<ItemType>(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<ItemType>(type), id) : nullptr;
|
|
};
|
|
auto command_frame_get = [](Document& document, const Reference& targetReference)
|
|
{
|
|
return document.anm2.element_get(targetReference.animationIndex, static_cast<ItemType>(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<Reference> 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<Reference> 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<Reference> 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<Reference> 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<Reference> selectedFrames) mutable
|
|
{
|
|
std::map<Reference, std::set<int>> 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<Reference, std::set<int>> groupedFrames{};
|
|
for (auto frameReference : selectedFrames)
|
|
{
|
|
auto itemReference = item_reference_from_frame_get(frameReference);
|
|
groupedFrames[itemReference].insert(frameReference.frameIndex);
|
|
}
|
|
|
|
std::set<Reference> 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<TimelineItemRow> 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<int> 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<Reference> 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<TimelineRowReference> 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<TimelineRowReference> 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<TimelineRowReference>{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<TimelineRowReference>& 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<int, std::set<int>> targetIds{};
|
|
std::map<int, std::set<int>> 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<int>{};
|
|
auto groupIds =
|
|
targetGroupIds.contains(targetType) ? targetGroupIds.at(targetType) : std::set<int>{};
|
|
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<Reference>{};
|
|
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<Reference>{};
|
|
auto itemType = selectedItems.begin()->itemType;
|
|
for (const auto& itemReference : selectedItems)
|
|
if (itemReference.itemType != itemType) return std::vector<Reference>{};
|
|
|
|
std::vector<Reference> 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<Reference>{};
|
|
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<int> 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<TimelineRowReference> 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<int> draggedIds{};
|
|
std::set<int> 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<Element> 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<int> indices{};
|
|
std::string errorString{};
|
|
int insertIndex = (int)item->children.size();
|
|
std::set<int> 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<TimelineRowReference> 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<TimelineRowReference> 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<Reference, std::set<int>> groupedFrames{};
|
|
for (auto frameReference : drag.references)
|
|
{
|
|
auto itemReference = item_reference_from_frame_get(frameReference);
|
|
groupedFrames[itemReference].insert(frameReference.frameIndex);
|
|
}
|
|
|
|
int removedBeforeTarget = 0;
|
|
std::vector<Element> 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<int>((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<int>();
|
|
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<bool>();
|
|
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();
|
|
}
|
|
}
|