Path: blob/master/editor/animation/animation_player_editor_plugin.cpp
9896 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/inspector_dock.h"37#include "editor/docks/scene_tree_dock.h"38#include "editor/editor_node.h"39#include "editor/editor_undo_redo_manager.h"40#include "editor/gui/editor_bottom_panel.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_server.h"5556///////////////////////////////////5758void AnimationPlayerEditor::_node_removed(Node *p_node) {59if (player && original_node == p_node) {60if (is_dummy) {61plugin->_clear_dummy_player();62}6364player = nullptr;6566set_process(false);6768track_editor->set_animation(Ref<Animation>(), true);69track_editor->set_root(nullptr);70track_editor->show_select_node_warning(true);71_update_player();7273_ensure_dummy_player();7475pin->set_pressed(false);76}77}7879void AnimationPlayerEditor::_notification(int p_what) {80switch (p_what) {81case NOTIFICATION_PROCESS: {82finishing = false;83if (!player || is_dummy) {84track_editor->show_inactive_player_warning(false);85} else {86track_editor->show_inactive_player_warning(!player->is_active());87}8889if (!player) {90return;91}9293updating = true;9495if (player->is_playing()) {96{97String animname = player->get_assigned_animation();9899if (player->has_animation(animname)) {100Ref<Animation> anim = player->get_animation(animname);101if (anim.is_valid()) {102frame->set_max((double)anim->get_length());103}104}105}106frame->set_value(player->get_current_animation_position());107track_editor->set_anim_pos(player->get_current_animation_position());108} else if (!player->is_valid()) {109// Reset timeline when the player has been stopped externally110frame->set_value(0);111} else if (last_active) {112// Need the last frame after it stopped.113frame->set_value(player->get_current_animation_position());114track_editor->set_anim_pos(player->get_current_animation_position());115stop->set_button_icon(stop_icon);116}117118last_active = player->is_playing();119120updating = false;121} break;122123case NOTIFICATION_ENTER_TREE: {124tool_anim->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationPlayerEditor::_animation_tool_menu));125126onion_skinning->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationPlayerEditor::_onion_skinning_menu));127128blend_editor.next->connect(SceneStringName(item_selected), callable_mp(this, &AnimationPlayerEditor::_blend_editor_next_changed));129130get_tree()->connect(SNAME("node_removed"), callable_mp(this, &AnimationPlayerEditor::_node_removed));131132add_theme_style_override(SceneStringName(panel), EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SceneStringName(panel), SNAME("Panel")));133} break;134135case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {136if (EditorThemeManager::is_generated_theme_outdated()) {137add_theme_style_override(SceneStringName(panel), EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SceneStringName(panel), SNAME("Panel")));138}139} break;140141case NOTIFICATION_TRANSLATION_CHANGED:142case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:143case NOTIFICATION_THEME_CHANGED: {144stop_icon = get_editor_theme_icon(SNAME("Stop"));145pause_icon = get_editor_theme_icon(SNAME("Pause"));146if (player && player->is_playing()) {147stop->set_button_icon(pause_icon);148} else {149stop->set_button_icon(stop_icon);150}151152autoplay->set_button_icon(get_editor_theme_icon(SNAME("AutoPlay")));153play->set_button_icon(get_editor_theme_icon(SNAME("PlayStart")));154play_from->set_button_icon(get_editor_theme_icon(SNAME("Play")));155play_bw->set_button_icon(get_editor_theme_icon(SNAME("PlayStartBackwards")));156play_bw_from->set_button_icon(get_editor_theme_icon(SNAME("PlayBackwards")));157158autoplay_icon = get_editor_theme_icon(SNAME("AutoPlay"));159reset_icon = get_editor_theme_icon(SNAME("Reload"));160{161Ref<Image> autoplay_img = autoplay_icon->get_image();162Ref<Image> reset_img = reset_icon->get_image();163Size2 icon_size = autoplay_img->get_size();164Ref<Image> autoplay_reset_img = Image::create_empty(icon_size.x * 2, icon_size.y, false, autoplay_img->get_format());165autoplay_reset_img->blit_rect(autoplay_img, Rect2i(Point2i(), icon_size), Point2i());166autoplay_reset_img->blit_rect(reset_img, Rect2i(Point2i(), icon_size), Point2i(icon_size.x, 0));167autoplay_reset_icon = ImageTexture::create_from_image(autoplay_reset_img);168}169170onion_toggle->set_button_icon(get_editor_theme_icon(SNAME("Onion")));171onion_skinning->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl")));172173pin->set_button_icon(get_editor_theme_icon(SNAME("Pin")));174175tool_anim->add_theme_style_override(CoreStringName(normal), get_theme_stylebox(CoreStringName(normal), SNAME("Button")));176track_editor->get_edit_menu()->add_theme_style_override(CoreStringName(normal), get_theme_stylebox(CoreStringName(normal), SNAME("Button")));177178#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)))179180ITEM_ICON(TOOL_NEW_ANIM, "New");181ITEM_ICON(TOOL_ANIM_LIBRARY, "AnimationLibrary");182ITEM_ICON(TOOL_DUPLICATE_ANIM, "Duplicate");183ITEM_ICON(TOOL_RENAME_ANIM, "Rename");184ITEM_ICON(TOOL_EDIT_TRANSITIONS, "Blend");185ITEM_ICON(TOOL_EDIT_RESOURCE, "Edit");186ITEM_ICON(TOOL_REMOVE_ANIM, "Remove");187188_update_animation_list_icons();189} break;190191case NOTIFICATION_VISIBILITY_CHANGED: {192_ensure_dummy_player();193} break;194}195}196197void AnimationPlayerEditor::_autoplay_pressed() {198if (updating) {199return;200}201if (animation->has_selectable_items() == 0) {202return;203}204205EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();206String current = animation->get_item_text(animation->get_selected());207if (player->get_autoplay() == current) {208//unset209undo_redo->create_action(TTR("Toggle Autoplay"));210undo_redo->add_do_method(player, "set_autoplay", "");211undo_redo->add_undo_method(player, "set_autoplay", player->get_autoplay());212undo_redo->add_do_method(this, "_animation_player_changed", player);213undo_redo->add_undo_method(this, "_animation_player_changed", player);214undo_redo->commit_action();215216} else {217//set218undo_redo->create_action(TTR("Toggle Autoplay"));219undo_redo->add_do_method(player, "set_autoplay", current);220undo_redo->add_undo_method(player, "set_autoplay", player->get_autoplay());221undo_redo->add_do_method(this, "_animation_player_changed", player);222undo_redo->add_undo_method(this, "_animation_player_changed", player);223undo_redo->commit_action();224}225}226227void AnimationPlayerEditor::_go_to_nearest_keyframe(bool p_backward) {228if (_get_current().is_empty()) {229return;230}231232Ref<Animation> anim = player->get_animation(player->get_assigned_animation());233234double current_time = player->get_current_animation_position();235// Offset the time to avoid finding the same keyframe with Animation::track_find_key().236double time_offset = MAX(CMP_EPSILON * 2, current_time * CMP_EPSILON * 2);237double current_time_offset = current_time + (p_backward ? -time_offset : time_offset);238239float nearest_key_time = p_backward ? 0 : anim->get_length();240int track_count = anim->get_track_count();241bool bezier_active = track_editor->is_bezier_editor_active();242243Node *root = get_tree()->get_edited_scene_root();244EditorSelection *selection = EditorNode::get_singleton()->get_editor_selection();245246Vector<int> selected_tracks;247for (int i = 0; i < track_count; ++i) {248if (selection->is_selected(root->get_node_or_null(anim->track_get_path(i)))) {249selected_tracks.push_back(i);250}251}252253// Find the nearest keyframe in selection if the scene has selected nodes254// or the nearest keyframe in the entire animation otherwise.255if (selected_tracks.size() > 0) {256for (int track : selected_tracks) {257if (bezier_active && anim->track_get_type(track) != Animation::TYPE_BEZIER) {258continue;259}260int key = anim->track_find_key(track, current_time_offset, Animation::FIND_MODE_NEAREST, false, !p_backward);261if (key == -1) {262continue;263}264double key_time = anim->track_get_key_time(track, key);265if ((p_backward && key_time > nearest_key_time) || (!p_backward && key_time < nearest_key_time)) {266nearest_key_time = key_time;267}268}269} else {270for (int track = 0; track < track_count; ++track) {271if (bezier_active && anim->track_get_type(track) != Animation::TYPE_BEZIER) {272continue;273}274int key = anim->track_find_key(track, current_time_offset, Animation::FIND_MODE_NEAREST, false, !p_backward);275if (key == -1) {276continue;277}278double key_time = anim->track_get_key_time(track, key);279if ((p_backward && key_time > nearest_key_time) || (!p_backward && key_time < nearest_key_time)) {280nearest_key_time = key_time;281}282}283}284285player->seek_internal(nearest_key_time, true, true, true);286frame->set_value(nearest_key_time);287track_editor->set_anim_pos(nearest_key_time);288}289290void AnimationPlayerEditor::_play_pressed() {291String current = _get_current();292293if (!current.is_empty()) {294if (current == player->get_assigned_animation()) {295player->stop(); //so it won't blend with itself296}297ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing.");298PackedStringArray markers = track_editor->get_selected_section();299if (markers.size() == 2) {300StringName start_marker = markers[0];301StringName end_marker = markers[1];302player->play_section_with_markers(current, start_marker, end_marker);303} else {304player->play(current);305}306}307308//unstop309stop->set_button_icon(pause_icon);310}311312void AnimationPlayerEditor::_play_from_pressed() {313String current = _get_current();314315if (!current.is_empty()) {316double time = player->get_current_animation_position();317if (current == player->get_assigned_animation() && player->is_playing()) {318player->clear_caches(); //so it won't blend with itself319}320ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing.");321player->seek_internal(time, true, true, true);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}335336String AnimationPlayerEditor::_get_current() const {337String current;338if (animation->get_selected() >= 0 && animation->get_selected() < animation->get_item_count() && !animation->is_item_separator(animation->get_selected())) {339current = animation->get_item_text(animation->get_selected());340}341return current;342}343void AnimationPlayerEditor::_play_bw_pressed() {344String current = _get_current();345if (!current.is_empty()) {346if (current == player->get_assigned_animation()) {347player->stop(); //so it won't blend with itself348}349ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing.");350PackedStringArray markers = track_editor->get_selected_section();351if (markers.size() == 2) {352StringName start_marker = markers[0];353StringName end_marker = markers[1];354player->play_section_with_markers_backwards(current, start_marker, end_marker);355} else {356player->play_backwards(current);357}358}359360//unstop361stop->set_button_icon(pause_icon);362}363364void AnimationPlayerEditor::_play_bw_from_pressed() {365String current = _get_current();366367if (!current.is_empty()) {368double time = player->get_current_animation_position();369if (current == player->get_assigned_animation() && player->is_playing()) {370player->clear_caches(); //so it won't blend with itself371}372ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing.");373player->seek_internal(time, true, true, true);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::_stop_pressed() {389if (!player) {390return;391}392393if (player->is_playing()) {394player->pause();395} else {396String current = _get_current();397player->stop();398player->set_assigned_animation(current);399frame->set_value(0);400track_editor->set_anim_pos(0);401}402stop->set_button_icon(stop_icon);403}404405void AnimationPlayerEditor::_animation_selected(int p_which) {406if (updating) {407return;408}409410#define ITEM_CHECK_DISABLED(m_item) tool_anim->get_popup()->set_item_disabled(tool_anim->get_popup()->get_item_index(m_item), true)411ITEM_CHECK_DISABLED(TOOL_RENAME_ANIM);412ITEM_CHECK_DISABLED(TOOL_DUPLICATE_ANIM);413ITEM_CHECK_DISABLED(TOOL_REMOVE_ANIM);414415ITEM_CHECK_DISABLED(TOOL_EDIT_TRANSITIONS);416ITEM_CHECK_DISABLED(TOOL_EDIT_RESOURCE);417#undef ITEM_CHECK_DISABLED418419// when selecting an animation, the idea is that the only interesting behavior420// ui-wise is that it should play/blend the next one if currently playing421String current = _get_current();422423if (!current.is_empty()) {424player->set_assigned_animation(current);425426Ref<Animation> anim = player->get_animation(current);427ERR_FAIL_COND(anim.is_null());428{429bool animation_is_readonly = EditorNode::get_singleton()->is_resource_read_only(anim);430431track_editor->set_animation(anim, animation_is_readonly);432Node *root = player->get_node_or_null(player->get_root_node());433434// Player shouldn't access parent if it's the scene root.435if (!root || (player == get_tree()->get_edited_scene_root() && player->get_root_node() == SceneStringName(path_pp))) {436NodePath cached_root_path = player->get_path_to(get_cached_root_node());437if (player->get_node_or_null(cached_root_path) != nullptr) {438player->set_root_node(cached_root_path);439} else {440player->set_root_node(SceneStringName(path_pp)); // No other choice, preventing crash.441}442} else {443cached_root_node_id = root->get_instance_id(); // Caching as `track_editor` can lose track of player's root node.444track_editor->set_root(root);445}446}447frame->set_max((double)anim->get_length());448autoplay->set_pressed(current == player->get_autoplay());449player->stop();450} else {451track_editor->set_animation(Ref<Animation>(), true);452track_editor->set_root(nullptr);453autoplay->set_pressed(false);454}455456AnimationPlayerEditor::get_singleton()->get_track_editor()->update_keying();457_animation_key_editor_seek(timeline_position);458459emit_signal("animation_selected", current);460}461462void AnimationPlayerEditor::_animation_new() {463int count = 1;464String base = "new_animation";465String current_library_name = "";466if (animation->has_selectable_items()) {467String current_animation_name = animation->get_item_text(animation->get_selected());468Ref<Animation> current_animation = player->get_animation(current_animation_name);469if (current_animation.is_valid()) {470current_library_name = player->find_animation_library(current_animation);471}472}473String attempt_prefix = (current_library_name == "") ? "" : current_library_name + "/";474while (true) {475String attempt = base;476if (count > 1) {477attempt += vformat("_%d", count);478}479if (player->has_animation(attempt_prefix + attempt)) {480count++;481continue;482}483base = attempt;484break;485}486487_update_name_dialog_library_dropdown();488489name_dialog_op = TOOL_NEW_ANIM;490name_dialog->set_title(TTR("Create New Animation"));491name_dialog->popup_centered(Size2(300, 90));492name_title->set_text(TTR("New Animation Name:"));493name->set_text(base);494name->select_all();495name->grab_focus();496}497498void AnimationPlayerEditor::_animation_rename() {499if (!animation->has_selectable_items()) {500return;501}502int selected = animation->get_selected();503String selected_name = animation->get_item_text(selected);504505// Remove library prefix if present.506if (selected_name.contains_char('/')) {507selected_name = selected_name.get_slicec('/', 1);508}509510name_dialog->set_title(TTR("Rename Animation"));511name_title->set_text(TTR("Change Animation Name:"));512name->set_text(selected_name);513name_dialog_op = TOOL_RENAME_ANIM;514name_dialog->popup_centered(Size2(300, 90));515name->select_all();516name->grab_focus();517library->hide();518}519520void AnimationPlayerEditor::_animation_remove() {521if (!animation->has_selectable_items()) {522return;523}524525String current = animation->get_item_text(animation->get_selected());526527delete_dialog->set_text(vformat(TTR("Delete Animation '%s'?"), current));528delete_dialog->popup_centered();529}530531void AnimationPlayerEditor::_animation_remove_confirmed() {532String current = animation->get_item_text(animation->get_selected());533Ref<Animation> anim = player->get_animation(current);534535Ref<AnimationLibrary> al = player->get_animation_library(player->find_animation_library(anim));536ERR_FAIL_COND(al.is_null());537538// For names of form lib_name/anim_name, remove library name prefix.539if (current.contains_char('/')) {540current = current.get_slicec('/', 1);541}542EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();543undo_redo->create_action(TTR("Remove Animation"));544if (player->get_autoplay() == current) {545undo_redo->add_do_method(player, "set_autoplay", "");546undo_redo->add_undo_method(player, "set_autoplay", current);547// Avoid having the autoplay icon linger around if there is only one animation in the player.548undo_redo->add_do_method(this, "_animation_player_changed", player);549}550undo_redo->add_do_method(al.ptr(), "remove_animation", current);551undo_redo->add_undo_method(al.ptr(), "add_animation", current, anim);552undo_redo->add_do_method(this, "_animation_player_changed", player);553undo_redo->add_undo_method(this, "_animation_player_changed", player);554if (animation->has_selectable_items() && animation->get_selectable_item(false) == animation->get_selectable_item(true)) { // Last item remaining.555undo_redo->add_do_method(this, "_stop_onion_skinning");556undo_redo->add_undo_method(this, "_start_onion_skinning");557}558undo_redo->commit_action();559}560561void AnimationPlayerEditor::_select_anim_by_name(const String &p_anim) {562int idx = -1;563for (int i = 0; i < animation->get_item_count(); i++) {564if (animation->get_item_text(i) == p_anim) {565idx = i;566break;567}568}569570ERR_FAIL_COND(idx == -1);571572animation->select(idx);573574_animation_selected(idx);575}576577float AnimationPlayerEditor::_get_editor_step() const {578const String current = player->get_assigned_animation();579const Ref<Animation> anim = player->get_animation(current);580ERR_FAIL_COND_V(anim.is_null(), 0.0);581582float step = track_editor->get_snap_unit();583584// Use more precise snapping when holding Shift585return Input::get_singleton()->is_key_pressed(Key::SHIFT) ? step * 0.25 : step;586}587588void AnimationPlayerEditor::_animation_name_edited() {589if (player->is_playing()) {590player->stop();591}592593String new_name = name->get_text();594if (!AnimationLibrary::is_valid_animation_name(new_name)) {595error_dialog->set_text(TTR("Invalid animation name!"));596error_dialog->popup_centered();597return;598}599600if (name_dialog_op == TOOL_RENAME_ANIM && animation->has_selectable_items() && animation->get_item_text(animation->get_selected()) == new_name) {601name_dialog->hide();602return;603}604605String test_name_prefix = "";606if (library->is_visible() && library->get_selected_id() != -1) {607test_name_prefix = library->get_item_metadata(library->get_selected_id());608test_name_prefix += (test_name_prefix != "") ? "/" : "";609}610611if (player->has_animation(test_name_prefix + new_name)) {612error_dialog->set_text(vformat(TTR("Animation '%s' already exists!"), test_name_prefix + new_name));613error_dialog->popup_centered();614return;615}616617EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();618switch (name_dialog_op) {619case TOOL_RENAME_ANIM: {620String current = animation->get_item_text(animation->get_selected());621Ref<Animation> anim = player->get_animation(current);622623Ref<AnimationLibrary> al = player->get_animation_library(player->find_animation_library(anim));624ERR_FAIL_COND(al.is_null());625626// Extract library prefix if present.627String new_library_prefix = "";628if (current.contains_char('/')) {629new_library_prefix = current.get_slicec('/', 0) + "/";630current = current.get_slicec('/', 1);631}632633undo_redo->create_action(TTR("Rename Animation"));634undo_redo->add_do_method(al.ptr(), "rename_animation", current, new_name);635undo_redo->add_do_method(anim.ptr(), "set_name", new_name);636undo_redo->add_undo_method(al.ptr(), "rename_animation", new_name, current);637undo_redo->add_undo_method(anim.ptr(), "set_name", current);638undo_redo->add_do_method(this, "_animation_player_changed", player);639undo_redo->add_undo_method(this, "_animation_player_changed", player);640undo_redo->commit_action();641642if (is_dummy) {643plugin->_update_dummy_player(original_node);644}645_select_anim_by_name(new_library_prefix + new_name);646} break;647648case TOOL_NEW_ANIM: {649Ref<Animation> new_anim = Ref<Animation>(memnew(Animation));650new_anim->set_name(new_name);651652if (animation->get_item_count() > 0) {653String current = animation->get_item_text(animation->get_selected());654Ref<Animation> current_anim = player->get_animation(current);655656if (current_anim.is_valid()) {657new_anim->set_step(current_anim->get_step());658}659} else {660new_anim->set_step(EDITOR_GET("editors/animation/default_animation_step"));661}662663String library_name;664Ref<AnimationLibrary> al;665library_name = library->get_item_metadata(library->get_selected());666// It's possible that [Global] was selected, but doesn't exist yet.667if (player->has_animation_library(library_name)) {668al = player->get_animation_library(library_name);669}670671undo_redo->create_action(TTR("Add Animation"));672673bool lib_added = false;674if (al.is_null()) {675al.instantiate();676lib_added = true;677undo_redo->add_do_method(fetch_mixer_for_library(), "add_animation_library", "", al);678library_name = "";679}680681undo_redo->add_do_method(al.ptr(), "add_animation", new_name, new_anim);682undo_redo->add_undo_method(al.ptr(), "remove_animation", new_name);683undo_redo->add_do_method(this, "_animation_player_changed", player);684undo_redo->add_undo_method(this, "_animation_player_changed", player);685if (!animation->has_selectable_items()) {686undo_redo->add_do_method(this, "_start_onion_skinning");687undo_redo->add_undo_method(this, "_stop_onion_skinning");688}689if (lib_added) {690undo_redo->add_undo_method(fetch_mixer_for_library(), "remove_animation_library", "");691}692undo_redo->commit_action();693694if (library_name != "") {695library_name = library_name + "/";696}697698if (is_dummy) {699plugin->_update_dummy_player(original_node);700}701_select_anim_by_name(library_name + new_name);702703} break;704705case TOOL_DUPLICATE_ANIM: {706String current = animation->get_item_text(animation->get_selected());707Ref<Animation> anim = player->get_animation(current);708709Ref<Animation> new_anim = _animation_clone(anim);710new_anim->set_name(new_name);711712String library_name;713Ref<AnimationLibrary> al;714if (library->is_visible()) {715library_name = library->get_item_metadata(library->get_selected());716// It's possible that [Global] was selected, but doesn't exist yet.717if (player->has_animation_library(library_name)) {718al = player->get_animation_library(library_name);719}720} else {721if (player->has_animation_library("")) {722al = player->get_animation_library("");723library_name = "";724}725}726727undo_redo->create_action(TTR("Duplicate Animation"));728729bool lib_added = false;730if (al.is_null()) {731al.instantiate();732lib_added = true;733undo_redo->add_do_method(player, "add_animation_library", "", al);734library_name = "";735}736737undo_redo->add_do_method(al.ptr(), "add_animation", new_name, new_anim);738undo_redo->add_undo_method(al.ptr(), "remove_animation", new_name);739undo_redo->add_do_method(this, "_animation_player_changed", player);740undo_redo->add_undo_method(this, "_animation_player_changed", player);741if (lib_added) {742undo_redo->add_undo_method(player, "remove_animation_library", "");743}744undo_redo->commit_action();745746if (library_name != "") {747library_name = library_name + "/";748}749750if (is_dummy) {751plugin->_update_dummy_player(original_node);752}753_select_anim_by_name(library_name + new_name);754} break;755}756757name_dialog->hide();758}759760void AnimationPlayerEditor::_blend_editor_next_changed(const int p_idx) {761if (!animation->has_selectable_items()) {762return;763}764765String current = animation->get_item_text(animation->get_selected());766767EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();768undo_redo->create_action(TTR("Blend Next Changed"));769undo_redo->add_do_method(player, "animation_set_next", current, blend_editor.next->get_item_text(p_idx));770undo_redo->add_undo_method(player, "animation_set_next", current, player->animation_get_next(current));771undo_redo->add_do_method(this, "_animation_player_changed", player);772undo_redo->add_undo_method(this, "_animation_player_changed", player);773undo_redo->commit_action();774}775776void AnimationPlayerEditor::_edit_animation_blend() {777if (updating_blends || !animation->has_selectable_items()) {778return;779}780781blend_editor.dialog->popup_centered(Size2(400, 400) * EDSCALE);782_update_animation_blend();783}784785void AnimationPlayerEditor::_update_animation_blend() {786if (updating_blends || !animation->has_selectable_items()) {787return;788}789790blend_editor.tree->clear();791792String current = animation->get_item_text(animation->get_selected());793794List<StringName> anims;795player->get_animation_list(&anims);796TreeItem *root = blend_editor.tree->create_item();797updating_blends = true;798799int i = 0;800bool anim_found = false;801blend_editor.next->clear();802blend_editor.next->add_item("", i);803804for (const StringName &to : anims) {805TreeItem *blend = blend_editor.tree->create_item(root);806blend->set_editable(0, false);807blend->set_editable(1, true);808blend->set_text(0, to);809blend->set_cell_mode(1, TreeItem::CELL_MODE_RANGE);810blend->set_range_config(1, 0, 3600, 0.001);811blend->set_range(1, player->get_blend_time(current, to));812813i++;814blend_editor.next->add_item(to, i);815if (to == player->animation_get_next(current)) {816blend_editor.next->select(i);817anim_found = true;818}819}820821// make sure we reset it else it becomes out of sync and could contain a deleted animation822if (!anim_found) {823blend_editor.next->select(0);824player->animation_set_next(current, blend_editor.next->get_item_text(0));825}826827updating_blends = false;828}829830void AnimationPlayerEditor::_blend_edited() {831if (updating_blends || !animation->has_selectable_items()) {832return;833}834835TreeItem *selected = blend_editor.tree->get_edited();836if (!selected) {837return;838}839840String current = animation->get_item_text(animation->get_selected());841842updating_blends = true;843String to = selected->get_text(0);844float blend_time = selected->get_range(1);845float prev_blend_time = player->get_blend_time(current, to);846847EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();848undo_redo->create_action(TTR("Change Blend Time"));849undo_redo->add_do_method(player, "set_blend_time", current, to, blend_time);850undo_redo->add_undo_method(player, "set_blend_time", current, to, prev_blend_time);851undo_redo->add_do_method(this, "_animation_player_changed", player);852undo_redo->add_undo_method(this, "_animation_player_changed", player);853undo_redo->commit_action();854updating_blends = false;855}856857void AnimationPlayerEditor::ensure_visibility() {858if (player) {859return; // another player is pinned, don't reset860}861862_animation_edit();863}864865Dictionary AnimationPlayerEditor::get_state() const {866Dictionary d;867868if (!is_dummy) {869d["visible"] = is_visible_in_tree();870if (EditorNode::get_singleton()->get_edited_scene() && is_visible_in_tree() && player) {871d["player"] = EditorNode::get_singleton()->get_edited_scene()->get_path_to(player);872d["animation"] = player->get_assigned_animation();873d["track_editor_state"] = track_editor->get_state();874}875}876877return d;878}879880void AnimationPlayerEditor::set_state(const Dictionary &p_state) {881if (!p_state.has("visible") || !p_state["visible"]) {882return;883}884if (!EditorNode::get_singleton()->get_edited_scene()) {885return;886}887888if (p_state.has("player")) {889Node *n = EditorNode::get_singleton()->get_edited_scene()->get_node(p_state["player"]);890if (Object::cast_to<AnimationPlayer>(n) && EditorNode::get_singleton()->get_editor_selection()->is_selected(n)) {891if (player) {892if (player->is_connected(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated))) {893player->disconnect(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated));894}895if (player->is_connected(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed))) {896player->disconnect(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed));897}898}899player = Object::cast_to<AnimationPlayer>(n);900if (player) {901if (!player->is_connected(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated))) {902player->connect(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated), CONNECT_DEFERRED);903}904if (!player->is_connected(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed))) {905player->connect(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed));906}907}908909_update_player();910EditorNode::get_bottom_panel()->make_item_visible(this);911set_process(true);912ensure_visibility();913914if (p_state.has("animation")) {915String anim = p_state["animation"];916if (!anim.is_empty() && player->has_animation(anim)) {917_select_anim_by_name(anim);918_animation_edit();919}920}921}922}923924if (p_state.has("track_editor_state")) {925track_editor->set_state(p_state["track_editor_state"]);926}927}928929void AnimationPlayerEditor::clear() {930track_editor->clear();931}932933void AnimationPlayerEditor::_animation_resource_edit() {934String current = _get_current();935if (current != String()) {936Ref<Animation> anim = player->get_animation(current);937EditorNode::get_singleton()->edit_resource(anim);938}939}940941void AnimationPlayerEditor::_animation_edit() {942String current = _get_current();943if (current != String()) {944Ref<Animation> anim = player->get_animation(current);945946track_editor->set_animation(anim, EditorNode::get_singleton()->is_resource_read_only(anim));947948Node *root = player->get_node_or_null(player->get_root_node());949if (root) {950track_editor->set_root(root);951}952} else {953track_editor->set_animation(Ref<Animation>(), true);954track_editor->set_root(nullptr);955}956}957958void AnimationPlayerEditor::_scale_changed(const String &p_scale) {959player->set_speed_scale(p_scale.to_float());960}961962void AnimationPlayerEditor::_update_animation() {963// the purpose of _update_animation is to reflect the current state964// of the animation player in the current editor..965966updating = true;967968if (player->is_playing()) {969stop->set_button_icon(pause_icon);970} else {971stop->set_button_icon(stop_icon);972}973974scale->set_text(String::num(player->get_speed_scale(), 2));975String current = player->get_assigned_animation();976977for (int i = 0; i < animation->get_item_count(); i++) {978if (animation->get_item_text(i) == current) {979animation->select(i);980break;981}982}983984updating = false;985}986987void AnimationPlayerEditor::_update_player() {988updating = true;989990animation->clear();991992tool_anim->set_disabled(player == nullptr);993pin->set_disabled(player == nullptr);994_set_controls_disabled(player == nullptr);995996if (!player) {997AnimationPlayerEditor::get_singleton()->get_track_editor()->update_keying();998return;999}10001001List<StringName> libraries;1002player->get_animation_library_list(&libraries);10031004int active_idx = -1;1005bool no_anims_found = true;1006bool global_animation_library_is_readonly = false;1007bool all_animation_libraries_are_readonly = libraries.size() > 0;10081009for (const StringName &K : libraries) {1010if (K != StringName()) {1011animation->add_separator(K);1012}10131014// Check if the global library is read-only since we want to disable options for adding/remove/renaming animations if it is.1015Ref<AnimationLibrary> anim_library = player->get_animation_library(K);1016bool is_animation_library_read_only = EditorNode::get_singleton()->is_resource_read_only(anim_library);1017if (!is_animation_library_read_only) {1018all_animation_libraries_are_readonly = false;1019} else {1020if (K == "") {1021global_animation_library_is_readonly = true;1022}1023}10241025List<StringName> animlist;1026anim_library->get_animation_list(&animlist);10271028for (const StringName &E : animlist) {1029String path = K;1030if (path != "") {1031path += "/";1032}1033path += E;1034animation->add_item(path);1035if (player->get_assigned_animation() == path) {1036active_idx = animation->get_selectable_item(true);1037}1038no_anims_found = false;1039}1040}1041#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))1042ITEM_CHECK_DISABLED(TOOL_NEW_ANIM);1043#undef ITEM_CHECK_DISABLED10441045_update_animation_list_icons();10461047updating = false;1048if (active_idx != -1) {1049animation->select(active_idx);1050autoplay->set_pressed(animation->get_item_text(active_idx) == player->get_autoplay());1051_animation_selected(active_idx);1052} else if (animation->has_selectable_items()) {1053int item = animation->get_selectable_item();1054animation->select(item);1055autoplay->set_pressed(animation->get_item_text(item) == player->get_autoplay());1056_animation_selected(item);1057} else {1058_animation_selected(0);1059}10601061if (no_anims_found) {1062_set_controls_disabled(true);1063} else {1064String current = animation->get_item_text(animation->get_selected());1065Ref<Animation> anim = player->get_animation(current);10661067bool animation_library_is_readonly = EditorNode::get_singleton()->is_resource_read_only(anim);10681069track_editor->set_animation(anim, animation_library_is_readonly);1070Node *root = player->get_node_or_null(player->get_root_node());1071if (root) {1072track_editor->set_root(root);1073}1074}10751076_update_animation();1077}10781079void AnimationPlayerEditor::_set_controls_disabled(bool p_disabled) {1080frame->set_editable(!p_disabled);10811082stop->set_disabled(p_disabled);1083play->set_disabled(p_disabled);1084play_bw->set_disabled(p_disabled);1085play_bw_from->set_disabled(p_disabled);1086play_from->set_disabled(p_disabled);1087animation->set_disabled(p_disabled);1088autoplay->set_disabled(p_disabled);1089onion_toggle->set_disabled(p_disabled);1090onion_skinning->set_disabled(p_disabled);1091}10921093void AnimationPlayerEditor::_update_animation_list_icons() {1094for (int i = 0; i < animation->get_item_count(); i++) {1095String anim_name = animation->get_item_text(i);1096if (animation->is_item_disabled(i) || animation->is_item_separator(i)) {1097continue;1098}10991100Ref<Texture2D> icon;1101if (anim_name == player->get_autoplay()) {1102if (anim_name == SceneStringName(RESET)) {1103icon = autoplay_reset_icon;1104} else {1105icon = autoplay_icon;1106}1107} else if (anim_name == SceneStringName(RESET)) {1108icon = reset_icon;1109}11101111animation->set_item_icon(i, icon);1112}1113}11141115void AnimationPlayerEditor::_update_name_dialog_library_dropdown() {1116StringName current_library_name;1117if (animation->has_selectable_items()) {1118String current_animation_name = animation->get_item_text(animation->get_selected());1119Ref<Animation> current_animation = player->get_animation(current_animation_name);1120if (current_animation.is_valid()) {1121current_library_name = player->find_animation_library(current_animation);1122}1123}11241125List<StringName> libraries;1126player->get_animation_library_list(&libraries);1127library->clear();11281129int valid_library_count = 0;11301131// When [Global] isn't present, but other libraries are, add option of creating [Global].1132int index_offset = 0;1133if (!player->has_animation_library(StringName())) {1134library->add_item(String(TTR("[Global] (create)")));1135library->set_item_metadata(0, "");1136if (!libraries.is_empty()) {1137index_offset = 1;1138}1139valid_library_count++;1140}11411142int current_lib_id = index_offset; // Don't default to [Global] if it doesn't exist yet.1143for (const StringName &library_name : libraries) {1144if (!EditorNode::get_singleton()->is_resource_read_only(player->get_animation_library(library_name))) {1145library->add_item((library_name == StringName()) ? String(TTR("[Global]")) : String(library_name));1146library->set_item_metadata(valid_library_count, String(library_name));1147// Default to duplicating into same library.1148if (library_name == current_library_name) {1149current_library_name = library_name;1150current_lib_id = valid_library_count;1151}1152valid_library_count++;1153}1154}11551156// If our library name is empty, but we have valid libraries, we can check here to auto assign the first1157// one which isn't a read-only library.1158bool auto_assigning_non_global_library = false;1159if (current_library_name == StringName() && valid_library_count > 0) {1160for (const StringName &library_name : libraries) {1161if (!EditorNode::get_singleton()->is_resource_read_only(player->get_animation_library(library_name))) {1162current_library_name = library_name;1163current_lib_id = 0;1164if (library_name != StringName()) {1165auto_assigning_non_global_library = true;1166}1167break;1168}1169}1170}11711172if (library->get_item_count() > 0) {1173library->select(current_lib_id);1174if (library->get_item_count() > 1 || auto_assigning_non_global_library) {1175library->show();1176library->set_disabled(auto_assigning_non_global_library && library->get_item_count() == 1);1177} else {1178library->hide();1179}1180}1181}11821183void AnimationPlayerEditor::_ensure_dummy_player() {1184bool dummy_exists = is_dummy && player && original_node;1185if (dummy_exists) {1186if (is_visible()) {1187player->set_active(true);1188original_node->set_editing(true);1189} else {1190player->set_active(false);1191original_node->set_editing(false);1192}1193}11941195int selected = animation->get_selected();1196autoplay->set_disabled(selected != -1 ? (animation->get_item_text(selected).is_empty() ? true : dummy_exists) : true);11971198// Show warning.1199if (track_editor) {1200track_editor->show_dummy_player_warning(dummy_exists);1201}1202}12031204void AnimationPlayerEditor::edit(AnimationMixer *p_node, AnimationPlayer *p_player, bool p_is_dummy) {1205if (player && pin->is_pressed()) {1206return; // Ignore, pinned.1207}12081209if (player) {1210if (player->is_connected(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated))) {1211player->disconnect(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated));1212}1213if (player->is_connected(SceneStringName(animation_finished), callable_mp(this, &AnimationPlayerEditor::_animation_finished))) {1214player->disconnect(SceneStringName(animation_finished), callable_mp(this, &AnimationPlayerEditor::_animation_finished));1215}1216if (player->is_connected(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed))) {1217player->disconnect(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed));1218}1219}12201221AnimationTree *tree = Object::cast_to<AnimationTree>(p_node);12221223if (tree) {1224if (tree->is_connected(SNAME("animation_player_changed"), callable_mp(this, &AnimationPlayerEditor::unpin))) {1225tree->disconnect(SNAME("animation_player_changed"), callable_mp(this, &AnimationPlayerEditor::unpin));1226}1227}12281229original_node = p_node;1230player = p_player;1231is_dummy = p_is_dummy;12321233if (tree) {1234if (!tree->is_connected(SNAME("animation_player_changed"), callable_mp(this, &AnimationPlayerEditor::unpin))) {1235tree->connect(SNAME("animation_player_changed"), callable_mp(this, &AnimationPlayerEditor::unpin));1236}1237}12381239if (player) {1240if (!player->is_connected(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated))) {1241player->connect(SNAME("animation_list_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_libraries_updated), CONNECT_DEFERRED);1242}1243if (!player->is_connected(SceneStringName(animation_finished), callable_mp(this, &AnimationPlayerEditor::_animation_finished))) {1244player->connect(SceneStringName(animation_finished), callable_mp(this, &AnimationPlayerEditor::_animation_finished));1245}1246if (!player->is_connected(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed))) {1247player->connect(SNAME("current_animation_changed"), callable_mp(this, &AnimationPlayerEditor::_current_animation_changed));1248}1249_update_player();12501251if (onion.enabled) {1252if (animation->has_selectable_items()) {1253_start_onion_skinning();1254} else {1255_stop_onion_skinning();1256}1257}12581259track_editor->show_select_node_warning(false);1260} else {1261if (onion.enabled) {1262_stop_onion_skinning();1263}12641265track_editor->show_select_node_warning(true);1266}12671268library_editor->set_animation_mixer(fetch_mixer_for_library());12691270_ensure_dummy_player();1271}12721273void AnimationPlayerEditor::forward_force_draw_over_viewport(Control *p_overlay) {1274if (!onion.can_overlay) {1275return;1276}12771278// Can happen on viewport resize, at least.1279if (!_are_onion_layers_valid()) {1280return;1281}12821283RID ci = p_overlay->get_canvas_item();1284Rect2 src_rect = p_overlay->get_global_rect();1285// Re-flip since captures are already flipped.1286src_rect.position.y = onion.capture_size.y - (src_rect.position.y + src_rect.size.y);1287src_rect.size.y *= -1;12881289Rect2 dst_rect = Rect2(Point2(), p_overlay->get_size());12901291float alpha_step = 1.0 / (onion.steps + 1);12921293uint32_t capture_idx = 0;1294if (onion.past) {1295float alpha = 0.0f;1296do {1297alpha += alpha_step;12981299if (onion.captures_valid[capture_idx]) {1300RS::get_singleton()->canvas_item_add_texture_rect_region(1301ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[capture_idx]), src_rect, Color(1, 1, 1, alpha));1302}13031304capture_idx++;1305} while (capture_idx < onion.steps);1306}1307if (onion.future) {1308float alpha = 1.0f;1309uint32_t base_cidx = capture_idx;1310do {1311alpha -= alpha_step;13121313if (onion.captures_valid[capture_idx]) {1314RS::get_singleton()->canvas_item_add_texture_rect_region(1315ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[capture_idx]), src_rect, Color(1, 1, 1, alpha));1316}13171318capture_idx++;1319} while (capture_idx < base_cidx + onion.steps); // In case there's the present capture at the end, skip it.1320}1321}13221323void AnimationPlayerEditor::_animation_duplicate() {1324if (!animation->has_selectable_items()) {1325return;1326}13271328String current = animation->get_item_text(animation->get_selected());1329Ref<Animation> anim = player->get_animation(current);1330if (anim.is_null()) {1331return;1332}13331334int count = 2;1335String new_name = current;1336PackedStringArray split = new_name.split("_");1337int last_index = split.size() - 1;1338if (last_index > 0 && split[last_index].is_valid_int() && split[last_index].to_int() >= 0) {1339count = split[last_index].to_int();1340split.remove_at(last_index);1341new_name = String("_").join(split);1342}1343while (true) {1344String attempt = new_name;1345attempt += vformat("_%d", count);1346if (player->has_animation(attempt)) {1347count++;1348continue;1349}1350new_name = attempt;1351break;1352}13531354if (new_name.contains_char('/')) {1355// Discard library prefix.1356new_name = new_name.get_slicec('/', 1);1357}13581359_update_name_dialog_library_dropdown();13601361name_dialog_op = TOOL_DUPLICATE_ANIM;1362name_dialog->set_title(TTR("Duplicate Animation"));1363// TRANSLATORS: This is a label for the new name field in the "Duplicate Animation" dialog.1364name_title->set_text(TTR("Duplicated Animation Name:"));1365name->set_text(new_name);1366name_dialog->popup_centered(Size2(300, 90));1367name->select_all();1368name->grab_focus();1369}13701371Ref<Animation> AnimationPlayerEditor::_animation_clone(Ref<Animation> p_anim) {1372Ref<Animation> new_anim = memnew(Animation);1373List<PropertyInfo> plist;1374p_anim->get_property_list(&plist);13751376for (const PropertyInfo &E : plist) {1377if (E.usage & PROPERTY_USAGE_STORAGE) {1378new_anim->set(E.name, p_anim->get(E.name));1379}1380}1381new_anim->set_path("");13821383return new_anim;1384}13851386void AnimationPlayerEditor::_seek_value_changed(float p_value, bool p_timeline_only) {1387if (updating || !player || player->is_playing()) {1388return;1389};13901391updating = true;1392String current = player->get_assigned_animation();1393if (current.is_empty() || !player->has_animation(current)) {1394updating = false;1395current = "";1396return;1397};13981399Ref<Animation> anim;1400anim = player->get_animation(current);14011402double pos = CLAMP((double)anim->get_length() * (p_value / frame->get_max()), 0, (double)anim->get_length());1403if (track_editor->is_snap_timeline_enabled()) {1404pos = Math::snapped(pos, _get_editor_step());1405}1406pos = CLAMP(pos, 0, (double)anim->get_length() - CMP_EPSILON2); // Hack: Avoid fposmod with LOOP_LINEAR.14071408if (!p_timeline_only && anim.is_valid() && (!player->is_valid() || !Math::is_equal_approx(pos, player->get_current_animation_position()))) {1409player->seek_internal(pos, true, true, false);1410}14111412track_editor->set_anim_pos(pos);1413}14141415void AnimationPlayerEditor::_animation_player_changed(Object *p_pl) {1416_update_player();14171418if (blend_editor.dialog->is_visible()) {1419_update_animation_blend(); // Update.1420}14211422if (library_editor->is_visible()) {1423library_editor->update_tree();1424}1425}14261427void AnimationPlayerEditor::_animation_libraries_updated() {1428_animation_player_changed(player);1429}14301431void AnimationPlayerEditor::_list_changed() {1432if (is_visible_in_tree()) {1433_update_player();1434}1435}14361437void AnimationPlayerEditor::_animation_finished(const String &p_name) {1438finishing = true;1439}14401441void AnimationPlayerEditor::_current_animation_changed(const String &p_name) {1442if (is_visible_in_tree()) {1443if (finishing) {1444finishing = false; // Maybe redundant since it will be false in the AnimationPlayerEditor::_process(), but for safety.1445return;1446} else if (p_name.is_empty()) {1447// Means [stop].1448frame->set_value(0);1449track_editor->set_anim_pos(0);1450_update_animation();1451return;1452}1453Ref<Animation> anim = player->get_animation(p_name);1454if (anim.is_null()) {1455return;1456}14571458// Determine the read-only status of the animation's library and the libraries as a whole.1459List<StringName> libraries;1460player->get_animation_library_list(&libraries);14611462bool current_animation_library_is_readonly = false;1463bool all_animation_libraries_are_readonly = true;1464for (const StringName &K : libraries) {1465Ref<AnimationLibrary> anim_library = player->get_animation_library(K);1466bool animation_library_is_readonly = EditorNode::get_singleton()->is_resource_read_only(anim_library);1467if (!animation_library_is_readonly) {1468all_animation_libraries_are_readonly = false;1469}14701471List<StringName> animlist;1472anim_library->get_animation_list(&animlist);1473bool animation_found = false;1474for (const StringName &E : animlist) {1475String path = K;1476if (path != "") {1477path += "/";1478}1479path += E;1480if (p_name == path) {1481current_animation_library_is_readonly = animation_library_is_readonly;1482break;1483}1484}1485if (animation_found) {1486break;1487}1488}14891490StringName library_name = player->find_animation_library(anim);14911492bool animation_is_readonly = EditorNode::get_singleton()->is_resource_read_only(anim);14931494track_editor->set_animation(anim, animation_is_readonly);1495_update_animation();14961497#define ITEM_CHECK_DISABLED(m_item) tool_anim->get_popup()->set_item_disabled(tool_anim->get_popup()->get_item_index(m_item), false)1498ITEM_CHECK_DISABLED(TOOL_EDIT_TRANSITIONS);1499ITEM_CHECK_DISABLED(TOOL_EDIT_RESOURCE);1500#undef ITEM_CHECK_DISABLED15011502#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)1503ITEM_CHECK_DISABLED(TOOL_RENAME_ANIM);1504ITEM_CHECK_DISABLED(TOOL_REMOVE_ANIM);1505#undef ITEM_CHECK_DISABLED15061507#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)1508ITEM_CHECK_DISABLED(TOOL_DUPLICATE_ANIM);1509#undef ITEM_CHECK_DISABLED1510}1511}15121513void AnimationPlayerEditor::_animation_key_editor_anim_len_changed(float p_len) {1514frame->set_max(p_len);1515}1516void AnimationPlayerEditor::_animation_key_editor_seek(float p_pos, bool p_timeline_only, bool p_update_position_only) {1517timeline_position = p_pos;15181519if (!is_visible_in_tree() ||1520p_update_position_only ||1521!player ||1522player->is_playing() ||1523!player->has_animation(player->get_assigned_animation())) {1524return;1525}15261527updating = true;1528frame->set_value(track_editor->is_snap_timeline_enabled() ? Math::snapped(p_pos, _get_editor_step()) : p_pos);1529updating = false;1530_seek_value_changed(p_pos, p_timeline_only);1531}15321533void AnimationPlayerEditor::_animation_update_key_frame() {1534if (player) {1535player->advance(0);1536}1537}15381539void AnimationPlayerEditor::_animation_tool_menu(int p_option) {1540String current = _get_current();15411542Ref<Animation> anim;1543if (!current.is_empty()) {1544anim = player->get_animation(current);1545}15461547switch (p_option) {1548case TOOL_NEW_ANIM: {1549_animation_new();1550} break;1551case TOOL_ANIM_LIBRARY: {1552library_editor->set_animation_mixer(fetch_mixer_for_library());1553library_editor->show_dialog();1554} break;1555case TOOL_DUPLICATE_ANIM: {1556_animation_duplicate();1557} break;1558case TOOL_RENAME_ANIM: {1559_animation_rename();1560} break;1561case TOOL_EDIT_TRANSITIONS: {1562_edit_animation_blend();1563} break;1564case TOOL_REMOVE_ANIM: {1565_animation_remove();1566} break;1567case TOOL_EDIT_RESOURCE: {1568if (anim.is_valid()) {1569EditorNode::get_singleton()->edit_resource(anim);1570}1571} break;1572}1573}15741575void AnimationPlayerEditor::_onion_skinning_menu(int p_option) {1576PopupMenu *menu = onion_skinning->get_popup();1577int idx = menu->get_item_index(p_option);15781579switch (p_option) {1580case ONION_SKINNING_ENABLE: {1581onion.enabled = !onion.enabled;15821583if (onion.enabled) {1584if (get_player() && !get_player()->has_animation(SceneStringName(RESET))) {1585EditorNode::get_singleton()->show_warning(TTR("Onion skinning requires a RESET animation."));1586}1587_start_onion_skinning(); // It will check for RESET animation anyway.1588} else {1589_stop_onion_skinning();1590}15911592} break;1593case ONION_SKINNING_PAST: {1594// Ensure at least one of past/future is checked.1595onion.past = onion.future ? !onion.past : true;1596menu->set_item_checked(idx, onion.past);1597} break;1598case ONION_SKINNING_FUTURE: {1599// Ensure at least one of past/future is checked.1600onion.future = onion.past ? !onion.future : true;1601menu->set_item_checked(idx, onion.future);1602} break;1603case ONION_SKINNING_1_STEP: // Fall-through.1604case ONION_SKINNING_2_STEPS:1605case ONION_SKINNING_3_STEPS: {1606onion.steps = (p_option - ONION_SKINNING_1_STEP) + 1;1607int one_frame_idx = menu->get_item_index(ONION_SKINNING_1_STEP);1608for (int i = 0; i <= ONION_SKINNING_LAST_STEPS_OPTION - ONION_SKINNING_1_STEP; i++) {1609menu->set_item_checked(one_frame_idx + i, (int)onion.steps == i + 1);1610}1611} break;1612case ONION_SKINNING_DIFFERENCES_ONLY: {1613onion.differences_only = !onion.differences_only;1614menu->set_item_checked(idx, onion.differences_only);1615} break;1616case ONION_SKINNING_FORCE_WHITE_MODULATE: {1617onion.force_white_modulate = !onion.force_white_modulate;1618menu->set_item_checked(idx, onion.force_white_modulate);1619} break;1620case ONION_SKINNING_INCLUDE_GIZMOS: {1621onion.include_gizmos = !onion.include_gizmos;1622menu->set_item_checked(idx, onion.include_gizmos);1623} break;1624}1625}16261627void AnimationPlayerEditor::shortcut_input(const Ref<InputEvent> &p_ev) {1628ERR_FAIL_COND(p_ev.is_null());16291630Ref<InputEventKey> k = p_ev;1631if (is_visible_in_tree() && k.is_valid() && k->is_pressed() && !k->is_echo()) {1632if (ED_IS_SHORTCUT("animation_editor/stop_animation", p_ev)) {1633_stop_pressed();1634accept_event();1635} else if (ED_IS_SHORTCUT("animation_editor/play_animation", p_ev)) {1636_play_from_pressed();1637accept_event();1638} else if (ED_IS_SHORTCUT("animation_editor/play_animation_backwards", p_ev)) {1639_play_bw_from_pressed();1640accept_event();1641} else if (ED_IS_SHORTCUT("animation_editor/play_animation_from_start", p_ev)) {1642_play_pressed();1643accept_event();1644} else if (ED_IS_SHORTCUT("animation_editor/play_animation_from_end", p_ev)) {1645_play_bw_pressed();1646accept_event();1647} else if (ED_IS_SHORTCUT("animation_editor/go_to_next_keyframe", p_ev)) {1648_go_to_nearest_keyframe(false);1649accept_event();1650} else if (ED_IS_SHORTCUT("animation_editor/go_to_previous_keyframe", p_ev)) {1651_go_to_nearest_keyframe(true);1652accept_event();1653}1654}1655}16561657void AnimationPlayerEditor::_editor_visibility_changed() {1658if (is_visible() && animation->has_selectable_items()) {1659_start_onion_skinning();1660}1661}16621663bool AnimationPlayerEditor::_are_onion_layers_valid() {1664ERR_FAIL_COND_V(!onion.past && !onion.future, false);16651666Size2 capture_size = DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID);1667return onion.captures.size() == onion.get_capture_count() && onion.capture_size == capture_size;1668}16691670void AnimationPlayerEditor::_allocate_onion_layers() {1671_free_onion_layers();16721673int captures = onion.get_capture_count();1674Size2 capture_size = DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID);16751676onion.captures.resize(captures);1677onion.captures_valid.resize(captures);16781679for (int i = 0; i < captures; i++) {1680bool is_present = onion.differences_only && i == captures - 1;16811682// Each capture is a viewport with a canvas item attached that renders a full-size rect with the contents of the main viewport.1683onion.captures[i] = RS::get_singleton()->viewport_create();16841685RS::get_singleton()->viewport_set_size(onion.captures[i], capture_size.width, capture_size.height);1686RS::get_singleton()->viewport_set_update_mode(onion.captures[i], RS::VIEWPORT_UPDATE_ALWAYS);1687RS::get_singleton()->viewport_set_transparent_background(onion.captures[i], !is_present);1688RS::get_singleton()->viewport_attach_canvas(onion.captures[i], onion.capture.canvas);1689}16901691// Reset the capture canvas item to the current root viewport texture (defensive).1692RS::get_singleton()->canvas_item_clear(onion.capture.canvas_item);1693RS::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());16941695onion.capture_size = capture_size;1696}16971698void AnimationPlayerEditor::_free_onion_layers() {1699for (uint32_t i = 0; i < onion.captures.size(); i++) {1700if (onion.captures[i].is_valid()) {1701RS::get_singleton()->free(onion.captures[i]);1702}1703}1704onion.captures.clear();1705onion.captures_valid.clear();1706}17071708void AnimationPlayerEditor::_prepare_onion_layers_1() {1709// This would be called per viewport and we want to act once only.1710int64_t cur_frame = get_tree()->get_frame();1711if (cur_frame == onion.last_frame) {1712return;1713}17141715if (!onion.enabled || !is_visible() || !get_player() || !get_player()->has_animation(SceneStringName(RESET))) {1716_stop_onion_skinning();1717return;1718}17191720onion.last_frame = cur_frame;17211722// Refresh viewports with no onion layers overlaid.1723onion.can_overlay = false;1724plugin->update_overlays();17251726if (player->is_playing()) {1727return;1728}17291730// And go to next step afterwards.1731callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_2_prolog).call_deferred();1732}17331734void AnimationPlayerEditor::_prepare_onion_layers_2_prolog() {1735Ref<Animation> anim = player->get_animation(player->get_assigned_animation());1736if (anim.is_null()) {1737return;1738}17391740if (!_are_onion_layers_valid()) {1741_allocate_onion_layers();1742}17431744// Hide superfluous elements that would make the overlay unnecessary cluttered.1745if (Node3DEditor::get_singleton()->is_visible()) {1746// 3D1747onion.temp.spatial_edit_state = Node3DEditor::get_singleton()->get_state();1748Dictionary new_state = onion.temp.spatial_edit_state.duplicate();1749new_state["show_grid"] = false;1750new_state["show_origin"] = false;1751Array orig_vp = onion.temp.spatial_edit_state["viewports"];1752Array vp;1753vp.resize(4);1754for (int i = 0; i < vp.size(); i++) {1755Dictionary d = ((Dictionary)orig_vp[i]).duplicate();1756d["use_environment"] = false;1757d["doppler"] = false;1758d["listener"] = false;1759d["gizmos"] = onion.include_gizmos ? d["gizmos"] : Variant(false);1760d["information"] = false;1761vp[i] = d;1762}1763new_state["viewports"] = vp;1764// TODO: Save/restore only affected entries.1765Node3DEditor::get_singleton()->set_state(new_state);1766} else {1767// CanvasItemEditor.1768onion.temp.canvas_edit_state = CanvasItemEditor::get_singleton()->get_state();1769Dictionary new_state = onion.temp.canvas_edit_state.duplicate();1770new_state["show_origin"] = false;1771new_state["show_grid"] = false;1772new_state["show_rulers"] = false;1773new_state["show_guides"] = false;1774new_state["show_helpers"] = false;1775new_state["show_zoom_control"] = false;1776new_state["show_edit_locks"] = false;1777new_state["grid_visibility"] = 2; // TODO: Expose CanvasItemEditor::GRID_VISIBILITY_HIDE somehow and use it.1778new_state["show_transformation_gizmos"] = onion.include_gizmos ? new_state["gizmos"] : Variant(false);1779// TODO: Save/restore only affected entries.1780CanvasItemEditor::get_singleton()->set_state(new_state);1781}17821783// Tweak the root viewport to ensure it's rendered before our target.1784RID root_vp = get_tree()->get_root()->get_viewport_rid();1785onion.temp.screen_rect = Rect2(Vector2(), DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID));1786RS::get_singleton()->viewport_attach_to_screen(root_vp, Rect2(), DisplayServer::INVALID_WINDOW_ID);1787RS::get_singleton()->viewport_set_update_mode(root_vp, RS::VIEWPORT_UPDATE_ALWAYS);17881789RID present_rid;1790if (onion.differences_only) {1791// Capture present scene as it is.1792RS::get_singleton()->canvas_item_set_material(onion.capture.canvas_item, RID());1793present_rid = onion.captures[onion.captures.size() - 1];1794RS::get_singleton()->viewport_set_active(present_rid, true);1795RS::get_singleton()->viewport_set_parent_viewport(root_vp, present_rid);1796RS::get_singleton()->draw(false);1797RS::get_singleton()->viewport_set_active(present_rid, false);1798}17991800// Backup current animation state.1801onion.temp.anim_values_backup = player->make_backup();1802onion.temp.anim_player_position = player->get_current_animation_position();18031804// Render every past/future step with the capture shader.18051806RS::get_singleton()->canvas_item_set_material(onion.capture.canvas_item, onion.capture.material->get_rid());1807onion.capture.material->set_shader_parameter("bkg_color", GLOBAL_GET("rendering/environment/defaults/default_clear_color"));1808onion.capture.material->set_shader_parameter("differences_only", onion.differences_only);1809onion.capture.material->set_shader_parameter("present", onion.differences_only ? RS::get_singleton()->viewport_get_texture(present_rid) : RID());1810onion.capture.material->set_shader_parameter("dir_color", onion.force_white_modulate ? Color(1, 1, 1) : Color(EDITOR_GET("editors/animation/onion_layers_past_color")));18111812uint32_t p_capture_idx = 0;1813int first_step_offset = onion.past ? -(int)onion.steps : 0;1814_prepare_onion_layers_2_step_prepare(first_step_offset, p_capture_idx);1815}18161817void AnimationPlayerEditor::_prepare_onion_layers_2_step_prepare(int p_step_offset, uint32_t p_capture_idx) {1818uint32_t next_capture_idx = p_capture_idx;1819if (p_step_offset == 0) {1820// Skip present step and switch to the color of future.1821if (!onion.force_white_modulate) {1822onion.capture.material->set_shader_parameter("dir_color", EDITOR_GET("editors/animation/onion_layers_future_color"));1823}1824} else {1825Ref<Animation> anim = player->get_animation(player->get_assigned_animation());1826double pos = onion.temp.anim_player_position + p_step_offset * anim->get_step();18271828bool valid = anim->get_loop_mode() != Animation::LOOP_NONE || (pos >= 0 && pos <= anim->get_length());1829onion.captures_valid[p_capture_idx] = valid;1830if (valid) {1831player->seek_internal(pos, true, true, false);1832OS::get_singleton()->get_main_loop()->process(0);1833// This is the key: process the frame and let all callbacks/updates/notifications happen1834// so everything (transforms, skeletons, etc.) is up-to-date visually.1835callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_2_step_capture).call_deferred(p_step_offset, p_capture_idx);1836return;1837} else {1838next_capture_idx++;1839}1840}18411842int last_step_offset = onion.future ? onion.steps : 0;1843if (p_step_offset < last_step_offset) {1844_prepare_onion_layers_2_step_prepare(p_step_offset + 1, next_capture_idx);1845} else {1846_prepare_onion_layers_2_epilog();1847}1848}18491850void AnimationPlayerEditor::_prepare_onion_layers_2_step_capture(int p_step_offset, uint32_t p_capture_idx) {1851DEV_ASSERT(p_step_offset != 0);1852DEV_ASSERT(onion.captures_valid[p_capture_idx]);18531854RID root_vp = get_tree()->get_root()->get_viewport_rid();1855RS::get_singleton()->viewport_set_active(onion.captures[p_capture_idx], true);1856RS::get_singleton()->viewport_set_parent_viewport(root_vp, onion.captures[p_capture_idx]);1857RS::get_singleton()->draw(false);1858RS::get_singleton()->viewport_set_active(onion.captures[p_capture_idx], false);18591860int last_step_offset = onion.future ? onion.steps : 0;1861if (p_step_offset < last_step_offset) {1862_prepare_onion_layers_2_step_prepare(p_step_offset + 1, p_capture_idx + 1);1863} else {1864_prepare_onion_layers_2_epilog();1865}1866}18671868void AnimationPlayerEditor::_prepare_onion_layers_2_epilog() {1869// Restore root viewport.1870RID root_vp = get_tree()->get_root()->get_viewport_rid();1871RS::get_singleton()->viewport_set_parent_viewport(root_vp, RID());1872RS::get_singleton()->viewport_attach_to_screen(root_vp, onion.temp.screen_rect, DisplayServer::MAIN_WINDOW_ID);1873RS::get_singleton()->viewport_set_update_mode(root_vp, RS::VIEWPORT_UPDATE_WHEN_VISIBLE);18741875// Restore animation state.1876// Here we're combine the power of seeking back to the original position and1877// restoring the values backup. In most cases they will bring the same value back,1878// but there are cases handled by one that the other can't.1879// Namely:1880// - Seeking won't restore any values that may have been modified by the user1881// in the node after the last time the AnimationPlayer updated it.1882// - Restoring the backup won't account for values that are not directly involved1883// in the animation but a consequence of them (e.g., SkeletonModification2DLookAt).1884// FIXME: Since backup of values is based on the reset animation, only values1885// backed by a proper reset animation will work correctly with onion1886// skinning and the possibility to restore the values mentioned in the1887// first point above is gone. Still good enough.1888player->seek_internal(onion.temp.anim_player_position, true, true, false);1889player->restore(onion.temp.anim_values_backup);18901891// Restore state of main editors.1892if (Node3DEditor::get_singleton()->is_visible()) {1893// 3D1894Node3DEditor::get_singleton()->set_state(onion.temp.spatial_edit_state);1895} else { // CanvasItemEditor1896// 2D1897CanvasItemEditor::get_singleton()->set_state(onion.temp.canvas_edit_state);1898}18991900// Update viewports with skin layers overlaid for the actual engine loop render.1901onion.can_overlay = true;1902plugin->update_overlays();1903}19041905void AnimationPlayerEditor::_start_onion_skinning() {1906if (get_player() && !get_player()->has_animation(SceneStringName(RESET))) {1907onion.enabled = false;1908onion_toggle->set_pressed_no_signal(false);1909return;1910}1911if (!get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1))) {1912get_tree()->connect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1));1913}1914}19151916void AnimationPlayerEditor::_stop_onion_skinning() {1917if (get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1))) {1918get_tree()->disconnect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1));19191920_free_onion_layers();19211922// Clean up.1923onion.can_overlay = false;1924plugin->update_overlays();1925onion.temp = {};1926}1927}19281929void AnimationPlayerEditor::_pin_pressed() {1930SceneTreeDock::get_singleton()->get_tree_editor()->update_tree();1931}19321933AnimationMixer *AnimationPlayerEditor::fetch_mixer_for_library() const {1934if (!original_node) {1935return nullptr;1936}1937// Does AnimationTree have AnimationPlayer?1938if (original_node->is_class("AnimationTree")) {1939AnimationTree *src_tree = Object::cast_to<AnimationTree>(original_node);1940Node *src_player = src_tree->get_node_or_null(src_tree->get_animation_player());1941if (src_player) {1942return Object::cast_to<AnimationMixer>(src_player);1943}1944}1945return original_node;1946}19471948Node *AnimationPlayerEditor::get_cached_root_node() const {1949return ObjectDB::get_instance<Node>(cached_root_node_id);1950}19511952bool AnimationPlayerEditor::_validate_tracks(const Ref<Animation> p_anim) {1953bool is_valid = true;1954if (p_anim.is_null()) {1955return true; // There is a problem outside of the animation track.1956}1957int len = p_anim->get_track_count();1958for (int i = 0; i < len; i++) {1959Animation::TrackType ttype = p_anim->track_get_type(i);1960if (ttype == Animation::TYPE_ROTATION_3D) {1961int key_len = p_anim->track_get_key_count(i);1962for (int j = 0; j < key_len; j++) {1963Quaternion q;1964p_anim->rotation_track_get_key(i, j, &q);1965ERR_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.");1966}1967} else if (ttype == Animation::TYPE_VALUE) {1968int key_len = p_anim->track_get_key_count(i);1969if (key_len == 0) {1970continue;1971}1972switch (p_anim->track_get_key_value(i, 0).get_type()) {1973case Variant::QUATERNION: {1974for (int j = 0; j < key_len; j++) {1975Quaternion q = Quaternion(p_anim->track_get_key_value(i, j));1976if (!q.is_normalized()) {1977is_valid = false;1978ERR_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.");1979}1980}1981} break;1982case Variant::TRANSFORM3D: {1983for (int j = 0; j < key_len; j++) {1984Transform3D t = Transform3D(p_anim->track_get_key_value(i, j));1985if (!t.basis.orthonormalized().is_rotation()) {1986is_valid = false;1987ERR_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.");1988}1989}1990} break;1991default: {1992} break;1993}1994}1995}1996return is_valid;1997}19981999void AnimationPlayerEditor::_bind_methods() {2000// Needed for UndoRedo.2001ClassDB::bind_method(D_METHOD("_animation_player_changed"), &AnimationPlayerEditor::_animation_player_changed);2002ClassDB::bind_method(D_METHOD("_animation_update_key_frame"), &AnimationPlayerEditor::_animation_update_key_frame);2003ClassDB::bind_method(D_METHOD("_start_onion_skinning"), &AnimationPlayerEditor::_start_onion_skinning);2004ClassDB::bind_method(D_METHOD("_stop_onion_skinning"), &AnimationPlayerEditor::_stop_onion_skinning);20052006ADD_SIGNAL(MethodInfo("animation_selected", PropertyInfo(Variant::STRING, "name")));2007}20082009AnimationPlayerEditor *AnimationPlayerEditor::singleton = nullptr;20102011AnimationPlayer *AnimationPlayerEditor::get_player() const {2012return player;2013}20142015AnimationMixer *AnimationPlayerEditor::get_editing_node() const {2016return original_node;2017}20182019AnimationPlayerEditor::AnimationPlayerEditor(AnimationPlayerEditorPlugin *p_plugin) {2020plugin = p_plugin;2021singleton = this;20222023set_focus_mode(FOCUS_ALL);2024set_process_shortcut_input(true);20252026HBoxContainer *hb = memnew(HBoxContainer);2027add_child(hb);20282029HBoxContainer *playback_container = memnew(HBoxContainer);2030playback_container->set_layout_direction(LAYOUT_DIRECTION_LTR);2031hb->add_child(playback_container);20322033play_bw_from = memnew(Button);2034play_bw_from->set_theme_type_variation(SceneStringName(FlatButton));2035play_bw_from->set_tooltip_text(TTR("Play Animation Backwards"));2036playback_container->add_child(play_bw_from);20372038play_bw = memnew(Button);2039play_bw->set_theme_type_variation(SceneStringName(FlatButton));2040play_bw->set_tooltip_text(TTR("Play Animation Backwards from End"));2041playback_container->add_child(play_bw);20422043stop = memnew(Button);2044stop->set_theme_type_variation(SceneStringName(FlatButton));2045stop->set_tooltip_text(TTR("Pause/Stop Animation"));2046playback_container->add_child(stop);20472048play = memnew(Button);2049play->set_theme_type_variation(SceneStringName(FlatButton));2050play->set_tooltip_text(TTR("Play Animation from Start"));2051playback_container->add_child(play);20522053play_from = memnew(Button);2054play_from->set_theme_type_variation(SceneStringName(FlatButton));2055play_from->set_tooltip_text(TTR("Play Animation"));2056playback_container->add_child(play_from);20572058frame = memnew(SpinBox);2059hb->add_child(frame);2060frame->set_custom_minimum_size(Size2(80, 0) * EDSCALE);2061frame->set_stretch_ratio(2);2062frame->set_step(0.0001);2063frame->set_tooltip_text(TTR("Animation position (in seconds)."));20642065hb->add_child(memnew(VSeparator));20662067scale = memnew(LineEdit);2068hb->add_child(scale);2069scale->set_h_size_flags(SIZE_EXPAND_FILL);2070scale->set_stretch_ratio(1);2071scale->set_tooltip_text(TTR("Scale animation playback globally for the node."));2072scale->hide();20732074delete_dialog = memnew(ConfirmationDialog);2075add_child(delete_dialog);2076delete_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationPlayerEditor::_animation_remove_confirmed));20772078tool_anim = memnew(MenuButton);2079tool_anim->set_shortcut_context(this);2080tool_anim->set_flat(false);2081tool_anim->set_tooltip_text(TTR("Animation Tools"));2082tool_anim->set_text(TTR("Animation"));2083tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/new_animation", TTRC("New...")), TOOL_NEW_ANIM);2084tool_anim->get_popup()->add_separator();2085tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/animation_libraries", TTRC("Manage Animations...")), TOOL_ANIM_LIBRARY);2086tool_anim->get_popup()->add_separator();2087tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/duplicate_animation", TTRC("Duplicate...")), TOOL_DUPLICATE_ANIM);2088tool_anim->get_popup()->add_separator();2089tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/rename_animation", TTRC("Rename...")), TOOL_RENAME_ANIM);2090tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/edit_transitions", TTRC("Edit Transitions...")), TOOL_EDIT_TRANSITIONS);2091tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/open_animation_in_inspector", TTRC("Open in Inspector")), TOOL_EDIT_RESOURCE);2092tool_anim->get_popup()->add_separator();2093tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/remove_animation", TTRC("Remove")), TOOL_REMOVE_ANIM);2094tool_anim->set_disabled(true);2095hb->add_child(tool_anim);20962097animation = memnew(OptionButton);2098hb->add_child(animation);2099animation->set_accessibility_name(TTRC("Animation"));2100animation->set_h_size_flags(SIZE_EXPAND_FILL);2101animation->set_tooltip_text(TTR("Display list of animations in player."));2102animation->set_clip_text(true);2103animation->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);21042105autoplay = memnew(Button);2106autoplay->set_theme_type_variation(SceneStringName(FlatButton));2107hb->add_child(autoplay);2108autoplay->set_tooltip_text(TTR("Autoplay on Load"));21092110hb->add_child(memnew(VSeparator));21112112track_editor = memnew(AnimationTrackEditor);2113hb->add_child(track_editor->get_edit_menu());21142115hb->add_child(memnew(VSeparator));21162117onion_toggle = memnew(Button);2118onion_toggle->set_theme_type_variation(SceneStringName(FlatButton));2119onion_toggle->set_toggle_mode(true);2120onion_toggle->set_tooltip_text(TTR("Enable Onion Skinning"));2121onion_toggle->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_onion_skinning_menu).bind(ONION_SKINNING_ENABLE));2122hb->add_child(onion_toggle);21232124onion_skinning = memnew(MenuButton);2125onion_skinning->set_accessibility_name(TTRC("Onion Skinning Options"));2126onion_skinning->set_flat(false);2127onion_skinning->set_theme_type_variation("FlatMenuButton");2128onion_skinning->set_tooltip_text(TTR("Onion Skinning Options"));2129onion_skinning->get_popup()->add_separator(TTR("Directions"));2130// TRANSLATORS: Opposite of "Future", refers to a direction in animation onion skinning.2131onion_skinning->get_popup()->add_check_item(TTR("Past"), ONION_SKINNING_PAST);2132onion_skinning->get_popup()->set_item_checked(-1, true);2133// TRANSLATORS: Opposite of "Past", refers to a direction in animation onion skinning.2134onion_skinning->get_popup()->add_check_item(TTR("Future"), ONION_SKINNING_FUTURE);2135onion_skinning->get_popup()->add_separator(TTR("Depth"));2136onion_skinning->get_popup()->add_radio_check_item(TTR("1 step"), ONION_SKINNING_1_STEP);2137onion_skinning->get_popup()->set_item_checked(-1, true);2138onion_skinning->get_popup()->add_radio_check_item(TTR("2 steps"), ONION_SKINNING_2_STEPS);2139onion_skinning->get_popup()->add_radio_check_item(TTR("3 steps"), ONION_SKINNING_3_STEPS);2140onion_skinning->get_popup()->add_separator();2141onion_skinning->get_popup()->add_check_item(TTR("Differences Only"), ONION_SKINNING_DIFFERENCES_ONLY);2142onion_skinning->get_popup()->add_check_item(TTR("Force White Modulate"), ONION_SKINNING_FORCE_WHITE_MODULATE);2143onion_skinning->get_popup()->add_check_item(TTR("Include Gizmos (3D)"), ONION_SKINNING_INCLUDE_GIZMOS);2144hb->add_child(onion_skinning);21452146hb->add_child(memnew(VSeparator));21472148pin = memnew(Button);2149pin->set_theme_type_variation(SceneStringName(FlatButton));2150pin->set_toggle_mode(true);2151pin->set_tooltip_text(TTR("Pin AnimationPlayer"));2152hb->add_child(pin);2153pin->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_pin_pressed));21542155file = memnew(EditorFileDialog);2156add_child(file);21572158name_dialog = memnew(ConfirmationDialog);2159name_dialog->set_title(TTR("Create New Animation"));2160name_dialog->set_hide_on_ok(false);2161add_child(name_dialog);2162VBoxContainer *vb = memnew(VBoxContainer);2163name_dialog->add_child(vb);21642165name_title = memnew(Label(TTR("Animation Name:")));2166vb->add_child(name_title);21672168HBoxContainer *name_hb = memnew(HBoxContainer);2169name = memnew(LineEdit);2170name_hb->add_child(name);2171name->set_h_size_flags(SIZE_EXPAND_FILL);2172library = memnew(OptionButton);2173name_hb->add_child(library);2174library->hide();2175vb->add_child(name_hb);2176name_dialog->register_text_enter(name);21772178error_dialog = memnew(AcceptDialog);2179error_dialog->set_ok_button_text(TTR("Close"));2180error_dialog->set_title(TTR("Error!"));2181name_dialog->add_child(error_dialog);21822183name_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationPlayerEditor::_animation_name_edited));21842185blend_editor.dialog = memnew(AcceptDialog);2186blend_editor.dialog->set_title(TTR("Cross-Animation Blend Times"));2187blend_editor.dialog->set_ok_button_text(TTR("Close"));2188blend_editor.dialog->set_hide_on_ok(true);2189add_child(blend_editor.dialog);21902191VBoxContainer *blend_vb = memnew(VBoxContainer);2192blend_editor.dialog->add_child(blend_vb);21932194blend_editor.tree = memnew(Tree);2195blend_editor.tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);2196blend_editor.tree->set_hide_root(true);2197blend_editor.tree->set_columns(2);2198blend_editor.tree->set_column_expand_ratio(0, 10);2199blend_editor.tree->set_column_clip_content(0, true);2200blend_editor.tree->set_column_expand_ratio(1, 3);2201blend_editor.tree->set_column_clip_content(1, true);2202blend_vb->add_margin_child(TTR("Blend Times:"), blend_editor.tree, true);2203blend_editor.tree->connect(SNAME("item_edited"), callable_mp(this, &AnimationPlayerEditor::_blend_edited));22042205blend_editor.next = memnew(OptionButton);2206blend_editor.next->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);2207blend_vb->add_margin_child(TTR("Next (Auto Queue):"), blend_editor.next);22082209autoplay->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_autoplay_pressed));2210autoplay->set_toggle_mode(true);2211play->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_play_pressed));2212play_from->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_play_from_pressed));2213play_bw->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_play_bw_pressed));2214play_bw_from->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_play_bw_from_pressed));2215stop->connect(SceneStringName(pressed), callable_mp(this, &AnimationPlayerEditor::_stop_pressed));22162217animation->connect(SceneStringName(item_selected), callable_mp(this, &AnimationPlayerEditor::_animation_selected));22182219frame->connect(SceneStringName(value_changed), callable_mp(this, &AnimationPlayerEditor::_seek_value_changed).bind(false));2220scale->connect(SceneStringName(text_submitted), callable_mp(this, &AnimationPlayerEditor::_scale_changed));22212222add_child(track_editor);2223track_editor->set_v_size_flags(SIZE_EXPAND_FILL);2224track_editor->connect(SNAME("timeline_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_key_editor_seek));2225track_editor->connect(SNAME("animation_len_changed"), callable_mp(this, &AnimationPlayerEditor::_animation_key_editor_anim_len_changed));22262227_update_player();22282229library_editor = memnew(AnimationLibraryEditor);2230add_child(library_editor);2231library_editor->connect(SNAME("update_editor"), callable_mp(this, &AnimationPlayerEditor::_animation_player_changed));22322233// Onion skinning.22342235track_editor->connect(SceneStringName(visibility_changed), callable_mp(this, &AnimationPlayerEditor::_editor_visibility_changed));22362237onion.capture.canvas = RS::get_singleton()->canvas_create();2238onion.capture.canvas_item = RS::get_singleton()->canvas_item_create();2239RS::get_singleton()->canvas_item_set_parent(onion.capture.canvas_item, onion.capture.canvas);22402241onion.capture.material.instantiate();22422243onion.capture.shader.instantiate();2244onion.capture.shader->set_code(R"(2245// Animation editor onion skinning shader.22462247shader_type canvas_item;22482249uniform vec4 bkg_color;2250uniform vec4 dir_color;2251uniform bool differences_only;2252uniform sampler2D present;22532254float zero_if_equal(vec4 a, vec4 b) {2255return smoothstep(0.0, 0.005, length(a.rgb - b.rgb) / sqrt(3.0));2256}22572258void fragment() {2259vec4 capture_samp = texture(TEXTURE, UV);2260float bkg_mask = zero_if_equal(capture_samp, bkg_color);2261float diff_mask = 1.0;2262if (differences_only) {2263// FIXME: If Y-flips across render target, canvas item, etc. was handled correctly,2264// this would not be as convoluted in the shader.2265vec4 capture_samp2 = texture(TEXTURE, vec2(UV.x, 1.0 - UV.y));2266vec4 present_samp = texture(present, vec2(UV.x, 1.0 - UV.y));2267diff_mask = 1.0 - zero_if_equal(present_samp, bkg_color);2268}2269COLOR = vec4(capture_samp.rgb * dir_color.rgb, bkg_mask * diff_mask);2270}2271)");2272RS::get_singleton()->material_set_shader(onion.capture.material->get_rid(), onion.capture.shader->get_rid());22732274ED_SHORTCUT("animation_editor/stop_animation", TTRC("Pause/Stop Animation"), Key::S);2275ED_SHORTCUT("animation_editor/play_animation", TTRC("Play Animation"), Key::D);2276ED_SHORTCUT("animation_editor/play_animation_backwards", TTRC("Play Animation Backwards"), Key::A);2277ED_SHORTCUT("animation_editor/play_animation_from_start", TTRC("Play Animation from Start"), KeyModifierMask::SHIFT + Key::D);2278ED_SHORTCUT("animation_editor/play_animation_from_end", TTRC("Play Animation Backwards from End"), KeyModifierMask::SHIFT + Key::A);2279ED_SHORTCUT("animation_editor/go_to_next_keyframe", TTRC("Go to Next Keyframe"), KeyModifierMask::SHIFT + KeyModifierMask::ALT + Key::D);2280ED_SHORTCUT("animation_editor/go_to_previous_keyframe", TTRC("Go to Previous Keyframe"), KeyModifierMask::SHIFT + KeyModifierMask::ALT + Key::A);2281}22822283AnimationPlayerEditor::~AnimationPlayerEditor() {2284_free_onion_layers();2285RS::get_singleton()->free(onion.capture.canvas);2286RS::get_singleton()->free(onion.capture.canvas_item);2287onion.capture = {};2288}22892290void AnimationPlayerEditorPlugin::_notification(int p_what) {2291switch (p_what) {2292case NOTIFICATION_ENTER_TREE: {2293Node3DEditor::get_singleton()->connect(SNAME("transform_key_request"), callable_mp(this, &AnimationPlayerEditorPlugin::_transform_key_request));2294InspectorDock::get_inspector_singleton()->connect(SNAME("property_keyed"), callable_mp(this, &AnimationPlayerEditorPlugin::_property_keyed));2295anim_editor->get_track_editor()->connect(SNAME("keying_changed"), callable_mp(this, &AnimationPlayerEditorPlugin::_update_keying));2296InspectorDock::get_inspector_singleton()->connect(SNAME("edited_object_changed"), callable_mp(anim_editor->get_track_editor(), &AnimationTrackEditor::update_keying));2297set_force_draw_over_forwarding_enabled();2298} break;2299}2300}23012302void AnimationPlayerEditorPlugin::_property_keyed(const String &p_keyed, const Variant &p_value, bool p_advance) {2303AnimationTrackEditor *te = anim_editor->get_track_editor();2304if (!te || !te->has_keying()) {2305return;2306}2307te->_clear_selection();2308te->insert_value_key(p_keyed, p_advance);2309}23102311void AnimationPlayerEditorPlugin::_transform_key_request(Object *sp, const String &p_sub, const Transform3D &p_key) {2312if (!anim_editor->get_track_editor()->has_keying()) {2313return;2314}2315Node3D *s = Object::cast_to<Node3D>(sp);2316if (!s) {2317return;2318}2319anim_editor->get_track_editor()->insert_transform_key(s, p_sub, Animation::TYPE_POSITION_3D, p_key.origin);2320anim_editor->get_track_editor()->insert_transform_key(s, p_sub, Animation::TYPE_ROTATION_3D, p_key.basis.get_rotation_quaternion());2321anim_editor->get_track_editor()->insert_transform_key(s, p_sub, Animation::TYPE_SCALE_3D, p_key.basis.get_scale());2322}23232324void AnimationPlayerEditorPlugin::_update_keying() {2325InspectorDock::get_inspector_singleton()->set_keying(anim_editor->get_track_editor()->has_keying());2326}23272328void AnimationPlayerEditorPlugin::edit(Object *p_object) {2329if (player && anim_editor && anim_editor->is_pinned()) {2330return; // Ignore, pinned.2331}23322333player = nullptr;2334if (!p_object) {2335return;2336}2337last_mixer = p_object->get_instance_id();23382339AnimationMixer *src_node = Object::cast_to<AnimationMixer>(p_object);2340bool is_dummy = false;2341if (!p_object->is_class("AnimationPlayer")) {2342// If it needs dummy AnimationPlayer, assign original AnimationMixer to LibraryEditor.2343_update_dummy_player(src_node);23442345is_dummy = true;23462347if (!src_node->is_connected(SNAME("mixer_updated"), callable_mp(this, &AnimationPlayerEditorPlugin::_update_dummy_player))) {2348src_node->connect(SNAME("mixer_updated"), callable_mp(this, &AnimationPlayerEditorPlugin::_update_dummy_player).bind(src_node), CONNECT_DEFERRED);2349}2350if (!src_node->is_connected(SNAME("animation_libraries_updated"), callable_mp(this, &AnimationPlayerEditorPlugin::_update_dummy_player))) {2351src_node->connect(SNAME("animation_libraries_updated"), callable_mp(this, &AnimationPlayerEditorPlugin::_update_dummy_player).bind(src_node), CONNECT_DEFERRED);2352}2353} else {2354_clear_dummy_player();2355player = Object::cast_to<AnimationPlayer>(p_object);2356}2357player->set_dummy(is_dummy);23582359anim_editor->edit(src_node, player, is_dummy);2360}23612362void AnimationPlayerEditorPlugin::_clear_dummy_player() {2363if (!dummy_player) {2364return;2365}2366Node *parent = dummy_player->get_parent();2367if (parent) {2368callable_mp(parent, &Node::remove_child).call_deferred(dummy_player);2369}2370dummy_player->queue_free();2371dummy_player = nullptr;2372}23732374void AnimationPlayerEditorPlugin::_update_dummy_player(AnimationMixer *p_mixer) {2375// Check current editing object.2376if (p_mixer->get_instance_id() != last_mixer && p_mixer->is_connected(SNAME("mixer_updated"), callable_mp(this, &AnimationPlayerEditorPlugin::_update_dummy_player))) {2377p_mixer->disconnect(SNAME("mixer_updated"), callable_mp(this, &AnimationPlayerEditorPlugin::_update_dummy_player));2378return;2379}23802381// Add dummy player to scene.2382if (!dummy_player) {2383Node *parent = p_mixer->get_parent();2384ERR_FAIL_NULL(parent);2385dummy_player = memnew(AnimationPlayer);2386dummy_player->set_active(false); // Inactive as default, it will be activated if the AnimationPlayerEditor visibility is changed.2387parent->add_child(dummy_player);2388}2389player = dummy_player;23902391// Convert AnimationTree (AnimationMixer) to AnimationPlayer.2392AnimationMixer *default_node = memnew(AnimationMixer);2393List<PropertyInfo> pinfo;2394default_node->get_property_list(&pinfo);2395for (const PropertyInfo &E : pinfo) {2396if (!(E.usage & PROPERTY_USAGE_STORAGE)) {2397continue;2398}2399if (E.name != "script" && E.name != "active" && E.name != "deterministic" && E.name != "root_motion_track") {2400dummy_player->set(E.name, p_mixer->get(E.name));2401}2402}2403memdelete(default_node);24042405if (anim_editor) {2406anim_editor->_update_player();2407}2408}24092410bool AnimationPlayerEditorPlugin::handles(Object *p_object) const {2411return p_object->is_class("AnimationPlayer") || p_object->is_class("AnimationTree") || p_object->is_class("AnimationMixer");2412}24132414void AnimationPlayerEditorPlugin::make_visible(bool p_visible) {2415if (p_visible) {2416// if AnimationTree editor is visible, do not occupy the bottom panel2417if (AnimationTreeEditor::get_singleton() && AnimationTreeEditor::get_singleton()->is_visible_in_tree()) {2418return;2419}2420EditorNode::get_bottom_panel()->make_item_visible(anim_editor);2421anim_editor->set_process(true);2422anim_editor->ensure_visibility();2423}2424}24252426AnimationPlayerEditorPlugin::AnimationPlayerEditorPlugin() {2427anim_editor = memnew(AnimationPlayerEditor(this));2428EditorNode::get_bottom_panel()->add_item(TTRC("Animation"), anim_editor, ED_SHORTCUT_AND_COMMAND("bottom_panels/toggle_animation_bottom_panel", TTRC("Toggle Animation Bottom Panel"), KeyModifierMask::ALT | Key::N));2429}24302431AnimationPlayerEditorPlugin::~AnimationPlayerEditorPlugin() {2432if (dummy_player) {2433memdelete(dummy_player);2434}2435}24362437// AnimationTrackKeyEditEditorPlugin24382439bool EditorInspectorPluginAnimationTrackKeyEdit::can_handle(Object *p_object) {2440return Object::cast_to<AnimationTrackKeyEdit>(p_object) != nullptr;2441}24422443void EditorInspectorPluginAnimationTrackKeyEdit::parse_begin(Object *p_object) {2444AnimationTrackKeyEdit *atk = Object::cast_to<AnimationTrackKeyEdit>(p_object);2445ERR_FAIL_NULL(atk);24462447atk_editor = memnew(AnimationTrackKeyEditEditor(atk->animation, atk->track, atk->key_ofs, atk->use_fps));2448add_custom_control(atk_editor);2449}24502451AnimationTrackKeyEditEditorPlugin::AnimationTrackKeyEditEditorPlugin() {2452atk_plugin = memnew(EditorInspectorPluginAnimationTrackKeyEdit);2453EditorInspector::add_inspector_plugin(atk_plugin);2454}24552456bool AnimationTrackKeyEditEditorPlugin::handles(Object *p_object) const {2457return p_object->is_class("AnimationTrackKeyEdit");2458}24592460bool EditorInspectorPluginAnimationMarkerKeyEdit::can_handle(Object *p_object) {2461return Object::cast_to<AnimationMarkerKeyEdit>(p_object) != nullptr;2462}24632464void EditorInspectorPluginAnimationMarkerKeyEdit::parse_begin(Object *p_object) {2465AnimationMarkerKeyEdit *amk = Object::cast_to<AnimationMarkerKeyEdit>(p_object);2466ERR_FAIL_NULL(amk);24672468amk_editor = memnew(AnimationMarkerKeyEditEditor(amk->animation, amk->marker_name, amk->use_fps));2469add_custom_control(amk_editor);2470}24712472AnimationMarkerKeyEditEditorPlugin::AnimationMarkerKeyEditEditorPlugin() {2473amk_plugin = memnew(EditorInspectorPluginAnimationMarkerKeyEdit);2474EditorInspector::add_inspector_plugin(amk_plugin);2475}24762477bool AnimationMarkerKeyEditEditorPlugin::handles(Object *p_object) const {2478return p_object->is_class("AnimationMarkerKeyEdit");2479}248024812482