Path: blob/master/editor/animation/animation_player_editor_plugin.cpp
20931 views
/**************************************************************************/1/* animation_player_editor_plugin.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "animation_player_editor_plugin.h"3132#include "core/config/project_settings.h"33#include "core/input/input.h"34#include "core/os/keyboard.h"35#include "editor/animation/animation_tree_editor_plugin.h"36#include "editor/docks/editor_dock_manager.h"37#include "editor/docks/inspector_dock.h"38#include "editor/docks/scene_tree_dock.h"39#include "editor/editor_node.h"40#include "editor/editor_undo_redo_manager.h"41#include "editor/gui/editor_file_dialog.h"42#include "editor/gui/editor_validation_panel.h"43#include "editor/scene/3d/node_3d_editor_plugin.h" // For onion skinning.44#include "editor/scene/canvas_item_editor_plugin.h" // For onion skinning.45#include "editor/settings/editor_command_palette.h"46#include "editor/settings/editor_settings.h"47#include "editor/themes/editor_scale.h"48#include "editor/themes/editor_theme_manager.h"49#include "scene/animation/animation_tree.h"50#include "scene/gui/separator.h"51#include "scene/main/window.h"52#include "scene/resources/animation.h"53#include "scene/resources/image_texture.h"54#include "servers/rendering/rendering_server.h"5556///////////////////////////////////5758void AnimationPlayerEditor::_find_player() {59if (!is_visible() || player) {60return;61}6263Node *edited_scene = EditorNode::get_singleton()->get_edited_scene();6465if (!edited_scene) {66return;67}6869TypedArray<Node> players = edited_scene->find_children("", "AnimationPlayer");7071if (players.size() == 1) {72// Replicating EditorNode::_plugin_over_edit to ensure an identical setup as when selecting manually.73plugin->edit(players.front());74plugin->make_visible(true);75}76}7778void AnimationPlayerEditor::_node_removed(Node *p_node) {79if (player && original_node == p_node) {80if (is_dummy) {81plugin->_clear_dummy_player();82}8384player = nullptr;8586set_process(false);8788track_editor->set_animation(Ref<Animation>(), true);89track_editor->set_root(nullptr);90track_editor->show_select_node_warning(true);91_update_player();9293_ensure_dummy_player();9495pin->set_pressed(false);96}97}9899void AnimationPlayerEditor::_notification(int p_what) {100switch (p_what) {101case NOTIFICATION_PROCESS: {102finishing = false;103if (!player || is_dummy) {104track_editor->show_inactive_player_warning(false);105} else {106track_editor->show_inactive_player_warning(!player->is_active());107}108109if (!player) {110return;111}112113updating = true;114115if (player->is_playing()) {116{117StringName animname = player->get_assigned_animation();118119if (player->has_animation(animname)) {120Ref<Animation> anim = player->get_animation(animname);121if (anim.is_valid()) {122frame->set_max((double)anim->get_length());123}124}125}126frame->set_value(player->get_current_animation_position());127track_editor->set_anim_pos(player->get_current_animation_position());128} else if (!player->is_valid()) {129// Reset timeline when the player has been stopped externally130frame->set_value(0);131} else if (last_active) {132// Need the last frame after it stopped.133frame->set_value(player->get_current_animation_position());134track_editor->set_anim_pos(player->get_current_animation_position());135stop->set_button_icon(stop_icon);136}137138last_active = player->is_playing();139140updating = false;141} break;142143case NOTIFICATION_ENTER_TREE: {144get_tree()->connect(SNAME("node_removed"), callable_mp(this, &AnimationPlayerEditor::_node_removed));145} break;146147case NOTIFICATION_EXIT_TREE: {148get_tree()->disconnect(SNAME("node_removed"), callable_mp(this, &AnimationPlayerEditor::_node_removed));149} break;150151case NOTIFICATION_READY: {152EditorNode::get_singleton()->connect("scene_changed", callable_mp(this, &AnimationPlayerEditor::_find_player));153154add_theme_style_override(SceneStringName(panel), EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SceneStringName(panel), SNAME("Panel")));155156_update_playback_tooltips();157} break;158159case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {160if (EditorThemeManager::is_generated_theme_outdated()) {161add_theme_style_override(SceneStringName(panel), EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SceneStringName(panel), SNAME("Panel")));162}163164_update_playback_tooltips();165} break;166167case NOTIFICATION_TRANSLATION_CHANGED:168case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:169case NOTIFICATION_THEME_CHANGED: {170stop_icon = get_editor_theme_icon(SNAME("Stop"));171pause_icon = get_editor_theme_icon(SNAME("Pause"));172if (player && player->is_playing()) {173stop->set_button_icon(pause_icon);174} else {175stop->set_button_icon(stop_icon);176}177178autoplay->set_button_icon(get_editor_theme_icon(SNAME("AutoPlay")));179play->set_button_icon(get_editor_theme_icon(SNAME("PlayStart")));180play_from->set_button_icon(get_editor_theme_icon(SNAME("Play")));181play_bw->set_button_icon(get_editor_theme_icon(SNAME("PlayStartBackwards")));182play_bw_from->set_button_icon(get_editor_theme_icon(SNAME("PlayBackwards")));183184autoplay_icon = get_editor_theme_icon(SNAME("AutoPlay"));185reset_icon = get_editor_theme_icon(SNAME("Reload"));186{187Ref<Image> autoplay_img = autoplay_icon->get_image();188Ref<Image> reset_img = reset_icon->get_image();189Size2 icon_size = autoplay_img->get_size();190Ref<Image> autoplay_reset_img = Image::create_empty(icon_size.x * 2, icon_size.y, false, autoplay_img->get_format());191autoplay_reset_img->blit_rect(autoplay_img, Rect2i(Point2i(), icon_size), Point2i());192autoplay_reset_img->blit_rect(reset_img, Rect2i(Point2i(), icon_size), Point2i(icon_size.x, 0));193autoplay_reset_icon = ImageTexture::create_from_image(autoplay_reset_img);194}195196onion_toggle->set_button_icon(get_editor_theme_icon(SNAME("Onion")));197onion_skinning->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl")));198199pin->set_button_icon(get_editor_theme_icon(SNAME("Pin")));200201#define ITEM_ICON(m_item, m_icon) tool_anim->get_popup()->set_item_icon(tool_anim->get_popup()->get_item_index(m_item), get_editor_theme_icon(SNAME(m_icon)))202203ITEM_ICON(TOOL_NEW_ANIM, "New");204ITEM_ICON(TOOL_ANIM_LIBRARY, "AnimationLibrary");205ITEM_ICON(TOOL_DUPLICATE_ANIM, "Duplicate");206ITEM_ICON(TOOL_RENAME_ANIM, "Rename");207ITEM_ICON(TOOL_EDIT_TRANSITIONS, "Blend");208ITEM_ICON(TOOL_EDIT_RESOURCE, "Edit");209ITEM_ICON(TOOL_REMOVE_ANIM, "Remove");210211_update_animation_list_icons();212} break;213214case NOTIFICATION_VISIBILITY_CHANGED: {215_find_player();216_ensure_dummy_player();217} break;218}219}220221void AnimationPlayerEditor::_autoplay_pressed() {222if (updating) {223return;224}225if (animation->has_selectable_items() == 0) {226return;227}228229EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();230String current = animation->get_item_text(animation->get_selected());231if (player->get_autoplay() == current) {232//unset233undo_redo->create_action(TTR("Toggle Autoplay"));234undo_redo->add_do_method(player, "set_autoplay", StringName());235undo_redo->add_undo_method(player, "set_autoplay", player->get_autoplay());236undo_redo->add_do_method(this, "_animation_player_changed", player);237undo_redo->add_undo_method(this, "_animation_player_changed", player);238undo_redo->commit_action();239240} else {241//set242undo_redo->create_action(TTR("Toggle Autoplay"));243undo_redo->add_do_method(player, "set_autoplay", StringName(current));244undo_redo->add_undo_method(player, "set_autoplay", player->get_autoplay());245undo_redo->add_do_method(this, "_animation_player_changed", player);246undo_redo->add_undo_method(this, "_animation_player_changed", player);247undo_redo->commit_action();248}249}250251void AnimationPlayerEditor::go_to_nearest_keyframe(bool p_backward) {252if (_get_current().is_empty()) {253return;254}255256Ref<Animation> anim = player->get_animation(player->get_assigned_animation());257258double current_time = player->get_current_animation_position();259// Offset the time to avoid finding the same keyframe with Animation::track_find_key().260double time_offset = MAX(CMP_EPSILON * 2, current_time * CMP_EPSILON * 2);261double current_time_offset = current_time + (p_backward ? -time_offset : time_offset);262263float nearest_key_time = p_backward ? 0 : anim->get_length();264int track_count = anim->get_track_count();265bool bezier_active = track_editor->is_bezier_editor_active();266267Node *root = get_tree()->get_edited_scene_root();268EditorSelection *selection = EditorNode::get_singleton()->get_editor_selection();269270Vector<int> selected_tracks;271for (int i = 0; i < track_count; ++i) {272if (selection->is_selected(root->get_node_or_null(anim->track_get_path(i)))) {273selected_tracks.push_back(i);274}275}276277// Find the nearest keyframe in selection if the scene has selected nodes278// or the nearest keyframe in the entire animation otherwise.279if (selected_tracks.size() > 0) {280for (int track : selected_tracks) {281if (bezier_active && anim->track_get_type(track) != Animation::TYPE_BEZIER) {282continue;283}284int key = anim->track_find_key(track, current_time_offset, Animation::FIND_MODE_NEAREST, false, !p_backward);285if (key == -1) {286continue;287}288double key_time = anim->track_get_key_time(track, key);289if ((p_backward && key_time > nearest_key_time) || (!p_backward && key_time < nearest_key_time)) {290nearest_key_time = key_time;291}292}293} else {294for (int track = 0; track < track_count; ++track) {295if (bezier_active && anim->track_get_type(track) != Animation::TYPE_BEZIER) {296continue;297}298int key = anim->track_find_key(track, current_time_offset, Animation::FIND_MODE_NEAREST, false, !p_backward);299if (key == -1) {300continue;301}302double key_time = anim->track_get_key_time(track, key);303if ((p_backward && key_time > nearest_key_time) || (!p_backward && key_time < nearest_key_time)) {304nearest_key_time = key_time;305}306}307}308309player->seek_internal(nearest_key_time, true, true, true);310frame->set_value(nearest_key_time);311track_editor->set_anim_pos(nearest_key_time);312}313314void AnimationPlayerEditor::_play_pressed() {315String current = _get_current();316317if (!current.is_empty()) {318if (current == player->get_assigned_animation()) {319player->stop(); //so it won't blend with itself320}321ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing.");322PackedStringArray markers = track_editor->get_selected_section();323if (markers.size() == 2) {324StringName start_marker = markers[0];325StringName end_marker = markers[1];326player->play_section_with_markers(current, start_marker, end_marker);327} else {328player->play(current);329}330}331332//unstop333stop->set_button_icon(pause_icon);334}335336void AnimationPlayerEditor::_play_from_pressed() {337String current = _get_current();338339if (!current.is_empty()) {340double time = player->get_current_animation_position();341if (current == player->get_assigned_animation() && player->is_playing()) {342player->clear_caches(); //so it won't blend with itself343}344ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing.");345player->seek_internal(time, true, true, true);346PackedStringArray markers = track_editor->get_selected_section();347if (markers.size() == 2) {348StringName start_marker = markers[0];349StringName end_marker = markers[1];350player->play_section_with_markers(current, start_marker, end_marker);351} else {352player->play(current);353}354}355356//unstop357stop->set_button_icon(pause_icon);358}359360String AnimationPlayerEditor::_get_current() const {361String current;362if (animation->get_selected() >= 0 && animation->get_selected() < animation->get_item_count() && !animation->is_item_separator(animation->get_selected())) {363current = animation->get_item_text(animation->get_selected());364}365return current;366}367void AnimationPlayerEditor::_play_bw_pressed() {368String current = _get_current();369if (!current.is_empty()) {370if (current == player->get_assigned_animation()) {371player->stop(); //so it won't blend with itself372}373ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing.");374PackedStringArray markers = track_editor->get_selected_section();375if (markers.size() == 2) {376StringName start_marker = markers[0];377StringName end_marker = markers[1];378player->play_section_with_markers_backwards(current, start_marker, end_marker);379} else {380player->play_backwards(current);381}382}383384//unstop385stop->set_button_icon(pause_icon);386}387388void AnimationPlayerEditor::_play_bw_from_pressed() {389String current = _get_current();390391if (!current.is_empty()) {392double time = player->get_current_animation_position();393if (current == player->get_assigned_animation() && player->is_playing()) {394player->clear_caches(); //so it won't blend with itself395}396ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing.");397player->seek_internal(time, true, true, true);398PackedStringArray markers = track_editor->get_selected_section();399if (markers.size() == 2) {400StringName start_marker = markers[0];401StringName end_marker = markers[1];402player->play_section_with_markers_backwards(current, start_marker, end_marker);403} else {404player->play_backwards(current);405}406}407408//unstop409stop->set_button_icon(pause_icon);410}411412void AnimationPlayerEditor::_stop_pressed() {413if (!player) {414return;415}416417if (player->is_playing()) {418player->pause();419} else {420String current = _get_current();421player->stop();422player->set_assigned_animation(current);423frame->set_value(0);424track_editor->set_anim_pos(0);425}426stop->set_button_icon(stop_icon);427}428429void AnimationPlayerEditor::_animation_selected(int p_which) {430if (updating) {431return;432}433434#define ITEM_CHECK_DISABLED(m_item) tool_anim->get_popup()->set_item_disabled(tool_anim->get_popup()->get_item_index(m_item), true)435ITEM_CHECK_DISABLED(TOOL_RENAME_ANIM);436ITEM_CHECK_DISABLED(TOOL_DUPLICATE_ANIM);437ITEM_CHECK_DISABLED(TOOL_REMOVE_ANIM);438439ITEM_CHECK_DISABLED(TOOL_EDIT_TRANSITIONS);440ITEM_CHECK_DISABLED(TOOL_EDIT_RESOURCE);441#undef ITEM_CHECK_DISABLED442443// when selecting an animation, the idea is that the only interesting behavior444// ui-wise is that it should play/blend the next one if currently playing445String current = _get_current();446447if (!current.is_empty()) {448player->set_assigned_animation(current);449450Ref<Animation> anim = player->get_animation(current);451ERR_FAIL_COND(anim.is_null());452{453bool animation_is_readonly = EditorNode::get_singleton()->is_resource_read_only(anim);454455track_editor->set_animation(anim, animation_is_readonly);456Node *root = player->get_node_or_null(player->get_root_node());457458// Player shouldn't access parent if it's the scene root.459if (!root || (player == get_tree()->get_edited_scene_root() && player->get_root_node() == SceneStringName(path_pp))) {460NodePath cached_root_path = player->get_path_to(get_cached_root_node());461if (player->get_node_or_null(cached_root_path) != nullptr) {462player->set_root_node(cached_root_path);463} else {464player->set_root_node(SceneStringName(path_pp)); // No other choice, preventing crash.465}466} else {467cached_root_node_id = root->get_instance_id(); // Caching as `track_editor` can lose track of player's root node.468track_editor->set_root(root);469}470}471frame->set_max((double)anim->get_length());472autoplay->set_pressed(current == player->get_autoplay());473player->stop();474} else {475track_editor->set_animation(Ref<Animation>(), true);476track_editor->set_root(nullptr);477autoplay->set_pressed(false);478}479480AnimationPlayerEditor::get_singleton()->get_track_editor()->update_keying();481_animation_key_editor_seek(timeline_position);482483emit_signal("animation_selected", current);484}485486void AnimationPlayerEditor::_animation_new() {487int count = 1;488String base = "new_animation";489String current_library_name = "";490if (animation->has_selectable_items()) {491String current_animation_name = animation->get_item_text(animation->get_selected());492Ref<Animation> current_animation = player->get_animation(current_animation_name);493if (current_animation.is_valid()) {494current_library_name = player->find_animation_library(current_animation);495}496}497String attempt_prefix = (current_library_name == "") ? "" : current_library_name + "/";498while (true) {499String attempt = base;500if (count > 1) {501attempt += vformat("_%d", count);502}503if (player->has_animation(attempt_prefix + attempt)) {504count++;505continue;506}507base = attempt;508break;509}510511_update_name_dialog_library_dropdown();512513name_dialog_op = TOOL_NEW_ANIM;514name_dialog->set_title(TTR("Create New Animation"));515name_dialog->popup_centered(Size2(300, 90));516name_title->set_text(TTR("New Animation Name:"));517name->set_text(base);518name->select_all();519name->grab_focus();520}521522void AnimationPlayerEditor::_animation_rename() {523if (!animation->has_selectable_items()) {524return;525}526int selected = animation->get_selected();527String selected_name = animation->get_item_text(selected);528529// Remove library prefix if present.530if (selected_name.contains_char('/')) {531selected_name = selected_name.get_slicec('/', 1);532}533534name_dialog->set_title(TTR("Rename Animation"));535name_title->set_text(TTR("Change Animation Name:"));536name->set_text(selected_name);537name_dialog_op = TOOL_RENAME_ANIM;538name_dialog->popup_centered(Size2(300, 90));539name->select_all();540name->grab_focus();541library->hide();542}543544void AnimationPlayerEditor::_animation_remove() {545if (!animation->has_selectable_items()) {546return;547}548549String current = animation->get_item_text(animation->get_selected());550551delete_dialog->set_text(vformat(TTR("Delete Animation '%s'?"), current));552delete_dialog->popup_centered();553}554555void AnimationPlayerEditor::_animation_remove_confirmed() {556String current = animation->get_item_text(animation->get_selected());557Ref<Animation> anim = player->get_animation(current);558559Ref<AnimationLibrary> al = player->get_animation_library(player->find_animation_library(anim));560ERR_FAIL_COND(al.is_null());561562// For names of form lib_name/anim_name, remove library name prefix.563if (current.contains_char('/')) {564current = current.get_slicec('/', 1);565}566EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();567undo_redo->create_action(TTR("Remove Animation"));568if (player->get_autoplay() == current) {569undo_redo->add_do_method(player, "set_autoplay", StringName());570undo_redo->add_undo_method(player, "set_autoplay", StringName(current));571// Avoid having the autoplay icon linger around if there is only one animation in the player.572undo_redo->add_do_method(this, "_animation_player_changed", player);573}574undo_redo->add_do_method(al.ptr(), "remove_animation", current);575undo_redo->add_undo_method(al.ptr(), "add_animation", current, anim);576undo_redo->add_do_method(this, "_animation_player_changed", player);577undo_redo->add_undo_method(this, "_animation_player_changed", player);578if (animation->has_selectable_items() && animation->get_selectable_item(false) == animation->get_selectable_item(true)) { // Last item remaining.579undo_redo->add_do_method(this, "_stop_onion_skinning");580undo_redo->add_undo_method(this, "_start_onion_skinning");581}582undo_redo->commit_action();583}584585void AnimationPlayerEditor::_select_anim_by_name(const String &p_anim) {586int idx = -1;587for (int i = 0; i < animation->get_item_count(); i++) {588if (animation->get_item_text(i) == p_anim) {589idx = i;590break;591}592}593594ERR_FAIL_COND(idx == -1);595596animation->select(idx);597598_animation_selected(idx);599}600601float AnimationPlayerEditor::_get_editor_step() const {602const StringName current = player->get_assigned_animation();603const Ref<Animation> anim = player->get_animation(current);604ERR_FAIL_COND_V(anim.is_null(), 0.0);605606float step = track_editor->get_snap_unit();607608// Use more precise snapping when holding Shift609return Input::get_singleton()->is_key_pressed(Key::SHIFT) ? step * 0.25 : step;610}611612void AnimationPlayerEditor::_animation_name_edited() {613if (player->is_playing()) {614player->stop();615}616617String new_name = name->get_text();618if (!AnimationLibrary::is_valid_animation_name(new_name)) {619error_dialog->set_text(TTR("Invalid animation name!"));620error_dialog->popup_centered();621return;622}623624if (name_dialog_op == TOOL_RENAME_ANIM && animation->has_selectable_items() && animation->get_item_text(animation->get_selected()) == new_name) {625name_dialog->hide();626return;627}628629String test_name_prefix = "";630if (library->is_visible() && library->get_selected_id() != -1) {631test_name_prefix = library->get_item_metadata(library->get_selected_id());632test_name_prefix += (test_name_prefix != "") ? "/" : "";633}634635if (player->has_animation(test_name_prefix + new_name)) {636error_dialog->set_text(vformat(TTR("Animation '%s' already exists!"), test_name_prefix + new_name));637error_dialog->popup_centered();638return;639}640641EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();642switch (name_dialog_op) {643case TOOL_RENAME_ANIM: {644String current = animation->get_item_text(animation->get_selected());645Ref<Animation> anim = player->get_animation(current);646647Ref<AnimationLibrary> al = player->get_animation_library(player->find_animation_library(anim));648ERR_FAIL_COND(al.is_null());649650// Extract library prefix if present.651String new_library_prefix = "";652if (current.contains_char('/')) {653new_library_prefix = current.get_slicec('/', 0) + "/";654current = current.get_slicec('/', 1);655}656657undo_redo->create_action(TTR("Rename Animation"));658undo_redo->add_do_method(al.ptr(), "rename_animation", current, new_name);659undo_redo->add_do_method(anim.ptr(), "set_name", new_name);660undo_redo->add_undo_method(al.ptr(), "rename_animation", new_name, current);661undo_redo->add_undo_method(anim.ptr(), "set_name", current);662undo_redo->add_do_method(this, "_animation_player_changed", player);663undo_redo->add_undo_method(this, "_animation_player_changed", player);664undo_redo->commit_action();665666if (is_dummy) {667plugin->_update_dummy_player(original_node);668}669_select_anim_by_name(new_library_prefix + new_name);670} break;671672case TOOL_NEW_ANIM: {673Ref<Animation> new_anim = Ref<Animation>(memnew(Animation));674new_anim->set_name(new_name);675676if (animation->get_item_count() > 0) {677String current = animation->get_item_text(animation->get_selected());678Ref<Animation> current_anim = player->get_animation(current);679680if (current_anim.is_valid()) {681new_anim->set_step(current_anim->get_step());682}683} else {684new_anim->set_step(EDITOR_GET("editors/animation/default_animation_step"));685}686687String library_name;688Ref<AnimationLibrary> al;689library_name = library->get_item_metadata(library->get_selected());690// It's possible that [Global] was selected, but doesn't exist yet.691if (player->has_animation_library(library_name)) {692al = player->get_animation_library(library_name);693}694695undo_redo->create_action(TTR("Add Animation"));696697bool lib_added = false;698if (al.is_null()) {699al.instantiate();700lib_added = true;701undo_redo->add_do_method(fetch_mixer_for_library(), "add_animation_library", "", al);702library_name = "";703}704705undo_redo->add_do_method(al.ptr(), "add_animation", new_name, new_anim);706undo_redo->add_undo_method(al.ptr(), "remove_animation", new_name);707undo_redo->add_do_method(this, "_animation_player_changed", player);708undo_redo->add_undo_method(this, "_animation_player_changed", player);709if (!animation->has_selectable_items()) {710undo_redo->add_do_method(this, "_start_onion_skinning");711undo_redo->add_undo_method(this, "_stop_onion_skinning");712}713if (lib_added) {714undo_redo->add_undo_method(fetch_mixer_for_library(), "remove_animation_library", "");715}716undo_redo->commit_action();717718if (library_name != "") {719library_name = library_name + "/";720}721722if (is_dummy) {723plugin->_update_dummy_player(original_node);724}725_select_anim_by_name(library_name + new_name);726727} break;728729case TOOL_DUPLICATE_ANIM: {730String current = animation->get_item_text(animation->get_selected());731Ref<Animation> anim = player->get_animation(current);732733Ref<Animation> new_anim = _animation_clone(anim);734new_anim->set_name(new_name);735736String library_name;737Ref<AnimationLibrary> al;738if (library->is_visible()) {739library_name = library->get_item_metadata(library->get_selected());740// It's possible that [Global] was selected, but doesn't exist yet.741if (player->has_animation_library(library_name)) {742al = player->get_animation_library(library_name);743}744} else {745if (player->has_animation_library("")) {746al = player->get_animation_library("");747library_name = "";748}749}750751undo_redo->create_action(TTR("Duplicate Animation"));752753bool lib_added = false;754if (al.is_null()) {755al.instantiate();756lib_added = true;757undo_redo->add_do_method(player, "add_animation_library", "", al);758library_name = "";759}760761undo_redo->add_do_method(al.ptr(), "add_animation", new_name, new_anim);762undo_redo->add_undo_method(al.ptr(), "remove_animation", new_name);763undo_redo->add_do_method(this, "_animation_player_changed", player);764undo_redo->add_undo_method(this, "_animation_player_changed", player);765if (lib_added) {766undo_redo->add_undo_method(player, "remove_animation_library", "");767}768undo_redo->commit_action();769770if (library_name != "") {771library_name = library_name + "/";772}773774if (is_dummy) {775plugin->_update_dummy_player(original_node);776}777_select_anim_by_name(library_name + new_name);778} break;779}780781name_dialog->hide();782}783784void AnimationPlayerEditor::_blend_editor_next_changed(const int p_idx) {785if (!animation->has_selectable_items()) {786return;787}788789String current = animation->get_item_text(animation->get_selected());790791EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();792undo_redo->create_action(TTR("Blend Next Changed"));793undo_redo->add_do_method(player, "animation_set_next", current, blend_editor.next->get_item_text(p_idx));794undo_redo->add_undo_method(player, "animation_set_next", current, player->animation_get_next(current));795undo_redo->add_do_method(this, "_animation_player_changed", player);796undo_redo->add_undo_method(this, "_animation_player_changed", player);797undo_redo->commit_action();798}799800void AnimationPlayerEditor::_edit_animation_blend() {801if (updating_blends || !animation->has_selectable_items()) {802return;803}804805blend_editor.dialog->popup_centered(Size2(400, 400) * EDSCALE);806_update_animation_blend();807}808809void AnimationPlayerEditor::_update_animation_blend() {810if (updating_blends || !animation->has_selectable_items()) {811return;812}813814blend_editor.tree->clear();815816String current = animation->get_item_text(animation->get_selected());817818List<StringName> anims;819player->get_animation_list(&anims);820TreeItem *root = blend_editor.tree->create_item();821updating_blends = true;822823int i = 0;824bool anim_found = false;825blend_editor.next->clear();826blend_editor.next->add_item("", i);827828for (const StringName &to : anims) {829TreeItem *blend = blend_editor.tree->create_item(root);830blend->set_editable(0, false);831blend->set_editable(1, true);832blend->set_text(0, to);833blend->set_cell_mode(1, TreeItem::CELL_MODE_RANGE);834blend->set_range_config(1, 0, 3600, 0.001);835blend->set_range(1, player->get_blend_time(current, to));836837i++;838blend_editor.next->add_item(to, i);839if (to == player->animation_get_next(current)) {840blend_editor.next->select(i);841anim_found = true;842}843}844845// make sure we reset it else it becomes out of sync and could contain a deleted animation846if (!anim_found) {847blend_editor.next->select(0);848player->animation_set_next(current, blend_editor.next->get_item_text(0));849}850851updating_blends = false;852}853854void AnimationPlayerEditor::_blend_edited() {855if (updating_blends || !animation->has_selectable_items()) {856return;857}858859TreeItem *selected = blend_editor.tree->get_edited();860if (!selected) {861return;862}863864String current = animation->get_item_text(animation->get_selected());865866updating_blends = true;867String to = selected->get_text(0);868float blend_time = selected->get_range(1);869float prev_blend_time = player->get_blend_time(current, to);870871EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();872undo_redo->create_action(TTR("Change Blend Time"));873undo_redo->add_do_method(player, "set_blend_time", current, to, blend_time);874undo_redo->add_undo_method(player, "set_blend_time", current, to, prev_blend_time);875undo_redo->add_do_method(this, "_animation_player_changed", player);876undo_redo->add_undo_method(this, "_animation_player_changed", player);877undo_redo->commit_action();878updating_blends = false;879}880881void AnimationPlayerEditor::ensure_visibility() {882if (player) {883return; // another player is pinned, don't reset884}885886_animation_edit();887}888889Dictionary AnimationPlayerEditor::get_state() const {890Dictionary d;891892if (!is_dummy) {893d["visible"] = is_visible_in_tree();894if (EditorNode::get_singleton()->get_edited_scene() && is_visible_in_tree() && player) {895d["player"] = EditorNode::get_singleton()->get_edited_scene()->get_path_to(player);896d["animation"] = player->get_assigned_animation();897d["track_editor_state"] = track_editor->get_state();898}899}900901return d;902}903904void AnimationPlayerEditor::set_state(const Dictionary &p_state) {905if (!p_state.has("visible") || !p_state["visible"]) {906return;907}908if (!EditorNode::get_singleton()->get_edited_scene()) {909return;910}911912if (p_state.has("player")) {913Node *n = EditorNode::get_singleton()->get_edited_scene()->get_node(p_state["player"]);914if (Object::cast_to<AnimationPlayer>(n) && EditorNode::get_singleton()->get_editor_selection()->is_selected(n)) {915if (player) {916if (player->is_connected(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated))) {917player->disconnect(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated));918}919if (player->is_connected(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed))) {920player->disconnect(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed));921}922}923player = Object::cast_to<AnimationPlayer>(n);924if (player) {925if (!player->is_connected(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated))) {926player->connect(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated), CONNECT_DEFERRED);927}928if (!player->is_connected(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed))) {929player->connect(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed));930}931}932933_update_player();934make_visible();935set_process(true);936ensure_visibility();937938if (p_state.has("animation")) {939String anim = p_state["animation"];940if (!anim.is_empty() && player->has_animation(anim)) {941_select_anim_by_name(anim);942_animation_edit();943}944}945}946}947948if (p_state.has("track_editor_state")) {949track_editor->set_state(p_state["track_editor_state"]);950}951}952953void AnimationPlayerEditor::clear() {954track_editor->clear();955}956957void AnimationPlayerEditor::_animation_resource_edit() {958String current = _get_current();959if (current != String()) {960Ref<Animation> anim = player->get_animation(current);961EditorNode::get_singleton()->edit_resource(anim);962}963}964965void AnimationPlayerEditor::_animation_edit() {966String current = _get_current();967if (current != String()) {968Ref<Animation> anim = player->get_animation(current);969970track_editor->set_animation(anim, EditorNode::get_singleton()->is_resource_read_only(anim));971972Node *root = player->get_node_or_null(player->get_root_node());973if (root) {974track_editor->set_root(root);975}976} else {977track_editor->set_animation(Ref<Animation>(), true);978track_editor->set_root(nullptr);979}980}981982void AnimationPlayerEditor::_scale_changed(const String &p_scale) {983player->set_speed_scale(p_scale.to_float());984}985986void AnimationPlayerEditor::_update_animation() {987// the purpose of _update_animation is to reflect the current state988// of the animation player in the current editor..989990updating = true;991992if (player->is_playing()) {993stop->set_button_icon(pause_icon);994} else {995stop->set_button_icon(stop_icon);996}997998scale->set_text(String::num(player->get_speed_scale(), 2));999String current = player->get_assigned_animation();10001001for (int i = 0; i < animation->get_item_count(); i++) {1002if (animation->get_item_text(i) == current) {1003animation->select(i);1004break;1005}1006}10071008updating = false;1009}10101011void AnimationPlayerEditor::_update_player() {1012updating = true;10131014animation->clear();10151016tool_anim->set_disabled(player == nullptr);1017pin->set_disabled(player == nullptr);1018_set_controls_disabled(player == nullptr);10191020if (!player) {1021AnimationPlayerEditor::get_singleton()->get_track_editor()->update_keying();1022return;1023}10241025List<StringName> libraries;1026player->get_animation_library_list(&libraries);10271028int active_idx = -1;1029bool no_anims_found = true;1030bool global_animation_library_is_readonly = false;1031bool all_animation_libraries_are_readonly = libraries.size() > 0;10321033for (const StringName &K : libraries) {1034if (K != StringName()) {1035animation->add_separator(K);1036}10371038// Check if the global library is read-only since we want to disable options for adding/remove/renaming animations if it is.1039Ref<AnimationLibrary> anim_library = player->get_animation_library(K);1040bool is_animation_library_read_only = EditorNode::get_singleton()->is_resource_read_only(anim_library);1041if (!is_animation_library_read_only) {1042all_animation_libraries_are_readonly = false;1043} else {1044if (K == "") {1045global_animation_library_is_readonly = true;1046}1047}10481049List<StringName> animlist;1050anim_library->get_animation_list(&animlist);10511052for (const StringName &E : animlist) {1053String path = K;1054if (path != "") {1055path += "/";1056}1057path += E;1058animation->add_item(path);1059if (player->get_assigned_animation() == path) {1060active_idx = animation->get_selectable_item(true);1061}1062no_anims_found = false;1063}1064}1065#define ITEM_CHECK_DISABLED(m_item) tool_anim->get_popup()->set_item_disabled(tool_anim->get_popup()->get_item_index(m_item), all_animation_libraries_are_readonly || (no_anims_found && global_animation_library_is_readonly))1066ITEM_CHECK_DISABLED(TOOL_NEW_ANIM);1067#undef ITEM_CHECK_DISABLED10681069_update_animation_list_icons();10701071updating = false;1072if (active_idx != -1) {1073animation->select(active_idx);1074autoplay->set_pressed(animation->get_item_text(active_idx) == player->get_autoplay());1075_animation_selected(active_idx);1076} else if (animation->has_selectable_items()) {1077int item = animation->get_selectable_item();1078animation->select(item);1079autoplay->set_pressed(animation->get_item_text(item) == player->get_autoplay());1080_animation_selected(item);1081} else {1082_animation_selected(0);1083}10841085if (no_anims_found) {1086_set_controls_disabled(true);1087} else {1088String current = animation->get_item_text(animation->get_selected());1089Ref<Animation> anim = player->get_animation(current);10901091bool animation_library_is_readonly = EditorNode::get_singleton()->is_resource_read_only(anim);10921093track_editor->set_animation(anim, animation_library_is_readonly);1094Node *root = player->get_node_or_null(player->get_root_node());1095if (root) {1096track_editor->set_root(root);1097}1098}10991100_update_animation();1101}11021103void AnimationPlayerEditor::_set_controls_disabled(bool p_disabled) {1104frame->set_editable(!p_disabled);11051106stop->set_disabled(p_disabled);1107play->set_disabled(p_disabled);1108play_bw->set_disabled(p_disabled);1109play_bw_from->set_disabled(p_disabled);1110play_from->set_disabled(p_disabled);1111animation->set_disabled(p_disabled);1112autoplay->set_disabled(p_disabled);1113onion_toggle->set_disabled(p_disabled);1114onion_skinning->set_disabled(p_disabled);1115}11161117void AnimationPlayerEditor::_update_animation_list_icons() {1118for (int i = 0; i < animation->get_item_count(); i++) {1119String anim_name = animation->get_item_text(i);1120if (animation->is_item_disabled(i) || animation->is_item_separator(i)) {1121continue;1122}11231124Ref<Texture2D> icon;1125if (anim_name == player->get_autoplay()) {1126if (anim_name == SceneStringName(RESET)) {1127icon = autoplay_reset_icon;1128} else {1129icon = autoplay_icon;1130}1131} else if (anim_name == SceneStringName(RESET)) {1132icon = reset_icon;1133}11341135animation->set_item_icon(i, icon);1136}1137}11381139void AnimationPlayerEditor::_update_name_dialog_library_dropdown() {1140StringName current_library_name;1141if (animation->has_selectable_items()) {1142String current_animation_name = animation->get_item_text(animation->get_selected());1143Ref<Animation> current_animation = player->get_animation(current_animation_name);1144if (current_animation.is_valid()) {1145current_library_name = player->find_animation_library(current_animation);1146}1147}11481149List<StringName> libraries;1150player->get_animation_library_list(&libraries);1151library->clear();11521153int valid_library_count = 0;11541155// When [Global] isn't present, but other libraries are, add option of creating [Global].1156int index_offset = 0;1157if (!player->has_animation_library(StringName())) {1158library->add_item(String(TTR("[Global] (create)")));1159library->set_item_metadata(0, "");1160if (!libraries.is_empty()) {1161index_offset = 1;1162}1163valid_library_count++;1164}11651166int current_lib_id = index_offset; // Don't default to [Global] if it doesn't exist yet.1167for (const StringName &library_name : libraries) {1168if (!EditorNode::get_singleton()->is_resource_read_only(player->get_animation_library(library_name))) {1169library->add_item((library_name == StringName()) ? String(TTR("[Global]")) : String(library_name));1170library->set_item_metadata(valid_library_count, String(library_name));1171// Default to duplicating into same library.1172if (library_name == current_library_name) {1173current_library_name = library_name;1174current_lib_id = valid_library_count;1175}1176valid_library_count++;1177}1178}11791180// If our library name is empty, but we have valid libraries, we can check here to auto assign the first1181// one which isn't a read-only library.1182bool auto_assigning_non_global_library = false;1183if (current_library_name == StringName() && valid_library_count > 0) {1184for (const StringName &library_name : libraries) {1185if (!EditorNode::get_singleton()->is_resource_read_only(player->get_animation_library(library_name))) {1186current_library_name = library_name;1187current_lib_id = 0;1188if (library_name != StringName()) {1189auto_assigning_non_global_library = true;1190}1191break;1192}1193}1194}11951196if (library->get_item_count() > 0) {1197library->select(current_lib_id);1198if (library->get_item_count() > 1 || auto_assigning_non_global_library) {1199library->show();1200library->set_disabled(auto_assigning_non_global_library && library->get_item_count() == 1);1201} else {1202library->hide();1203}1204}1205}12061207void AnimationPlayerEditor::_update_playback_tooltips() {1208stop->set_tooltip_text(TTR("Pause/Stop Animation") + " (" + ED_GET_SHORTCUT("animation_editor/stop_animation")->get_as_text() + ")");1209play->set_tooltip_text(TTR("Play Animation from Start") + " (" + ED_GET_SHORTCUT("animation_editor/play_animation_from_start")->get_as_text() + ")");1210play_from->set_tooltip_text(TTR("Play Animation") + " (" + ED_GET_SHORTCUT("animation_editor/play_animation")->get_as_text() + ")");1211play_bw_from->set_tooltip_text(TTR("Play Animation Backwards") + " (" + ED_GET_SHORTCUT("animation_editor/play_animation_backwards")->get_as_text() + ")");1212play_bw->set_tooltip_text(TTR("Play Animation Backwards from End") + " (" + ED_GET_SHORTCUT("animation_editor/play_animation_from_end")->get_as_text() + ")");1213}12141215void AnimationPlayerEditor::_ensure_dummy_player() {1216bool dummy_exists = is_dummy && player && original_node;1217if (dummy_exists) {1218if (is_visible()) {1219player->set_active(true);1220original_node->set_editing(true);1221} else {1222player->set_active(false);1223original_node->set_editing(false);1224}1225}12261227int selected = animation->get_selected();1228autoplay->set_disabled(selected != -1 ? (animation->get_item_text(selected).is_empty() ? true : dummy_exists) : true);12291230// Show warning.1231if (track_editor) {1232track_editor->show_dummy_player_warning(dummy_exists);1233}1234}12351236void AnimationPlayerEditor::edit(AnimationMixer *p_node, AnimationPlayer *p_player, bool p_is_dummy) {1237if (player && pin->is_pressed()) {1238return; // Ignore, pinned.1239}12401241if (player) {1242if (player->is_connected(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated))) {1243player->disconnect(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated));1244}1245if (player->is_connected(SceneStringName(animation_finished), callable_mp(this, &AnimationPlayerEditor::_animation_finished))) {1246player->disconnect(SceneStringName(animation_finished), callable_mp(this, &AnimationPlayerEditor::_animation_finished));1247}1248if (player->is_connected(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed))) {1249player->disconnect(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed));1250}1251}12521253AnimationTree *tree = Object::cast_to<AnimationTree>(p_node);12541255if (tree) {1256if (tree->is_connected(SNAME("animation_player_changed"), callable_mp(this, &AnimationPlayerEditor::unpin))) {1257tree->disconnect(SNAME("animation_player_changed"), callable_mp(this, &AnimationPlayerEditor::unpin));1258}1259}12601261original_node = p_node;1262player = p_player;1263is_dummy = p_is_dummy;12641265if (tree) {1266if (!tree->is_connected(SNAME("animation_player_changed"), callable_mp(this, &AnimationPlayerEditor::unpin))) {1267tree->connect(SNAME("animation_player_changed"), callable_mp(this, &AnimationPlayerEditor::unpin));1268}1269}12701271if (player) {1272if (!player->is_connected(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated))) {1273player->connect(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated), CONNECT_DEFERRED);1274}1275if (!player->is_connected(SceneStringName(animation_finished), callable_mp(this, &AnimationPlayerEditor::_animation_finished))) {1276player->connect(SceneStringName(animation_finished), callable_mp(this, &AnimationPlayerEditor::_animation_finished));1277}1278if (!player->is_connected(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed))) {1279player->connect(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed));1280}1281_update_player();12821283if (onion.enabled) {1284if (animation->has_selectable_items()) {1285_start_onion_skinning();1286} else {1287_stop_onion_skinning();1288}1289}12901291track_editor->show_select_node_warning(false);1292} else {1293if (onion.enabled) {1294_stop_onion_skinning();1295}12961297track_editor->show_select_node_warning(true);1298}12991300library_editor->set_animation_mixer(fetch_mixer_for_library());13011302_ensure_dummy_player();1303}13041305void AnimationPlayerEditor::forward_force_draw_over_viewport(Control *p_overlay) {1306if (!onion.can_overlay) {1307return;1308}13091310// Can happen on viewport resize, at least.1311if (!_are_onion_layers_valid()) {1312return;1313}13141315RID ci = p_overlay->get_canvas_item();1316Rect2 src_rect = p_overlay->get_global_rect();1317// Re-flip since captures are already flipped.1318src_rect.position.y = onion.capture_size.y - (src_rect.position.y + src_rect.size.y);1319src_rect.size.y *= -1;13201321Rect2 dst_rect = Rect2(Point2(), p_overlay->get_size());13221323float alpha_step = 1.0 / (onion.steps + 1);13241325uint32_t capture_idx = 0;1326if (onion.past) {1327float alpha = 0.0f;1328do {1329alpha += alpha_step;13301331if (onion.captures_valid[capture_idx]) {1332RS::get_singleton()->canvas_item_add_texture_rect_region(1333ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[capture_idx]), src_rect, Color(1, 1, 1, alpha));1334}13351336capture_idx++;1337} while (capture_idx < onion.steps);1338}1339if (onion.future) {1340float alpha = 1.0f;1341uint32_t base_cidx = capture_idx;1342do {1343alpha -= alpha_step;13441345if (onion.captures_valid[capture_idx]) {1346RS::get_singleton()->canvas_item_add_texture_rect_region(1347ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[capture_idx]), src_rect, Color(1, 1, 1, alpha));1348}13491350capture_idx++;1351} while (capture_idx < base_cidx + onion.steps); // In case there's the present capture at the end, skip it.1352}1353}13541355void AnimationPlayerEditor::_animation_duplicate() {1356if (!animation->has_selectable_items()) {1357return;1358}13591360String current = animation->get_item_text(animation->get_selected());1361Ref<Animation> anim = player->get_animation(current);1362if (anim.is_null()) {1363return;1364}13651366int count = 2;1367String new_name = current;1368PackedStringArray split = new_name.split("_");1369int last_index = split.size() - 1;1370if (last_index > 0 && split[last_index].is_valid_int() && split[last_index].to_int() >= 0) {1371count = split[last_index].to_int();1372split.remove_at(last_index);1373new_name = String("_").join(split);1374}1375while (true) {1376String attempt = new_name;1377attempt += vformat("_%d", count);1378if (player->has_animation(attempt)) {1379count++;1380continue;1381}1382new_name = attempt;1383break;1384}13851386if (new_name.contains_char('/')) {1387// Discard library prefix.1388new_name = new_name.get_slicec('/', 1);1389}13901391_update_name_dialog_library_dropdown();13921393name_dialog_op = TOOL_DUPLICATE_ANIM;1394name_dialog->set_title(TTR("Duplicate Animation"));1395// TRANSLATORS: This is a label for the new name field in the "Duplicate Animation" dialog.1396name_title->set_text(TTR("Duplicated Animation Name:"));1397name->set_text(new_name);1398name_dialog->popup_centered(Size2(300, 90));1399name->select_all();1400name->grab_focus();1401}14021403Ref<Animation> AnimationPlayerEditor::_animation_clone(Ref<Animation> p_anim) {1404Ref<Animation> new_anim = memnew(Animation);1405List<PropertyInfo> plist;1406p_anim->get_property_list(&plist);14071408for (const PropertyInfo &E : plist) {1409if (E.usage & PROPERTY_USAGE_STORAGE) {1410new_anim->set(E.name, p_anim->get(E.name));1411}1412}1413new_anim->set_path("");14141415return new_anim;1416}14171418void AnimationPlayerEditor::_seek_value_changed(float p_value, bool p_timeline_only) {1419if (updating || !player || player->is_playing()) {1420return;1421};14221423updating = true;1424StringName current = player->get_assigned_animation();1425if (current.is_empty() || !player->has_animation(current)) {1426updating = false;1427current = "";1428return;1429};14301431Ref<Animation> anim;1432anim = player->get_animation(current);14331434double pos = CLAMP((double)anim->get_length() * (p_value / frame->get_max()), 0, (double)anim->get_length());1435if (track_editor->is_snap_timeline_enabled()) {1436pos = Math::snapped(pos, _get_editor_step());1437}1438pos = CLAMP(pos, 0, (double)anim->get_length() - CMP_EPSILON2); // Hack: Avoid fposmod with LOOP_LINEAR.14391440if (!p_timeline_only && anim.is_valid() && (!player->is_valid() || !Math::is_equal_approx(pos, player->get_current_animation_position()))) {1441player->seek_internal(pos, true, true, false);1442}14431444track_editor->set_anim_pos(pos);1445}14461447void AnimationPlayerEditor::_animation_player_changed(Object *p_pl) {1448_update_player();14491450if (blend_editor.dialog->is_visible()) {1451_update_animation_blend(); // Update.1452}14531454if (library_editor->is_visible()) {1455library_editor->update_tree();1456}1457}14581459void AnimationPlayerEditor::_animation_libraries_updated() {1460_animation_player_changed(player);1461}14621463void AnimationPlayerEditor::_list_changed() {1464if (is_visible_in_tree()) {1465_update_player();1466}1467}14681469void AnimationPlayerEditor::_animation_finished(const String &p_name) {1470finishing = true;1471}14721473void AnimationPlayerEditor::_current_animation_changed(const StringName &p_name) {1474if (is_visible_in_tree()) {1475if (finishing) {1476finishing = false; // Maybe redundant since it will be false in the AnimationPlayerEditor::_process(), but for safety.1477return;1478} else if (p_name.is_empty()) {1479// Means [stop].1480frame->set_value(0);1481track_editor->set_anim_pos(0);1482_update_animation();1483return;1484}1485Ref<Animation> anim = player->get_animation(p_name);1486if (anim.is_null()) {1487return;1488}14891490// Determine the read-only status of the animation's library and the libraries as a whole.1491List<StringName> libraries;1492player->get_animation_library_list(&libraries);14931494bool current_animation_library_is_readonly = false;1495bool all_animation_libraries_are_readonly = true;1496for (const StringName &K : libraries) {1497Ref<AnimationLibrary> anim_library = player->get_animation_library(K);1498bool animation_library_is_readonly = EditorNode::get_singleton()->is_resource_read_only(anim_library);1499if (!animation_library_is_readonly) {1500all_animation_libraries_are_readonly = false;1501}15021503List<StringName> animlist;1504anim_library->get_animation_list(&animlist);1505bool animation_found = false;1506for (const StringName &E : animlist) {1507String path = K;1508if (path != "") {1509path += "/";1510}1511path += E;1512if (p_name == path) {1513current_animation_library_is_readonly = animation_library_is_readonly;1514break;1515}1516}1517if (animation_found) {1518break;1519}1520}15211522StringName library_name = player->find_animation_library(anim);15231524bool animation_is_readonly = EditorNode::get_singleton()->is_resource_read_only(anim);15251526track_editor->set_animation(anim, animation_is_readonly);1527_update_animation();15281529#define ITEM_CHECK_DISABLED(m_item) tool_anim->get_popup()->set_item_disabled(tool_anim->get_popup()->get_item_index(m_item), false)1530ITEM_CHECK_DISABLED(TOOL_EDIT_TRANSITIONS);1531ITEM_CHECK_DISABLED(TOOL_EDIT_RESOURCE);1532#undef ITEM_CHECK_DISABLED15331534#define ITEM_CHECK_DISABLED(m_item) tool_anim->get_popup()->set_item_disabled(tool_anim->get_popup()->get_item_index(m_item), current_animation_library_is_readonly)1535ITEM_CHECK_DISABLED(TOOL_RENAME_ANIM);1536ITEM_CHECK_DISABLED(TOOL_REMOVE_ANIM);1537#undef ITEM_CHECK_DISABLED15381539#define ITEM_CHECK_DISABLED(m_item) tool_anim->get_popup()->set_item_disabled(tool_anim->get_popup()->get_item_index(m_item), all_animation_libraries_are_readonly)1540ITEM_CHECK_DISABLED(TOOL_DUPLICATE_ANIM);1541#undef ITEM_CHECK_DISABLED1542}1543}15441545void AnimationPlayerEditor::_animation_key_editor_anim_len_changed(float p_len) {1546frame->set_max(p_len);1547}1548void AnimationPlayerEditor::_animation_key_editor_seek(float p_pos, bool p_timeline_only, bool p_update_position_only) {1549timeline_position = p_pos;15501551if (!is_visible_in_tree() ||1552p_update_position_only ||1553!player ||1554player->is_playing() ||1555!player->has_animation(player->get_assigned_animation())) {1556return;1557}15581559updating = true;1560frame->set_value(track_editor->is_snap_timeline_enabled() ? Math::snapped(p_pos, _get_editor_step()) : p_pos);1561updating = false;1562_seek_value_changed(p_pos, p_timeline_only);1563}15641565void AnimationPlayerEditor::_animation_update_key_frame() {1566if (player) {1567player->advance(0);1568}1569}15701571void AnimationPlayerEditor::_animation_tool_menu(int p_option) {1572String current = _get_current();15731574Ref<Animation> anim;1575if (!current.is_empty()) {1576anim = player->get_animation(current);1577}15781579switch (p_option) {1580case TOOL_NEW_ANIM: {1581_animation_new();1582} break;1583case TOOL_ANIM_LIBRARY: {1584library_editor->set_animation_mixer(fetch_mixer_for_library());1585library_editor->show_dialog();1586} break;1587case TOOL_DUPLICATE_ANIM: {1588_animation_duplicate();1589} break;1590case TOOL_RENAME_ANIM: {1591_animation_rename();1592} break;1593case TOOL_EDIT_TRANSITIONS: {1594_edit_animation_blend();1595} break;1596case TOOL_REMOVE_ANIM: {1597_animation_remove();1598} break;1599case TOOL_EDIT_RESOURCE: {1600if (anim.is_valid()) {1601EditorNode::get_singleton()->edit_resource(anim);1602}1603} break;1604}1605}16061607void AnimationPlayerEditor::_onion_skinning_menu(int p_option) {1608PopupMenu *menu = onion_skinning->get_popup();1609int idx = menu->get_item_index(p_option);16101611switch (p_option) {1612case ONION_SKINNING_ENABLE: {1613onion.enabled = !onion.enabled;16141615if (onion.enabled) {1616if (get_player() && !get_player()->has_animation(SceneStringName(RESET))) {1617EditorNode::get_singleton()->show_warning(TTR("Onion skinning requires a RESET animation."));1618}1619_start_onion_skinning(); // It will check for RESET animation anyway.1620} else {1621_stop_onion_skinning();1622}16231624} break;1625case ONION_SKINNING_PAST: {1626// Ensure at least one of past/future is checked.1627onion.past = onion.future ? !onion.past : true;1628menu->set_item_checked(idx, onion.past);1629} break;1630case ONION_SKINNING_FUTURE: {1631// Ensure at least one of past/future is checked.1632onion.future = onion.past ? !onion.future : true;1633menu->set_item_checked(idx, onion.future);1634} break;1635case ONION_SKINNING_1_STEP: // Fall-through.1636case ONION_SKINNING_2_STEPS:1637case ONION_SKINNING_3_STEPS: {1638onion.steps = (p_option - ONION_SKINNING_1_STEP) + 1;1639int one_frame_idx = menu->get_item_index(ONION_SKINNING_1_STEP);1640for (int i = 0; i <= ONION_SKINNING_LAST_STEPS_OPTION - ONION_SKINNING_1_STEP; i++) {1641menu->set_item_checked(one_frame_idx + i, (int)onion.steps == i + 1);1642}1643} break;1644case ONION_SKINNING_DIFFERENCES_ONLY: {1645onion.differences_only = !onion.differences_only;1646menu->set_item_checked(idx, onion.differences_only);1647} break;1648case ONION_SKINNING_FORCE_WHITE_MODULATE: {1649onion.force_white_modulate = !onion.force_white_modulate;1650menu->set_item_checked(idx, onion.force_white_modulate);1651} break;1652case ONION_SKINNING_INCLUDE_GIZMOS: {1653onion.include_gizmos = !onion.include_gizmos;1654menu->set_item_checked(idx, onion.include_gizmos);1655} break;1656}1657}16581659void AnimationPlayerEditor::shortcut_input(const Ref<InputEvent> &p_ev) {1660ERR_FAIL_COND(p_ev.is_null());16611662Ref<InputEventKey> k = p_ev;1663if (is_visible_in_tree() && k.is_valid() && k->is_pressed() && !k->is_echo()) {1664if (ED_IS_SHORTCUT("animation_editor/stop_animation", p_ev)) {1665_stop_pressed();1666accept_event();1667} else if (ED_IS_SHORTCUT("animation_editor/play_animation", p_ev)) {1668_play_from_pressed();1669accept_event();1670} else if (ED_IS_SHORTCUT("animation_editor/play_animation_backwards", p_ev)) {1671_play_bw_from_pressed();1672accept_event();1673} else if (ED_IS_SHORTCUT("animation_editor/play_animation_from_start", p_ev)) {1674_play_pressed();1675accept_event();1676} else if (ED_IS_SHORTCUT("animation_editor/play_animation_from_end", p_ev)) {1677_play_bw_pressed();1678accept_event();1679} else if (ED_IS_SHORTCUT("animation_editor/go_to_next_keyframe", p_ev)) {1680go_to_nearest_keyframe(false);1681accept_event();1682} else if (ED_IS_SHORTCUT("animation_editor/go_to_previous_keyframe", p_ev)) {1683go_to_nearest_keyframe(true);1684accept_event();1685}1686}1687}16881689void AnimationPlayerEditor::_editor_visibility_changed() {1690if (is_visible() && animation->has_selectable_items()) {1691_start_onion_skinning();1692}1693}16941695bool AnimationPlayerEditor::_are_onion_layers_valid() {1696ERR_FAIL_COND_V(!onion.past && !onion.future, false);16971698Size2 capture_size = DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID);1699return onion.captures.size() == onion.get_capture_count() && onion.capture_size == capture_size;1700}17011702void AnimationPlayerEditor::_allocate_onion_layers() {1703_free_onion_layers();17041705int captures = onion.get_capture_count();1706Size2 capture_size = DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID);17071708onion.captures.resize(captures);1709onion.captures_valid.resize(captures);17101711for (int i = 0; i < captures; i++) {1712bool is_present = onion.differences_only && i == captures - 1;17131714// Each capture is a viewport with a canvas item attached that renders a full-size rect with the contents of the main viewport.1715onion.captures[i] = RS::get_singleton()->viewport_create();17161717RS::get_singleton()->viewport_set_size(onion.captures[i], capture_size.width, capture_size.height);1718RS::get_singleton()->viewport_set_update_mode(onion.captures[i], RS::VIEWPORT_UPDATE_ALWAYS);1719RS::get_singleton()->viewport_set_transparent_background(onion.captures[i], !is_present);1720RS::get_singleton()->viewport_attach_canvas(onion.captures[i], onion.capture.canvas);1721}17221723// Reset the capture canvas item to the current root viewport texture (defensive).1724RS::get_singleton()->canvas_item_clear(onion.capture.canvas_item);1725RS::get_singleton()->canvas_item_add_texture_rect(onion.capture.canvas_item, Rect2(Point2(), Point2(capture_size.x, -capture_size.y)), get_tree()->get_root()->get_texture()->get_rid());17261727onion.capture_size = capture_size;1728}17291730void AnimationPlayerEditor::_free_onion_layers() {1731for (uint32_t i = 0; i < onion.captures.size(); i++) {1732if (onion.captures[i].is_valid()) {1733RS::get_singleton()->free_rid(onion.captures[i]);1734}1735}1736onion.captures.clear();1737onion.captures_valid.clear();1738}17391740void AnimationPlayerEditor::_prepare_onion_layers_1() {1741// This would be called per viewport and we want to act once only.1742int64_t cur_frame = get_tree()->get_frame();1743if (cur_frame == onion.last_frame) {1744return;1745}17461747if (!onion.enabled || !is_visible() || !get_player() || !get_player()->has_animation(SceneStringName(RESET))) {1748_stop_onion_skinning();1749return;1750}17511752onion.last_frame = cur_frame;17531754// Refresh viewports with no onion layers overlaid.1755onion.can_overlay = false;1756plugin->update_overlays();17571758if (player->is_playing()) {1759return;1760}17611762// And go to next step afterwards.1763callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_2_prolog).call_deferred();1764}17651766void AnimationPlayerEditor::_prepare_onion_layers_2_prolog() {1767Ref<Animation> anim = player->get_animation(player->get_assigned_animation());1768if (anim.is_null()) {1769return;1770}17711772if (!_are_onion_layers_valid()) {1773_allocate_onion_layers();1774}17751776// Hide superfluous elements that would make the overlay unnecessary cluttered.1777if (Node3DEditor::get_singleton()->is_visible()) {1778// 3D1779onion.temp.spatial_edit_state = Node3DEditor::get_singleton()->get_state();1780Dictionary new_state = onion.temp.spatial_edit_state.duplicate();1781new_state["show_grid"] = false;1782new_state["show_origin"] = false;1783Array orig_vp = onion.temp.spatial_edit_state["viewports"];1784Array vp;1785vp.resize(4);1786for (int i = 0; i < vp.size(); i++) {1787Dictionary d = ((Dictionary)orig_vp[i]).duplicate();1788d["use_environment"] = false;1789d["doppler"] = false;1790d["listener"] = false;1791d["gizmos"] = onion.include_gizmos ? d["gizmos"] : Variant(false);1792d["information"] = false;1793vp[i] = d;1794}1795new_state["viewports"] = vp;1796// TODO: Save/restore only affected entries.1797Node3DEditor::get_singleton()->set_state(new_state);1798} else {1799// CanvasItemEditor.1800onion.temp.canvas_edit_state = CanvasItemEditor::get_singleton()->get_state();1801Dictionary new_state = onion.temp.canvas_edit_state.duplicate();1802new_state["show_origin"] = false;1803new_state["show_grid"] = false;1804new_state["show_rulers"] = false;1805new_state["show_guides"] = false;1806new_state["show_helpers"] = false;1807new_state["show_zoom_control"] = false;1808new_state["show_edit_locks"] = false;1809new_state["grid_visibility"] = 2; // TODO: Expose CanvasItemEditor::GRID_VISIBILITY_HIDE somehow and use it.1810new_state["show_transformation_gizmos"] = onion.include_gizmos ? new_state["gizmos"] : Variant(false);1811// TODO: Save/restore only affected entries.1812CanvasItemEditor::get_singleton()->set_state(new_state);1813}18141815// Tweak the root viewport to ensure it's rendered before our target.1816RID root_vp = get_tree()->get_root()->get_viewport_rid();1817onion.temp.screen_rect = Rect2(Vector2(), DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID));1818RS::get_singleton()->viewport_attach_to_screen(root_vp, Rect2(), DisplayServer::INVALID_WINDOW_ID);1819RS::get_singleton()->viewport_set_update_mode(root_vp, RS::VIEWPORT_UPDATE_ALWAYS);18201821RID present_rid;1822if (onion.differences_only) {1823// Capture present scene as it is.1824RS::get_singleton()->canvas_item_set_material(onion.capture.canvas_item, RID());1825present_rid = onion.captures[onion.captures.size() - 1];1826RS::get_singleton()->viewport_set_active(present_rid, true);1827RS::get_singleton()->viewport_set_parent_viewport(root_vp, present_rid);1828RS::get_singleton()->draw(false);1829RS::get_singleton()->viewport_set_active(present_rid, false);1830}18311832// Backup current animation state.1833onion.temp.anim_values_backup = player->make_backup();1834onion.temp.anim_player_position = player->get_current_animation_position();18351836// Render every past/future step with the capture shader.18371838RS::get_singleton()->canvas_item_set_material(onion.capture.canvas_item, onion.capture.material->get_rid());1839onion.capture.material->set_shader_parameter("bkg_color", GLOBAL_GET("rendering/environment/defaults/default_clear_color"));1840onion.capture.material->set_shader_parameter("differences_only", onion.differences_only);1841onion.capture.material->set_shader_parameter("present", onion.differences_only ? RS::get_singleton()->viewport_get_texture(present_rid) : RID());1842onion.capture.material->set_shader_parameter("dir_color", onion.force_white_modulate ? Color(1, 1, 1) : Color(EDITOR_GET("editors/animation/onion_layers_past_color")));18431844uint32_t p_capture_idx = 0;1845int first_step_offset = onion.past ? -(int)onion.steps : 0;1846_prepare_onion_layers_2_step_prepare(first_step_offset, p_capture_idx);1847}18481849void AnimationPlayerEditor::_prepare_onion_layers_2_step_prepare(int p_step_offset, uint32_t p_capture_idx) {1850uint32_t next_capture_idx = p_capture_idx;1851if (p_step_offset == 0) {1852// Skip present step and switch to the color of future.1853if (!onion.force_white_modulate) {1854onion.capture.material->set_shader_parameter("dir_color", EDITOR_GET("editors/animation/onion_layers_future_color"));1855}1856} else {1857Ref<Animation> anim = player->get_animation(player->get_assigned_animation());1858double pos = onion.temp.anim_player_position + p_step_offset * anim->get_step();18591860bool valid = anim->get_loop_mode() != Animation::LOOP_NONE || (pos >= 0 && pos <= anim->get_length());1861onion.captures_valid[p_capture_idx] = valid;1862if (valid) {1863player->seek_internal(pos, true, true, false);1864OS::get_singleton()->get_main_loop()->process(0);1865// This is the key: process the frame and let all callbacks/updates/notifications happen1866// so everything (transforms, skeletons, etc.) is up-to-date visually.1867callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_2_step_capture).call_deferred(p_step_offset, p_capture_idx);1868return;1869} else {1870next_capture_idx++;1871}1872}18731874int last_step_offset = onion.future ? onion.steps : 0;1875if (p_step_offset < last_step_offset) {1876_prepare_onion_layers_2_step_prepare(p_step_offset + 1, next_capture_idx);1877} else {1878_prepare_onion_layers_2_epilog();1879}1880}18811882void AnimationPlayerEditor::_prepare_onion_layers_2_step_capture(int p_step_offset, uint32_t p_capture_idx) {1883DEV_ASSERT(p_step_offset != 0);1884DEV_ASSERT(onion.captures_valid[p_capture_idx]);18851886RID root_vp = get_tree()->get_root()->get_viewport_rid();1887RS::get_singleton()->viewport_set_active(onion.captures[p_capture_idx], true);1888RS::get_singleton()->viewport_set_parent_viewport(root_vp, onion.captures[p_capture_idx]);1889RS::get_singleton()->draw(false);1890RS::get_singleton()->viewport_set_active(onion.captures[p_capture_idx], false);18911892int last_step_offset = onion.future ? onion.steps : 0;1893if (p_step_offset < last_step_offset) {1894_prepare_onion_layers_2_step_prepare(p_step_offset + 1, p_capture_idx + 1);1895} else {1896_prepare_onion_layers_2_epilog();1897}1898}18991900void AnimationPlayerEditor::_prepare_onion_layers_2_epilog() {1901// Restore root viewport.1902RID root_vp = get_tree()->get_root()->get_viewport_rid();1903RS::get_singleton()->viewport_set_parent_viewport(root_vp, RID());1904RS::get_singleton()->viewport_attach_to_screen(root_vp, onion.temp.screen_rect, DisplayServer::MAIN_WINDOW_ID);1905RS::get_singleton()->viewport_set_update_mode(root_vp, RS::VIEWPORT_UPDATE_WHEN_VISIBLE);19061907// Restore animation state.1908// Here we're combine the power of seeking back to the original position and1909// restoring the values backup. In most cases they will bring the same value back,1910// but there are cases handled by one that the other can't.1911// Namely:1912// - Seeking won't restore any values that may have been modified by the user1913// in the node after the last time the AnimationPlayer updated it.1914// - Restoring the backup won't account for values that are not directly involved1915// in the animation but a consequence of them (e.g., SkeletonModification2DLookAt).1916// FIXME: Since backup of values is based on the reset animation, only values1917// backed by a proper reset animation will work correctly with onion1918// skinning and the possibility to restore the values mentioned in the1919// first point above is gone. Still good enough.1920player->seek_internal(onion.temp.anim_player_position, true, true, false);1921player->restore(onion.temp.anim_values_backup);19221923// Restore state of main editors.1924if (Node3DEditor::get_singleton()->is_visible()) {1925// 3D1926Node3DEditor::get_singleton()->set_state(onion.temp.spatial_edit_state);1927} else { // CanvasItemEditor1928// 2D1929CanvasItemEditor::get_singleton()->set_state(onion.temp.canvas_edit_state);1930}19311932// Update viewports with skin layers overlaid for the actual engine loop render.1933onion.can_overlay = true;1934plugin->update_overlays();1935}19361937void AnimationPlayerEditor::_start_onion_skinning() {1938if (get_player() && !get_player()->has_animation(SceneStringName(RESET))) {1939onion.enabled = false;1940onion_toggle->set_pressed_no_signal(false);1941return;1942}1943if (!get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1))) {1944get_tree()->connect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1));1945}1946}19471948void AnimationPlayerEditor::_stop_onion_skinning() {1949if (get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1))) {1950get_tree()->disconnect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1));19511952_free_onion_layers();19531954// Clean up.1955onion.can_overlay = false;1956plugin->update_overlays();1957onion.temp = {};1958}1959}19601961void AnimationPlayerEditor::_pin_pressed() {1962SceneTreeDock::get_singleton()->get_tree_editor()->update_tree();1963}19641965AnimationMixer *AnimationPlayerEditor::fetch_mixer_for_library() const {1966if (!original_node) {1967return nullptr;1968}1969// Does AnimationTree have AnimationPlayer?1970if (original_node->is_class("AnimationTree")) {1971AnimationTree *src_tree = Object::cast_to<AnimationTree>(original_node);1972Node *src_player = src_tree->get_node_or_null(src_tree->get_animation_player());1973if (src_player) {1974return Object::cast_to<AnimationMixer>(src_player);1975}1976}1977return original_node;1978}19791980Node *AnimationPlayerEditor::get_cached_root_node() const {1981return ObjectDB::get_instance<Node>(cached_root_node_id);1982}19831984bool AnimationPlayerEditor::_validate_tracks(const Ref<Animation> p_anim) {1985bool is_valid = true;1986if (p_anim.is_null()) {1987return true; // There is a problem outside of the animation track.1988}1989int len = p_anim->get_track_count();1990for (int i = 0; i < len; i++) {1991Animation::TrackType ttype = p_anim->track_get_type(i);1992if (ttype == Animation::TYPE_ROTATION_3D) {1993int key_len = p_anim->track_get_key_count(i);1994for (int j = 0; j < key_len; j++) {1995Quaternion q;1996p_anim->rotation_track_get_key(i, j, &q);1997ERR_BREAK_EDMSG(!q.is_normalized(), "AnimationPlayer: '" + player->get_name() + "', Animation: '" + player->get_current_animation() + "', 3D Rotation Track: '" + String(p_anim->track_get_path(i)) + "' contains unnormalized Quaternion key.");1998}1999} else if (ttype == Animation::TYPE_VALUE) {2000int key_len = p_anim->track_get_key_count(i);2001if (key_len == 0) {2002continue;2003}2004switch (p_anim->track_get_key_value(i, 0).get_type()) {2005case Variant::QUATERNION: {2006for (int j = 0; j < key_len; j++) {2007Quaternion q = Quaternion(p_anim->track_get_key_value(i, j));2008if (!q.is_normalized()) {2009is_valid = false;2010ERR_BREAK_EDMSG(true, "AnimationPlayer: '" + player->get_name() + "', Animation: '" + player->get_current_animation() + "', Value Track: '" + String(p_anim->track_get_path(i)) + "' contains unnormalized Quaternion key.");2011}2012}2013} break;2014case Variant::TRANSFORM3D: {2015for (int j = 0; j < key_len; j++) {2016Transform3D t = Transform3D(p_anim->track_get_key_value(i, j));2017if (!t.basis.orthonormalized().is_rotation()) {2018is_valid = false;2019ERR_BREAK_EDMSG(true, "AnimationPlayer: '" + player->get_name() + "', Animation: '" + player->get_current_animation() + "', Value Track: '" + String(p_anim->track_get_path(i)) + "' contains corrupted basis (some axes are too close other axis or scaled by zero) Transform3D key.");2020}2021}2022} break;2023default: {2024} break;2025}2026}2027}2028return is_valid;2029}20302031void AnimationPlayerEditor::_bind_methods() {2032// Needed for UndoRedo.2033ClassDB::bind_method(D_METHOD("_animation_player_changed"), &AnimationPlayerEditor::_animation_player_changed);2034ClassDB::bind_method(D_METHOD("_animation_update_key_frame"), &AnimationPlayerEditor::_animation_update_key_frame);2035ClassDB::bind_method(D_METHOD("_start_onion_skinning"), &AnimationPlayerEditor::_start_onion_skinning);2036ClassDB::bind_method(D_METHOD("_stop_onion_skinning"), &AnimationPlayerEditor::_stop_onion_skinning);20372038ADD_SIGNAL(MethodInfo("animation_selected", PropertyInfo(Variant::STRING, "name")));2039}20402041AnimationPlayerEditor *AnimationPlayerEditor::singleton = nullptr;20422043AnimationPlayer *AnimationPlayerEditor::get_player() const {2044return player;2045}20462047AnimationMixer *AnimationPlayerEditor::get_editing_node() const {2048return original_node;2049}20502051AnimationPlayerEditor::AnimationPlayerEditor(AnimationPlayerEditorPlugin *p_plugin) {2052plugin = p_plugin;2053singleton = this;20542055set_name(TTRC("Animation"));2056set_icon_name("Animation");2057set_dock_shortcut(ED_SHORTCUT_AND_COMMAND("bottom_panels/toggle_animation_bottom_panel", TTRC("Toggle Animation Dock"), KeyModifierMask::ALT | Key::N));2058set_default_slot(EditorDock::DOCK_SLOT_BOTTOM);2059set_available_layouts(EditorDock::DOCK_LAYOUT_HORIZONTAL | EditorDock::DOCK_LAYOUT_FLOATING);20602061set_focus_mode(FOCUS_ALL);2062set_process_shortcut_input(true);20632064VBoxContainer *main_vbox_container = memnew(VBoxContainer);2065add_child(main_vbox_container);2066HBoxContainer *hb = memnew(HBoxContainer);2067main_vbox_container->add_child(hb);20682069HBoxContainer *playback_container = memnew(HBoxContainer);2070playback_container->set_layout_direction(LAYOUT_DIRECTION_LTR);2071hb->add_child(playback_container);20722073play_bw_from = memnew(Button);2074play_bw_from->set_theme_type_variation(SceneStringName(FlatButton));2075playback_container->add_child(play_bw_from);20762077play_bw = memnew(Button);2078play_bw->set_theme_type_variation(SceneStringName(FlatButton));2079playback_container->add_child(play_bw);20802081stop = memnew(Button);2082stop->set_theme_type_variation(SceneStringName(FlatButton));2083playback_container->add_child(stop);20842085play = memnew(Button);2086play->set_theme_type_variation(SceneStringName(FlatButton));2087playback_container->add_child(play);20882089play_from = memnew(Button);2090play_from->set_theme_type_variation(SceneStringName(FlatButton));2091playback_container->add_child(play_from);20922093frame = memnew(SpinBox);2094hb->add_child(frame);2095frame->set_custom_minimum_size(Size2(80, 0) * EDSCALE);2096frame->set_stretch_ratio(2);2097frame->set_step(0.0001);2098frame->set_tooltip_text(TTR("Animation position (in seconds)."));20992100hb->add_child(memnew(VSeparator));21012102scale = memnew(LineEdit);2103hb->add_child(scale);2104scale->set_h_size_flags(SIZE_EXPAND_FILL);2105scale->set_stretch_ratio(1);2106scale->set_tooltip_text(TTR("Scale animation playback globally for the node."));2107scale->hide();21082109delete_dialog = memnew(ConfirmationDialog);2110add_child(delete_dialog);2111delete_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationPlayerEditor::_animation_remove_confirmed));21122113tool_anim = memnew(MenuButton);2114tool_anim->set_shortcut_context(this);2115tool_anim->set_flat(false);2116tool_anim->set_tooltip_text(TTR("Animation Tools"));2117tool_anim->set_text(TTR("Animation"));2118tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/new_animation", TTRC("New...")), TOOL_NEW_ANIM);2119tool_anim->get_popup()->add_separator();2120tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/animation_libraries", TTRC("Manage Animations...")), TOOL_ANIM_LIBRARY);2121tool_anim->get_popup()->add_separator();2122tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/duplicate_animation", TTRC("Duplicate...")), TOOL_DUPLICATE_ANIM);2123tool_anim->get_popup()->add_separator();2124tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/rename_animation", TTRC("Rename...")), TOOL_RENAME_ANIM);2125tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/edit_transitions", TTRC("Edit Transitions...")), TOOL_EDIT_TRANSITIONS);2126tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/open_animation_in_inspector", TTRC("Open in Inspector")), TOOL_EDIT_RESOURCE);2127tool_anim->get_popup()->add_separator();2128tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/remove_animation", TTRC("Remove")), TOOL_REMOVE_ANIM);2129tool_anim->set_disabled(true);2130tool_anim->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationPlayerEditor::_animation_tool_menu));2131hb->add_child(tool_anim);21322133animation = memnew(OptionButton);2134hb->add_child(animation);2135animation->set_accessibility_name(TTRC("Animation"));2136animation->set_h_size_flags(SIZE_EXPAND_FILL);2137animation->set_tooltip_text(TTR("Display list of animations in player."));2138animation->set_clip_text(true);2139animation->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);21402141autoplay = memnew(Button);2142autoplay->set_theme_type_variation(SceneStringName(FlatButton));2143hb->add_child(autoplay);2144autoplay->set_tooltip_text(TTR("Autoplay on Load"));21452146hb->add_child(memnew(VSeparator));21472148track_editor = memnew(AnimationTrackEditor);2149hb->add_child(track_editor->get_edit_menu());21502151hb->add_child(memnew(VSeparator));21522153onion_toggle = memnew(Button);2154onion_toggle->set_theme_type_variation(SceneStringName(FlatButton));2155onion_toggle->set_toggle_mode(true);2156onion_toggle->set_tooltip_text(TTR("Enable Onion Skinning"));2157onion_toggle->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_onion_skinning_menu).bind(ONION_SKINNING_ENABLE));2158hb->add_child(onion_toggle);21592160onion_skinning = memnew(MenuButton);2161onion_skinning->set_accessibility_name(TTRC("Onion Skinning Options"));2162onion_skinning->set_flat(false);2163onion_skinning->set_theme_type_variation("FlatMenuButton");2164onion_skinning->set_tooltip_text(TTR("Onion Skinning Options"));2165onion_skinning->get_popup()->add_separator(TTR("Directions"));2166// TRANSLATORS: Opposite of "Future", refers to a direction in animation onion skinning.2167onion_skinning->get_popup()->add_check_item(TTR("Past"), ONION_SKINNING_PAST);2168onion_skinning->get_popup()->set_item_checked(-1, true);2169// TRANSLATORS: Opposite of "Past", refers to a direction in animation onion skinning.2170onion_skinning->get_popup()->add_check_item(TTR("Future"), ONION_SKINNING_FUTURE);2171onion_skinning->get_popup()->add_separator(TTR("Depth"));2172onion_skinning->get_popup()->add_radio_check_item(TTR("1 step"), ONION_SKINNING_1_STEP);2173onion_skinning->get_popup()->set_item_checked(-1, true);2174onion_skinning->get_popup()->add_radio_check_item(TTR("2 steps"), ONION_SKINNING_2_STEPS);2175onion_skinning->get_popup()->add_radio_check_item(TTR("3 steps"), ONION_SKINNING_3_STEPS);2176onion_skinning->get_popup()->add_separator();2177onion_skinning->get_popup()->add_check_item(TTR("Differences Only"), ONION_SKINNING_DIFFERENCES_ONLY);2178onion_skinning->get_popup()->add_check_item(TTR("Force White Modulate"), ONION_SKINNING_FORCE_WHITE_MODULATE);2179onion_skinning->get_popup()->add_check_item(TTR("Include Gizmos (3D)"), ONION_SKINNING_INCLUDE_GIZMOS);2180onion_skinning->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationPlayerEditor::_onion_skinning_menu));2181hb->add_child(onion_skinning);21822183hb->add_child(memnew(VSeparator));21842185pin = memnew(Button);2186pin->set_theme_type_variation(SceneStringName(FlatButton));2187pin->set_toggle_mode(true);2188pin->set_tooltip_text(TTR("Pin AnimationPlayer"));2189hb->add_child(pin);2190pin->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_pin_pressed));21912192file = memnew(EditorFileDialog);2193add_child(file);21942195name_dialog = memnew(ConfirmationDialog);2196name_dialog->set_title(TTR("Create New Animation"));2197name_dialog->set_hide_on_ok(false);2198add_child(name_dialog);2199VBoxContainer *vb = memnew(VBoxContainer);2200name_dialog->add_child(vb);22012202name_title = memnew(Label(TTR("Animation Name:")));2203vb->add_child(name_title);22042205HBoxContainer *name_hb = memnew(HBoxContainer);2206name = memnew(LineEdit);2207name_hb->add_child(name);2208name->set_h_size_flags(SIZE_EXPAND_FILL);2209library = memnew(OptionButton);2210name_hb->add_child(library);2211library->hide();2212vb->add_child(name_hb);2213name_dialog->register_text_enter(name);22142215error_dialog = memnew(AcceptDialog);2216error_dialog->set_ok_button_text(TTR("Close"));2217error_dialog->set_title(TTR("Error!"));2218name_dialog->add_child(error_dialog);22192220name_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationPlayerEditor::_animation_name_edited));22212222blend_editor.dialog = memnew(AcceptDialog);2223blend_editor.dialog->set_title(TTR("Cross-Animation Blend Times"));2224blend_editor.dialog->set_ok_button_text(TTR("Close"));2225blend_editor.dialog->set_hide_on_ok(true);2226add_child(blend_editor.dialog);22272228VBoxContainer *blend_vb = memnew(VBoxContainer);2229blend_editor.dialog->add_child(blend_vb);22302231blend_editor.tree = memnew(Tree);2232blend_editor.tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);2233blend_editor.tree->set_hide_root(true);2234blend_editor.tree->set_columns(2);2235blend_editor.tree->set_column_expand_ratio(0, 10);2236blend_editor.tree->set_column_clip_content(0, true);2237blend_editor.tree->set_column_expand_ratio(1, 3);2238blend_editor.tree->set_column_clip_content(1, true);2239blend_vb->add_margin_child(TTR("Blend Times:"), blend_editor.tree, true);2240blend_editor.tree->connect(SNAME("item_edited"), callable_mp(this, &AnimationPlayerEditor::_blend_edited));22412242blend_editor.next = memnew(OptionButton);2243blend_editor.next->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);2244blend_editor.next->connect(SceneStringName(item_selected), callable_mp(this, &AnimationPlayerEditor::_blend_editor_next_changed));2245blend_vb->add_margin_child(TTR("Next (Auto Queue):"), blend_editor.next);22462247autoplay->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_autoplay_pressed));2248autoplay->set_toggle_mode(true);2249play->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_play_pressed));2250play_from->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_play_from_pressed));2251play_bw->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_play_bw_pressed));2252play_bw_from->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_play_bw_from_pressed));2253stop->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_stop_pressed));22542255animation->connect(SceneStringName(item_selected), callable_mp(this, &AnimationPlayerEditor::_animation_selected));22562257frame->connect(SceneStringName(value_changed), callable_mp(this, &AnimationPlayerEditor::_seek_value_changed).bind(false));2258scale->connect(SceneStringName(text_submitted), callable_mp(this, &AnimationPlayerEditor::_scale_changed));22592260main_vbox_container->add_child(track_editor);2261track_editor->set_v_size_flags(SIZE_EXPAND_FILL);2262track_editor->connect(SNAME("timeline_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_key_editor_seek));2263track_editor->connect(SNAME("animation_len_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_key_editor_anim_len_changed));22642265_update_player();22662267library_editor = memnew(AnimationLibraryEditor);2268add_child(library_editor);2269library_editor->connect(SNAME("update_editor"), callable_mp(this, &AnimationPlayerEditor::_animation_player_changed));22702271// Onion skinning.22722273track_editor->connect(SceneStringName(visibility_changed), callable_mp(this, &AnimationPlayerEditor::_editor_visibility_changed));22742275onion.capture.canvas = RS::get_singleton()->canvas_create();2276onion.capture.canvas_item = RS::get_singleton()->canvas_item_create();2277RS::get_singleton()->canvas_item_set_parent(onion.capture.canvas_item, onion.capture.canvas);22782279onion.capture.material.instantiate();22802281onion.capture.shader.instantiate();2282onion.capture.shader->set_code(R"(2283// Animation editor onion skinning shader.22842285shader_type canvas_item;22862287uniform vec4 bkg_color;2288uniform vec4 dir_color;2289uniform bool differences_only;2290uniform sampler2D present;22912292float zero_if_equal(vec4 a, vec4 b) {2293return smoothstep(0.0, 0.005, length(a.rgb - b.rgb) / sqrt(3.0));2294}22952296void fragment() {2297vec4 capture_samp = texture(TEXTURE, UV);2298float bkg_mask = zero_if_equal(capture_samp, bkg_color);2299float diff_mask = 1.0;2300if (differences_only) {2301// FIXME: If Y-flips across render target, canvas item, etc. was handled correctly,2302// this would not be as convoluted in the shader.2303vec4 capture_samp2 = texture(TEXTURE, vec2(UV.x, 1.0 - UV.y));2304vec4 present_samp = texture(present, vec2(UV.x, 1.0 - UV.y));2305diff_mask = 1.0 - zero_if_equal(present_samp, bkg_color);2306}2307COLOR = vec4(capture_samp.rgb * dir_color.rgb, bkg_mask * diff_mask);2308}2309)");2310RS::get_singleton()->material_set_shader(onion.capture.material->get_rid(), onion.capture.shader->get_rid());23112312ED_SHORTCUT("animation_editor/stop_animation", TTRC("Pause/Stop Animation"), Key::S, true);2313ED_SHORTCUT("animation_editor/play_animation", TTRC("Play Animation"), Key::D, true);2314ED_SHORTCUT("animation_editor/play_animation_backwards", TTRC("Play Animation Backwards"), Key::A, true);2315ED_SHORTCUT("animation_editor/play_animation_from_start", TTRC("Play Animation from Start"), KeyModifierMask::SHIFT + Key::D, true);2316ED_SHORTCUT("animation_editor/play_animation_from_end", TTRC("Play Animation Backwards from End"), KeyModifierMask::SHIFT + Key::A, true);2317}23182319AnimationPlayerEditor::~AnimationPlayerEditor() {2320_free_onion_layers();2321RS::get_singleton()->free_rid(onion.capture.canvas);2322RS::get_singleton()->free_rid(onion.capture.canvas_item);2323onion.capture = {};2324}23252326void AnimationPlayerEditorPlugin::_notification(int p_what) {2327switch (p_what) {2328case NOTIFICATION_READY: {2329Node3DEditor::get_singleton()->connect(SNAME("transform_key_request"), callable_mp(this, &AnimationPlayerEditorPlugin::_transform_key_request));2330InspectorDock::get_inspector_singleton()->connect(SNAME("property_keyed"), callable_mp(this, &AnimationPlayerEditorPlugin::_property_keyed));2331anim_editor->get_track_editor()->connect(SNAME("keying_changed"), callable_mp(this, &AnimationPlayerEditorPlugin::_update_keying));2332InspectorDock::get_inspector_singleton()->connect(SNAME("edited_object_changed"), callable_mp(anim_editor->get_track_editor(), &AnimationTrackEditor::update_keying));2333set_force_draw_over_forwarding_enabled();2334} break;2335}2336}23372338void AnimationPlayerEditorPlugin::_property_keyed(const String &p_keyed, const Variant &p_value, bool p_advance) {2339AnimationTrackEditor *te = anim_editor->get_track_editor();2340if (!te || !te->has_keying()) {2341return;2342}2343te->_clear_selection();2344te->insert_value_key(p_keyed, p_advance);2345}23462347void AnimationPlayerEditorPlugin::_transform_key_request(Object *sp, const String &p_sub, const Transform3D &p_key) {2348if (!anim_editor->get_track_editor()->has_keying()) {2349return;2350}2351Node3D *s = Object::cast_to<Node3D>(sp);2352if (!s) {2353return;2354}2355anim_editor->get_track_editor()->insert_transform_key(s, p_sub, Animation::TYPE_POSITION_3D, p_key.origin);2356anim_editor->get_track_editor()->insert_transform_key(s, p_sub, Animation::TYPE_ROTATION_3D, p_key.basis.get_rotation_quaternion());2357anim_editor->get_track_editor()->insert_transform_key(s, p_sub, Animation::TYPE_SCALE_3D, p_key.basis.get_scale());2358}23592360void AnimationPlayerEditorPlugin::_update_keying() {2361InspectorDock::get_inspector_singleton()->set_keying(anim_editor->get_track_editor()->has_keying());2362}23632364void AnimationPlayerEditorPlugin::edit(Object *p_object) {2365if (player && anim_editor && anim_editor->is_pinned()) {2366return; // Ignore, pinned.2367}23682369player = nullptr;2370if (!p_object) {2371return;2372}2373last_mixer = p_object->get_instance_id();23742375AnimationMixer *src_node = Object::cast_to<AnimationMixer>(p_object);2376bool is_dummy = false;2377if (!p_object->is_class("AnimationPlayer")) {2378// If it needs dummy AnimationPlayer, assign original AnimationMixer to LibraryEditor.2379_update_dummy_player(src_node);23802381is_dummy = true;23822383if (!src_node->is_connected(SNAME("mixer_updated"), callable_mp(this, &AnimationPlayerEditorPlugin::_update_dummy_player))) {2384src_node->connect(SNAME("mixer_updated"), callable_mp(this, &AnimationPlayerEditorPlugin::_update_dummy_player).bind(src_node), CONNECT_DEFERRED);2385}2386if (!src_node->is_connected(SNAME("animation_libraries_updated"), callable_mp(this, &AnimationPlayerEditorPlugin::_update_dummy_player))) {2387src_node->connect(SNAME("animation_libraries_updated"), callable_mp(this, &AnimationPlayerEditorPlugin::_update_dummy_player).bind(src_node), CONNECT_DEFERRED);2388}2389} else {2390_clear_dummy_player();2391player = Object::cast_to<AnimationPlayer>(p_object);2392}2393player->set_dummy(is_dummy);23942395anim_editor->edit(src_node, player, is_dummy);2396}23972398void AnimationPlayerEditorPlugin::_clear_dummy_player() {2399if (!dummy_player) {2400return;2401}2402Node *parent = dummy_player->get_parent();2403if (parent) {2404callable_mp(parent, &Node::remove_child).call_deferred(dummy_player);2405}2406dummy_player->queue_free();2407dummy_player = nullptr;2408}24092410void AnimationPlayerEditorPlugin::_update_dummy_player(AnimationMixer *p_mixer) {2411// Check current editing object.2412if (p_mixer->get_instance_id() != last_mixer && p_mixer->is_connected(SNAME("mixer_updated"), callable_mp(this, &AnimationPlayerEditorPlugin::_update_dummy_player))) {2413p_mixer->disconnect(SNAME("mixer_updated"), callable_mp(this, &AnimationPlayerEditorPlugin::_update_dummy_player));2414return;2415}24162417// Add dummy player to scene.2418if (!dummy_player) {2419Node *parent = p_mixer->get_parent();2420ERR_FAIL_NULL(parent);2421dummy_player = memnew(AnimationPlayer);2422dummy_player->set_active(false); // Inactive as default, it will be activated if the AnimationPlayerEditor visibility is changed.2423parent->add_child(dummy_player);2424}2425player = dummy_player;24262427// Convert AnimationTree (AnimationMixer) to AnimationPlayer.2428AnimationMixer *default_node = memnew(AnimationMixer);2429List<PropertyInfo> pinfo;2430default_node->get_property_list(&pinfo);2431for (const PropertyInfo &E : pinfo) {2432if (!(E.usage & PROPERTY_USAGE_STORAGE)) {2433continue;2434}2435if (E.name != "script" && E.name != "active" && E.name != "deterministic" && E.name != "root_motion_track") {2436dummy_player->set(E.name, p_mixer->get(E.name));2437}2438}2439memdelete(default_node);24402441// Library list is dynamically added to property list, should be copied explicitly.2442List<StringName> libraries;2443p_mixer->get_animation_library_list(&libraries);2444for (const StringName &K : libraries) {2445dummy_player->add_animation_library(K, p_mixer->get_animation_library(K));2446}24472448if (anim_editor) {2449anim_editor->_update_player();2450}2451}24522453bool AnimationPlayerEditorPlugin::handles(Object *p_object) const {2454return p_object->is_class("AnimationPlayer") || p_object->is_class("AnimationTree") || p_object->is_class("AnimationMixer");2455}24562457void AnimationPlayerEditorPlugin::make_visible(bool p_visible) {2458if (p_visible) {2459// if AnimationTree editor is visible, do not occupy the bottom panel2460if (AnimationTreeEditor::get_singleton() && AnimationTreeEditor::get_singleton()->is_visible_in_tree()) {2461return;2462}2463anim_editor->make_visible();2464anim_editor->set_process(true);2465anim_editor->ensure_visibility();2466}2467}24682469AnimationPlayerEditorPlugin::AnimationPlayerEditorPlugin() {2470anim_editor = memnew(AnimationPlayerEditor(this));2471EditorDockManager::get_singleton()->add_dock(anim_editor);2472}24732474AnimationPlayerEditorPlugin::~AnimationPlayerEditorPlugin() {2475if (dummy_player) {2476memdelete(dummy_player);2477}2478}24792480// AnimationTrackKeyEditEditorPlugin24812482bool EditorInspectorPluginAnimationTrackKeyEdit::can_handle(Object *p_object) {2483return Object::cast_to<AnimationTrackKeyEdit>(p_object) != nullptr;2484}24852486void EditorInspectorPluginAnimationTrackKeyEdit::parse_begin(Object *p_object) {2487AnimationTrackKeyEdit *atk = Object::cast_to<AnimationTrackKeyEdit>(p_object);2488ERR_FAIL_NULL(atk);24892490atk_editor = memnew(AnimationTrackKeyEditEditor(atk->animation, atk->track, atk->key_ofs, atk->use_fps));2491add_custom_control(atk_editor);2492}24932494AnimationTrackKeyEditEditorPlugin::AnimationTrackKeyEditEditorPlugin() {2495atk_plugin = memnew(EditorInspectorPluginAnimationTrackKeyEdit);2496EditorInspector::add_inspector_plugin(atk_plugin);2497}24982499bool AnimationTrackKeyEditEditorPlugin::handles(Object *p_object) const {2500return p_object->is_class("AnimationTrackKeyEdit");2501}25022503bool EditorInspectorPluginAnimationMarkerKeyEdit::can_handle(Object *p_object) {2504return Object::cast_to<AnimationMarkerKeyEdit>(p_object) != nullptr;2505}25062507void EditorInspectorPluginAnimationMarkerKeyEdit::parse_begin(Object *p_object) {2508AnimationMarkerKeyEdit *amk = Object::cast_to<AnimationMarkerKeyEdit>(p_object);2509ERR_FAIL_NULL(amk);25102511amk_editor = memnew(AnimationMarkerKeyEditEditor(amk->animation, amk->marker_name, amk->use_fps));2512add_custom_control(amk_editor);2513}25142515AnimationMarkerKeyEditEditorPlugin::AnimationMarkerKeyEditEditorPlugin() {2516amk_plugin = memnew(EditorInspectorPluginAnimationMarkerKeyEdit);2517EditorInspector::add_inspector_plugin(amk_plugin);2518}25192520bool AnimationMarkerKeyEditEditorPlugin::handles(Object *p_object) const {2521return p_object->is_class("AnimationMarkerKeyEdit");2522}252325242525