From 7d8370a5fb10de0b5687709f9fe6e52986a3449f Mon Sep 17 00:00:00 2001 From: DexterFstone Date: Mon, 23 Jun 2025 02:30:37 -0700 Subject: [PATCH] Add ability to copy and paste animations in SpriteFrames --- editor/scene/sprite_frames_editor_plugin.cpp | 220 +++++++++++++++---- editor/scene/sprite_frames_editor_plugin.h | 22 ++ scene/2d/animated_sprite_2d.cpp | 5 +- scene/3d/sprite_3d.cpp | 5 +- 4 files changed, 200 insertions(+), 52 deletions(-) diff --git a/editor/scene/sprite_frames_editor_plugin.cpp b/editor/scene/sprite_frames_editor_plugin.cpp index 293617c7eec..7c4ce1038e4 100644 --- a/editor/scene/sprite_frames_editor_plugin.cpp +++ b/editor/scene/sprite_frames_editor_plugin.cpp @@ -666,6 +666,9 @@ void SpriteFramesEditor::_notification(int p_what) { zoom_in->set_button_icon(get_editor_theme_icon(SNAME("ZoomMore"))); add_anim->set_button_icon(get_editor_theme_icon(SNAME("New"))); duplicate_anim->set_button_icon(get_editor_theme_icon(SNAME("Duplicate"))); + cut_anim->set_button_icon(get_editor_theme_icon(SNAME("ActionCut"))); + copy_anim->set_button_icon(get_editor_theme_icon(SNAME("ActionCopy"))); + paste_anim->set_button_icon(get_editor_theme_icon(SNAME("ActionPaste"))); delete_anim->set_button_icon(get_editor_theme_icon(SNAME("Remove"))); anim_search_box->set_right_icon(get_editor_theme_icon(SNAME("Search"))); split_sheet_zoom_out->set_button_icon(get_editor_theme_icon(SNAME("ZoomLess"))); @@ -1221,19 +1224,7 @@ void SpriteFramesEditor::_animation_duplicate() { return; } - int counter = 1; - String new_name = edited_anim; - PackedStringArray name_component = new_name.rsplit("_", true, 1); - String base_name = name_component[0]; - if (name_component.size() > 1 && name_component[1].is_valid_int() && name_component[1].to_int() >= 0) { - counter = name_component[1].to_int(); - } - new_name = base_name + "_" + itos(counter); - while (frames->has_animation(new_name)) { - counter++; - new_name = base_name + "_" + itos(counter); - } - + String new_name = _generate_unique_animation_name(edited_anim); EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Duplicate Animation"), UndoRedo::MERGE_DISABLE, EditorNode::get_singleton()->get_edited_scene()); undo_redo->add_do_method(frames.ptr(), "duplicate_animation", edited_anim, new_name); @@ -1247,6 +1238,57 @@ void SpriteFramesEditor::_animation_duplicate() { animations->grab_focus(); } +void SpriteFramesEditor::_animation_cut() { + if (!frames->has_animation(edited_anim)) { + return; + } + + // Copy animation to clipboard. + Ref clipboard_anim = ClipboardAnimation::from_sprite_frames(frames, edited_anim); + EditorSettings::get_singleton()->set_resource_clipboard(clipboard_anim); + + // Remove animation with undo/redo (no confirmation dialog). + _animation_remove_undo_redo(TTR("Cut Animation"), &clipboard_anim->frames); +} + +void SpriteFramesEditor::_animation_copy() { + if (!frames->has_animation(edited_anim)) { + return; + } + + Ref clipboard_anim = ClipboardAnimation::from_sprite_frames(frames, edited_anim); + EditorSettings::get_singleton()->set_resource_clipboard(clipboard_anim); +} + +void SpriteFramesEditor::_animation_paste() { + if (updating) { + return; + } + + Ref clipboard_anim = EditorSettings::get_singleton()->get_resource_clipboard(); + if (clipboard_anim.is_null()) { + return; + } + + String new_name = _generate_unique_animation_name(clipboard_anim->name); + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Paste Animation"), UndoRedo::MERGE_DISABLE, EditorNode::get_singleton()->get_edited_scene()); + undo_redo->add_do_method(frames.ptr(), "add_animation", new_name); + undo_redo->add_undo_method(frames.ptr(), "remove_animation", new_name); + undo_redo->add_do_method(frames.ptr(), "set_animation_speed", new_name, clipboard_anim->speed); + undo_redo->add_do_method(frames.ptr(), "set_animation_loop", new_name, clipboard_anim->loop); + + for (ClipboardSpriteFrames::Frame &frame : clipboard_anim->frames) { + undo_redo->add_do_method(frames.ptr(), "add_frame", new_name, frame.texture, frame.duration); + } + + undo_redo->add_do_method(this, "_select_animation", new_name); + undo_redo->add_undo_method(this, "_select_animation", edited_anim); + undo_redo->add_do_method(this, "_update_library"); + undo_redo->add_undo_method(this, "_update_library"); + undo_redo->commit_action(); +} + void SpriteFramesEditor::_animation_remove() { if (updating) { return; @@ -1261,39 +1303,7 @@ void SpriteFramesEditor::_animation_remove() { } void SpriteFramesEditor::_animation_remove_confirmed() { - StringName new_edited; - List anim_names; - frames->get_animation_list(&anim_names); - anim_names.sort_custom(); - if (anim_names.size() >= 2) { - if (edited_anim == anim_names.get(0)) { - new_edited = anim_names.get(1); - } else { - new_edited = anim_names.get(0); - } - } else { - new_edited = StringName(); - } - - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->create_action(TTR("Remove Animation"), UndoRedo::MERGE_DISABLE, frames.ptr()); - _rename_node_animation(undo_redo, false, edited_anim, new_edited, ""); - undo_redo->add_do_method(frames.ptr(), "remove_animation", edited_anim); - undo_redo->add_undo_method(frames.ptr(), "add_animation", edited_anim); - _rename_node_animation(undo_redo, true, edited_anim, edited_anim, edited_anim); - undo_redo->add_undo_method(frames.ptr(), "set_animation_speed", edited_anim, frames->get_animation_speed(edited_anim)); - undo_redo->add_undo_method(frames.ptr(), "set_animation_loop", edited_anim, frames->get_animation_loop(edited_anim)); - int fc = frames->get_frame_count(edited_anim); - for (int i = 0; i < fc; i++) { - Ref texture = frames->get_frame_texture(edited_anim, i); - float duration = frames->get_frame_duration(edited_anim, i); - undo_redo->add_undo_method(frames.ptr(), "add_frame", edited_anim, texture, duration); - } - undo_redo->add_do_method(this, "_select_animation", new_edited); - undo_redo->add_undo_method(this, "_select_animation", edited_anim); - undo_redo->add_do_method(this, "_update_library"); - undo_redo->add_undo_method(this, "_update_library"); - undo_redo->commit_action(); + _animation_remove_undo_redo(TTR("Remove Animation"), nullptr); } void SpriteFramesEditor::_animation_search_text_changed(const String &p_text) { @@ -1336,6 +1346,78 @@ void SpriteFramesEditor::_animation_speed_changed(double p_value) { undo_redo->commit_action(); } +void SpriteFramesEditor::_animation_remove_undo_redo(const StringName &p_action_name, const Vector *p_frames) { + StringName new_edited = _find_next_animation(); + int frame_count = frames->get_frame_count(edited_anim); + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(p_action_name, UndoRedo::MERGE_DISABLE, frames.ptr()); + _rename_node_animation(undo_redo, false, edited_anim, new_edited, ""); + undo_redo->add_do_method(frames.ptr(), "remove_animation", edited_anim); + undo_redo->add_undo_method(frames.ptr(), "add_animation", edited_anim); + _rename_node_animation(undo_redo, true, edited_anim, edited_anim, edited_anim); + undo_redo->add_undo_method(frames.ptr(), "set_animation_speed", edited_anim, frames->get_animation_speed(edited_anim)); + undo_redo->add_undo_method(frames.ptr(), "set_animation_loop", edited_anim, frames->get_animation_loop(edited_anim)); + for (int i = 0; i < frame_count; i++) { + Ref texture; + float duration; + if (p_frames) { + texture = (*p_frames)[i].texture; + duration = (*p_frames)[i].duration; + } else { + texture = frames->get_frame_texture(edited_anim, i); + duration = frames->get_frame_duration(edited_anim, i); + } + undo_redo->add_undo_method(frames.ptr(), "add_frame", edited_anim, texture, duration); + } + undo_redo->add_do_method(this, "_select_animation", new_edited); + undo_redo->add_undo_method(this, "_select_animation", edited_anim); + undo_redo->add_do_method(this, "_update_library"); + undo_redo->add_undo_method(this, "_update_library"); + undo_redo->commit_action(); +} + +StringName SpriteFramesEditor::_find_next_animation() { + List anim_names; + frames->get_animation_list(&anim_names); + anim_names.sort_custom(); + if (anim_names.size() >= 2) { + if (edited_anim == anim_names.get(0)) { + return anim_names.get(1); + } else { + return anim_names.get(0); + } + } else { + return StringName(); + } +} + +String SpriteFramesEditor::_generate_unique_animation_name(const String &p_base_name) const { + if (!frames->has_animation(p_base_name)) { + return p_base_name; + } + + int count = 2; + String new_name = p_base_name; + PackedStringArray split = new_name.split("_"); + int last_index = split.size() - 1; + if (last_index > 0 && split[last_index].is_valid_int() && split[last_index].to_int() >= 0) { + count = split[last_index].to_int(); + split.remove_at(last_index); + new_name = String("_").join(split); + } + while (true) { + String attempt = new_name; + attempt += vformat("_%d", count); + if (frames->has_animation(attempt)) { + count++; + continue; + } + new_name = attempt; + break; + } + return new_name; +} + void SpriteFramesEditor::_frame_list_gui_input(const Ref &p_event) { const Ref mb = p_event; @@ -1498,6 +1580,7 @@ void SpriteFramesEditor::_update_library_impl() { if (!anim_names.size()) { missing_anim_label->show(); anim_frames_vb->hide(); + updating = false; return; } missing_anim_label->hide(); @@ -1658,6 +1741,9 @@ void SpriteFramesEditor::edit(Ref p_frames) { add_anim->set_disabled(read_only); duplicate_anim->set_disabled(read_only); + cut_anim->set_disabled(read_only); + copy_anim->set_disabled(read_only); + paste_anim->set_disabled(read_only); delete_anim->set_disabled(read_only); anim_speed->set_editable(!read_only); anim_loop->set_disabled(read_only); @@ -2003,6 +2089,25 @@ SpriteFramesEditor::SpriteFramesEditor() { duplicate_anim->set_accessibility_name(TTRC("Duplicate Animation")); hbc_animlist->add_child(duplicate_anim); duplicate_anim->connect(SceneStringName(pressed), callable_mp(this, &SpriteFramesEditor::_animation_duplicate)); + duplicate_anim->set_visible(false); + + cut_anim = memnew(Button); + cut_anim->set_theme_type_variation(SceneStringName(FlatButton)); + cut_anim->set_accessibility_name(TTRC("Cut Animation")); + hbc_animlist->add_child(cut_anim); + cut_anim->connect(SceneStringName(pressed), callable_mp(this, &SpriteFramesEditor::_animation_cut)); + + copy_anim = memnew(Button); + copy_anim->set_theme_type_variation(SceneStringName(FlatButton)); + copy_anim->set_accessibility_name(TTRC("Copy Animation")); + hbc_animlist->add_child(copy_anim); + copy_anim->connect(SceneStringName(pressed), callable_mp(this, &SpriteFramesEditor::_animation_copy)); + + paste_anim = memnew(Button); + paste_anim->set_theme_type_variation(SceneStringName(FlatButton)); + paste_anim->set_accessibility_name(TTRC("Paste Animation")); + hbc_animlist->add_child(paste_anim); + paste_anim->connect(SceneStringName(pressed), callable_mp(this, &SpriteFramesEditor::_animation_paste)); delete_anim = memnew(Button); delete_anim->set_theme_type_variation(SceneStringName(FlatButton)); @@ -2064,6 +2169,12 @@ SpriteFramesEditor::SpriteFramesEditor() { add_anim->set_shortcut(ED_SHORTCUT("sprite_frames/new_animation", TTRC("Add Animation"), KeyModifierMask::CMD_OR_CTRL | Key::N)); duplicate_anim->set_shortcut_context(animations); duplicate_anim->set_shortcut(ED_SHORTCUT("sprite_frames/duplicate_animation", TTRC("Duplicate Animation"), KeyModifierMask::CMD_OR_CTRL | Key::D)); + cut_anim->set_shortcut_context(animations); + cut_anim->set_shortcut(ED_SHORTCUT("sprite_frames/cut_animation", TTRC("Cut Animation"), KeyModifierMask::CMD_OR_CTRL | Key::X)); + copy_anim->set_shortcut_context(animations); + copy_anim->set_shortcut(ED_SHORTCUT("sprite_frames/copy_animation", TTRC("Copy Animation"), KeyModifierMask::CMD_OR_CTRL | Key::C)); + paste_anim->set_shortcut_context(animations); + paste_anim->set_shortcut(ED_SHORTCUT("sprite_frames/paste_animation", TTRC("Paste Animation"), KeyModifierMask::CMD_OR_CTRL | Key::V)); delete_anim->set_shortcut_context(animations); delete_anim->set_shortcut(ED_SHORTCUT("sprite_frames/delete_animation", TTRC("Delete Animation"), Key::KEY_DELETE)); @@ -2613,3 +2724,20 @@ SpriteFramesEditorPlugin::SpriteFramesEditorPlugin() { button = EditorNode::get_bottom_panel()->add_item(TTRC("SpriteFrames"), frames_editor, ED_SHORTCUT_AND_COMMAND("bottom_panels/toggle_sprite_frames_bottom_panel", TTRC("Toggle SpriteFrames Bottom Panel"))); button->hide(); } + +Ref ClipboardAnimation::from_sprite_frames(const Ref &p_frames, const String &p_anim) { + Ref clipboard_anim; + clipboard_anim.instantiate(); + clipboard_anim->name = p_anim; + clipboard_anim->speed = p_frames->get_animation_speed(p_anim); + clipboard_anim->loop = p_frames->get_animation_loop(p_anim); + + int frame_count = p_frames->get_frame_count(p_anim); + for (int i = 0; i < frame_count; ++i) { + ClipboardSpriteFrames::Frame frame; + frame.texture = p_frames->get_frame_texture(p_anim, i); + frame.duration = p_frames->get_frame_duration(p_anim, i); + clipboard_anim->frames.push_back(frame); + } + return clipboard_anim; +} diff --git a/editor/scene/sprite_frames_editor_plugin.h b/editor/scene/sprite_frames_editor_plugin.h index 1c4b39fed7b..3e9bf358db0 100644 --- a/editor/scene/sprite_frames_editor_plugin.h +++ b/editor/scene/sprite_frames_editor_plugin.h @@ -57,6 +57,18 @@ public: Vector frames; }; +class ClipboardAnimation : public Resource { + GDCLASS(ClipboardAnimation, Resource); + +public: + String name; + Vector frames; + float speed = 1.0f; + bool loop = false; + + static Ref from_sprite_frames(const Ref &p_frames, const String &p_anim); +}; + class SpriteFramesEditor : public HSplitContainer { GDCLASS(SpriteFramesEditor, HSplitContainer); @@ -125,6 +137,9 @@ class SpriteFramesEditor : public HSplitContainer { Button *add_anim = nullptr; Button *duplicate_anim = nullptr; + Button *cut_anim = nullptr; + Button *copy_anim = nullptr; + Button *paste_anim = nullptr; Button *delete_anim = nullptr; SpinBox *anim_speed = nullptr; Button *anim_loop = nullptr; @@ -217,12 +232,19 @@ class SpriteFramesEditor : public HSplitContainer { void _animation_name_edited(); void _animation_add(); void _animation_duplicate(); + void _animation_cut(); + void _animation_copy(); + void _animation_paste(); void _animation_remove(); void _animation_remove_confirmed(); void _animation_search_text_changed(const String &p_text); void _animation_loop_changed(); void _animation_speed_resized(); void _animation_speed_changed(double p_value); + void _animation_remove_undo_redo(const StringName &p_action_name, const Vector *p_frames = nullptr); + + StringName _find_next_animation(); + String _generate_unique_animation_name(const String &p_base_name) const; void _frame_list_gui_input(const Ref &p_event); void _frame_list_item_selected(int p_index, bool p_selected); diff --git a/scene/2d/animated_sprite_2d.cpp b/scene/2d/animated_sprite_2d.cpp index deb72795a26..525b9414acc 100644 --- a/scene/2d/animated_sprite_2d.cpp +++ b/scene/2d/animated_sprite_2d.cpp @@ -565,8 +565,7 @@ void AnimatedSprite2D::set_animation(const StringName &p_name) { ERR_FAIL_MSG(vformat("There is no animation with name '%s'.", p_name)); } - int frame_count = frames->get_frame_count(animation); - if (animation == StringName() || frame_count == 0) { + if (animation == StringName() || frames->get_frame_count(animation) == 0) { stop(); return; } else if (!frames->get_animation_names().has(animation)) { @@ -576,7 +575,7 @@ void AnimatedSprite2D::set_animation(const StringName &p_name) { } if (std::signbit(get_playing_speed())) { - set_frame_and_progress(frame_count - 1, 1.0); + set_frame_and_progress(frames->get_frame_count(animation) - 1, 1.0); } else { set_frame_and_progress(0, 0.0); } diff --git a/scene/3d/sprite_3d.cpp b/scene/3d/sprite_3d.cpp index cda1dacfeb4..4f62383d5a2 100644 --- a/scene/3d/sprite_3d.cpp +++ b/scene/3d/sprite_3d.cpp @@ -1431,8 +1431,7 @@ void AnimatedSprite3D::set_animation(const StringName &p_name) { ERR_FAIL_MSG(vformat("There is no animation with name '%s'.", p_name)); } - int frame_count = frames->get_frame_count(animation); - if (animation == StringName() || frame_count == 0) { + if (animation == StringName() || frames->get_frame_count(animation) == 0) { stop(); return; } else if (!frames->get_animation_names().has(animation)) { @@ -1442,7 +1441,7 @@ void AnimatedSprite3D::set_animation(const StringName &p_name) { } if (std::signbit(get_playing_speed())) { - set_frame_and_progress(frame_count - 1, 1.0); + set_frame_and_progress(frames->get_frame_count(animation) - 1, 1.0); } else { set_frame_and_progress(0, 0.0); }