Path: blob/master/editor/animation/animation_track_editor.cpp
21147 views
/**************************************************************************/1/* animation_track_editor.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_track_editor.h"3132#include "animation_track_editor_plugins.h"33#include "core/config/project_settings.h"34#include "core/error/error_macros.h"35#include "core/input/input.h"36#include "core/string/translation_server.h"37#include "editor/animation/animation_bezier_editor.h"38#include "editor/animation/animation_player_editor_plugin.h"39#include "editor/docks/inspector_dock.h"40#include "editor/editor_node.h"41#include "editor/editor_string_names.h"42#include "editor/editor_undo_redo_manager.h"43#include "editor/gui/editor_spin_slider.h"44#include "editor/gui/editor_validation_panel.h"45#include "editor/inspector/multi_node_edit.h"46#include "editor/scene/scene_tree_editor.h"47#include "editor/script/script_editor_plugin.h"48#include "editor/settings/editor_settings.h"49#include "editor/themes/editor_scale.h"50#include "scene/3d/mesh_instance_3d.h"51#include "scene/animation/animation_player.h"52#include "scene/animation/tween.h"53#include "scene/gui/check_box.h"54#include "scene/gui/color_picker.h"55#include "scene/gui/flow_container.h"56#include "scene/gui/grid_container.h"57#include "scene/gui/option_button.h"58#include "scene/gui/panel_container.h"59#include "scene/gui/separator.h"60#include "scene/gui/slider.h"61#include "scene/gui/spin_box.h"62#include "scene/gui/texture_rect.h"63#include "scene/gui/view_panner.h"64#include "scene/main/window.h"65#include "servers/audio/audio_stream.h"6667constexpr double FPS_DECIMAL = 1.0;68constexpr double SECOND_DECIMAL = 0.0001;6970void AnimationTrackKeyEdit::_bind_methods() {71ClassDB::bind_method(D_METHOD("_update_obj"), &AnimationTrackKeyEdit::_update_obj);72ClassDB::bind_method(D_METHOD("_key_ofs_changed"), &AnimationTrackKeyEdit::_key_ofs_changed);73ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationTrackKeyEdit::_hide_script_from_inspector);74ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationTrackKeyEdit::_hide_metadata_from_inspector);75ClassDB::bind_method(D_METHOD("get_root_path"), &AnimationTrackKeyEdit::get_root_path);76ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationTrackKeyEdit::_dont_undo_redo);77ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationTrackKeyEdit::_is_read_only);78}7980void AnimationTrackKeyEdit::_fix_node_path(Variant &value) {81NodePath np = value;8283if (np == NodePath()) {84return;85}8687Node *root = EditorNode::get_singleton()->get_tree()->get_root();8889Node *np_node = root->get_node_or_null(np);90ERR_FAIL_NULL(np_node);9192Node *edited_node = root->get_node_or_null(base);93ERR_FAIL_NULL(edited_node);9495value = edited_node->get_path_to(np_node);96}9798void AnimationTrackKeyEdit::_update_obj(const Ref<Animation> &p_anim) {99if (setting || animation != p_anim) {100return;101}102103notify_change();104}105106void AnimationTrackKeyEdit::_key_ofs_changed(const Ref<Animation> &p_anim, float from, float to) {107if (animation != p_anim || from != key_ofs) {108return;109}110111key_ofs = to;112113if (setting) {114return;115}116117notify_change();118}119120bool AnimationTrackKeyEdit::_set(const StringName &p_name, const Variant &p_value) {121int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX);122ERR_FAIL_COND_V(key == -1, false);123124String name = p_name;125if (name == "easing") {126float val = p_value;127float prev_val = animation->track_get_key_transition(track, key);128setting = true;129130EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();131undo_redo->create_action(TTR("Animation Change Transition"), UndoRedo::MERGE_ENDS);132undo_redo->add_do_method(animation.ptr(), "track_set_key_transition", track, key, val);133undo_redo->add_undo_method(animation.ptr(), "track_set_key_transition", track, key, prev_val);134undo_redo->add_do_method(this, "_update_obj", animation);135undo_redo->add_undo_method(this, "_update_obj", animation);136AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();137if (ape) {138undo_redo->add_do_method(ape, "_animation_update_key_frame");139undo_redo->add_undo_method(ape, "_animation_update_key_frame");140}141undo_redo->commit_action();142143setting = false;144return true;145}146147EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();148switch (animation->track_get_type(track)) {149case Animation::TYPE_POSITION_3D:150case Animation::TYPE_ROTATION_3D:151case Animation::TYPE_SCALE_3D: {152if (name == "position" || name == "rotation" || name == "scale") {153Variant old = animation->track_get_key_value(track, key);154setting = true;155String action_name;156switch (animation->track_get_type(track)) {157case Animation::TYPE_POSITION_3D:158action_name = TTR("Animation Change Position3D");159break;160case Animation::TYPE_ROTATION_3D:161action_name = TTR("Animation Change Rotation3D");162break;163case Animation::TYPE_SCALE_3D:164action_name = TTR("Animation Change Scale3D");165break;166default: {167}168}169170undo_redo->create_action(action_name);171undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, p_value);172undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, old);173undo_redo->add_do_method(this, "_update_obj", animation);174undo_redo->add_undo_method(this, "_update_obj", animation);175undo_redo->commit_action();176177setting = false;178return true;179}180181} break;182case Animation::TYPE_BLEND_SHAPE:183case Animation::TYPE_VALUE: {184if (name == "value") {185Variant value = p_value;186187if (value.get_type() == Variant::NODE_PATH) {188_fix_node_path(value);189}190191setting = true;192193undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);194Variant prev = animation->track_get_key_value(track, key);195undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, value);196undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, prev);197undo_redo->add_do_method(this, "_update_obj", animation);198undo_redo->add_undo_method(this, "_update_obj", animation);199AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();200if (ape) {201undo_redo->add_do_method(ape, "_animation_update_key_frame");202undo_redo->add_undo_method(ape, "_animation_update_key_frame");203}204undo_redo->commit_action();205206setting = false;207return true;208}209} break;210case Animation::TYPE_METHOD: {211Dictionary d_old = animation->track_get_key_value(track, key);212Dictionary d_new = d_old.duplicate();213214bool change_notify_deserved = false;215bool mergeable = false;216217if (name == "name") {218d_new["method"] = p_value;219} else if (name == "arg_count") {220Vector<Variant> args = d_old["args"];221args.resize(p_value);222d_new["args"] = args;223change_notify_deserved = true;224} else if (name.begins_with("args/")) {225Vector<Variant> args = d_old["args"];226int idx = name.get_slicec('/', 1).to_int();227ERR_FAIL_INDEX_V(idx, args.size(), false);228229String what = name.get_slicec('/', 2);230if (what == "type") {231Variant::Type t = Variant::Type(int(p_value));232233if (t != args[idx].get_type()) {234Callable::CallError err;235if (Variant::can_convert_strict(args[idx].get_type(), t)) {236Variant old = args[idx];237Variant *ptrs[1] = { &old };238Variant::construct(t, args.write[idx], (const Variant **)ptrs, 1, err);239} else {240Variant::construct(t, args.write[idx], nullptr, 0, err);241}242change_notify_deserved = true;243d_new["args"] = args;244}245} else if (what == "value") {246Variant value = p_value;247if (value.get_type() == Variant::NODE_PATH) {248_fix_node_path(value);249}250251args.write[idx] = value;252d_new["args"] = args;253mergeable = true;254}255}256257if (mergeable) {258undo_redo->create_action(TTR("Animation Change Call"), UndoRedo::MERGE_ENDS);259} else {260undo_redo->create_action(TTR("Animation Change Call"));261}262263setting = true;264undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, d_new);265undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, d_old);266undo_redo->add_do_method(this, "_update_obj", animation);267undo_redo->add_undo_method(this, "_update_obj", animation);268undo_redo->commit_action();269270setting = false;271if (change_notify_deserved) {272notify_change();273}274return true;275} break;276case Animation::TYPE_BEZIER: {277if (name == "value") {278const Variant &value = p_value;279280setting = true;281undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);282float prev = animation->bezier_track_get_key_value(track, key);283undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_value", track, key, value);284undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_value", track, key, prev);285undo_redo->add_do_method(this, "_update_obj", animation);286undo_redo->add_undo_method(this, "_update_obj", animation);287AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();288if (ape) {289undo_redo->add_do_method(ape, "_animation_update_key_frame");290undo_redo->add_undo_method(ape, "_animation_update_key_frame");291}292undo_redo->commit_action();293294setting = false;295return true;296}297298if (name == "in_handle") {299const Variant &value = p_value;300301setting = true;302undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);303Vector2 prev = animation->bezier_track_get_key_in_handle(track, key);304undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, value);305undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, prev);306undo_redo->add_do_method(this, "_update_obj", animation);307undo_redo->add_undo_method(this, "_update_obj", animation);308AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();309if (ape) {310undo_redo->add_do_method(ape, "_animation_update_key_frame");311undo_redo->add_undo_method(ape, "_animation_update_key_frame");312}313undo_redo->commit_action();314315setting = false;316return true;317}318319if (name == "out_handle") {320const Variant &value = p_value;321322setting = true;323undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);324Vector2 prev = animation->bezier_track_get_key_out_handle(track, key);325undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, value);326undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, prev);327undo_redo->add_do_method(this, "_update_obj", animation);328undo_redo->add_undo_method(this, "_update_obj", animation);329AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();330if (ape) {331undo_redo->add_do_method(ape, "_animation_update_key_frame");332undo_redo->add_undo_method(ape, "_animation_update_key_frame");333}334undo_redo->commit_action();335336setting = false;337return true;338}339340if (name == "handle_mode") {341const Variant &value = p_value;342343setting = true;344undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS, animation.ptr());345int prev_mode = animation->bezier_track_get_key_handle_mode(track, key);346Vector2 prev_in_handle = animation->bezier_track_get_key_in_handle(track, key);347Vector2 prev_out_handle = animation->bezier_track_get_key_out_handle(track, key);348undo_redo->add_do_method(editor, "_bezier_track_set_key_handle_mode", animation.ptr(), track, key, value);349undo_redo->add_do_method(this, "_update_obj", animation);350undo_redo->add_undo_method(editor, "_bezier_track_set_key_handle_mode", animation.ptr(), track, key, prev_mode);351undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, prev_in_handle);352undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, prev_out_handle);353undo_redo->add_undo_method(this, "_update_obj", animation);354AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();355if (ape) {356undo_redo->add_do_method(ape, "_animation_update_key_frame");357undo_redo->add_undo_method(ape, "_animation_update_key_frame");358}359undo_redo->commit_action();360361setting = false;362return true;363}364} break;365case Animation::TYPE_AUDIO: {366if (name == "stream") {367Ref<AudioStream> stream = p_value;368369setting = true;370undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);371Ref<Resource> prev = animation->audio_track_get_key_stream(track, key);372undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_stream", track, key, stream);373undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_stream", track, key, prev);374undo_redo->add_do_method(this, "_update_obj", animation);375undo_redo->add_undo_method(this, "_update_obj", animation);376undo_redo->commit_action();377378setting = false;379notify_change(); // To update limits for `start_offset`/`end_offset` sliders (they depend on the stream length).380return true;381}382383if (name == "start_offset") {384float value = p_value;385386setting = true;387undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);388float prev = animation->audio_track_get_key_start_offset(track, key);389undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_start_offset", track, key, value);390undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_start_offset", track, key, prev);391undo_redo->add_do_method(this, "_update_obj", animation);392undo_redo->add_undo_method(this, "_update_obj", animation);393undo_redo->commit_action();394395setting = false;396return true;397}398399if (name == "end_offset") {400float value = p_value;401402setting = true;403undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);404float prev = animation->audio_track_get_key_end_offset(track, key);405undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_end_offset", track, key, value);406undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_end_offset", track, key, prev);407undo_redo->add_do_method(this, "_update_obj", animation);408undo_redo->add_undo_method(this, "_update_obj", animation);409undo_redo->commit_action();410411setting = false;412return true;413}414} break;415case Animation::TYPE_ANIMATION: {416if (name == "animation") {417StringName anim_name = p_value;418419setting = true;420undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);421StringName prev = animation->animation_track_get_key_animation(track, key);422undo_redo->add_do_method(animation.ptr(), "animation_track_set_key_animation", track, key, anim_name);423undo_redo->add_undo_method(animation.ptr(), "animation_track_set_key_animation", track, key, prev);424undo_redo->add_do_method(this, "_update_obj", animation);425undo_redo->add_undo_method(this, "_update_obj", animation);426undo_redo->commit_action();427428setting = false;429return true;430}431} break;432}433434return false;435}436437bool AnimationTrackKeyEdit::_get(const StringName &p_name, Variant &r_ret) const {438int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX);439ERR_FAIL_COND_V(key == -1, false);440441String name = p_name;442if (name == "easing") {443r_ret = animation->track_get_key_transition(track, key);444return true;445}446447switch (animation->track_get_type(track)) {448case Animation::TYPE_POSITION_3D:449case Animation::TYPE_ROTATION_3D:450case Animation::TYPE_SCALE_3D: {451if (name == "position" || name == "rotation" || name == "scale") {452r_ret = animation->track_get_key_value(track, key);453return true;454}455} break;456case Animation::TYPE_BLEND_SHAPE:457case Animation::TYPE_VALUE: {458if (name == "value") {459r_ret = animation->track_get_key_value(track, key);460return true;461}462463} break;464case Animation::TYPE_METHOD: {465Dictionary d = animation->track_get_key_value(track, key);466467if (name == "name") {468ERR_FAIL_COND_V(!d.has("method"), false);469r_ret = d["method"];470return true;471}472473ERR_FAIL_COND_V(!d.has("args"), false);474475Vector<Variant> args = d["args"];476477if (name == "arg_count") {478r_ret = args.size();479return true;480}481482if (name.begins_with("args/")) {483int idx = name.get_slicec('/', 1).to_int();484ERR_FAIL_INDEX_V(idx, args.size(), false);485486String what = name.get_slicec('/', 2);487if (what == "type") {488r_ret = args[idx].get_type();489return true;490}491492if (what == "value") {493r_ret = args[idx];494return true;495}496}497498} break;499case Animation::TYPE_BEZIER: {500if (name == "value") {501r_ret = animation->bezier_track_get_key_value(track, key);502return true;503}504505if (name == "in_handle") {506r_ret = animation->bezier_track_get_key_in_handle(track, key);507return true;508}509510if (name == "out_handle") {511r_ret = animation->bezier_track_get_key_out_handle(track, key);512return true;513}514515if (name == "handle_mode") {516r_ret = animation->bezier_track_get_key_handle_mode(track, key);517return true;518}519520} break;521case Animation::TYPE_AUDIO: {522if (name == "stream") {523r_ret = animation->audio_track_get_key_stream(track, key);524return true;525}526527if (name == "start_offset") {528r_ret = animation->audio_track_get_key_start_offset(track, key);529return true;530}531532if (name == "end_offset") {533r_ret = animation->audio_track_get_key_end_offset(track, key);534return true;535}536537} break;538case Animation::TYPE_ANIMATION: {539if (name == "animation") {540r_ret = animation->animation_track_get_key_animation(track, key);541return true;542}543544} break;545}546547return false;548}549550void AnimationTrackKeyEdit::_get_property_list(List<PropertyInfo> *p_list) const {551if (animation.is_null()) {552return;553}554555ERR_FAIL_INDEX(track, animation->get_track_count());556int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX);557ERR_FAIL_COND(key == -1);558559switch (animation->track_get_type(track)) {560case Animation::TYPE_POSITION_3D: {561p_list->push_back(PropertyInfo(Variant::VECTOR3, PNAME("position")));562} break;563case Animation::TYPE_ROTATION_3D: {564p_list->push_back(PropertyInfo(Variant::QUATERNION, PNAME("rotation")));565} break;566case Animation::TYPE_SCALE_3D: {567p_list->push_back(PropertyInfo(Variant::VECTOR3, PNAME("scale")));568} break;569case Animation::TYPE_BLEND_SHAPE: {570p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("value")));571} break;572case Animation::TYPE_VALUE: {573Variant v = animation->track_get_key_value(track, key);574575if (hint.type != Variant::NIL) {576PropertyInfo pi = hint;577pi.name = PNAME("value");578p_list->push_back(pi);579} else {580PropertyHint val_hint = PROPERTY_HINT_NONE;581String val_hint_string;582583if (v.get_type() == Variant::OBJECT) {584// Could actually check the object property if exists..? Yes I will!585Ref<Resource> res = v;586if (res.is_valid()) {587val_hint = PROPERTY_HINT_RESOURCE_TYPE;588val_hint_string = res->get_class();589}590}591592if (v.get_type() != Variant::NIL) {593p_list->push_back(PropertyInfo(v.get_type(), PNAME("value"), val_hint, val_hint_string));594}595}596597} break;598case Animation::TYPE_METHOD: {599p_list->push_back(PropertyInfo(Variant::STRING_NAME, PNAME("name")));600p_list->push_back(PropertyInfo(Variant::INT, PNAME("arg_count"), PROPERTY_HINT_RANGE, "0,32,1,or_greater"));601602Dictionary d = animation->track_get_key_value(track, key);603ERR_FAIL_COND(!d.has("args"));604Vector<Variant> args = d["args"];605String vtypes;606for (int i = 0; i < Variant::VARIANT_MAX; i++) {607if (i > 0) {608vtypes += ",";609}610vtypes += Variant::get_type_name(Variant::Type(i));611}612613for (int i = 0; i < args.size(); i++) {614p_list->push_back(PropertyInfo(Variant::INT, vformat("%s/%d/%s", PNAME("args"), i, PNAME("type")), PROPERTY_HINT_ENUM, vtypes));615if (args[i].get_type() != Variant::NIL) {616p_list->push_back(PropertyInfo(args[i].get_type(), vformat("%s/%d/%s", PNAME("args"), i, PNAME("value"))));617}618}619620} break;621case Animation::TYPE_BEZIER: {622Animation::HandleMode hm = animation->bezier_track_get_key_handle_mode(track, key);623p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("value")));624if (hm == Animation::HANDLE_MODE_LINEAR) {625p_list->push_back(PropertyInfo(Variant::VECTOR2, PNAME("in_handle"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY));626p_list->push_back(PropertyInfo(Variant::VECTOR2, PNAME("out_handle"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY));627} else {628p_list->push_back(PropertyInfo(Variant::VECTOR2, PNAME("in_handle")));629p_list->push_back(PropertyInfo(Variant::VECTOR2, PNAME("out_handle")));630}631p_list->push_back(PropertyInfo(Variant::INT, PNAME("handle_mode"), PROPERTY_HINT_ENUM, "Free,Linear,Balanced,Mirrored"));632633} break;634case Animation::TYPE_AUDIO: {635p_list->push_back(PropertyInfo(Variant::OBJECT, PNAME("stream"), PROPERTY_HINT_RESOURCE_TYPE, AudioStream::get_class_static()));636Ref<AudioStream> audio_stream = animation->audio_track_get_key_stream(track, key);637String hint_string = vformat("0,%.4f,0.0001,or_greater", audio_stream.is_valid() ? audio_stream->get_length() : 3600.0);638p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("start_offset"), PROPERTY_HINT_RANGE, hint_string));639p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("end_offset"), PROPERTY_HINT_RANGE, hint_string));640641} break;642case Animation::TYPE_ANIMATION: {643String animations;644645if (root_path) {646AnimationPlayer *ap = Object::cast_to<AnimationPlayer>(root_path->get_node_or_null(animation->track_get_path(track)));647if (ap) {648List<StringName> anims;649ap->get_animation_list(&anims);650for (const StringName &E : anims) {651if (!animations.is_empty()) {652animations += ",";653}654655animations += String(E);656}657}658}659660if (!animations.is_empty()) {661animations += ",";662}663animations += "[stop]";664665p_list->push_back(PropertyInfo(Variant::STRING_NAME, PNAME("animation"), PROPERTY_HINT_ENUM, animations));666667} break;668}669670if (animation->track_get_type(track) == Animation::TYPE_VALUE) {671p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("easing"), PROPERTY_HINT_EXP_EASING));672}673}674675void AnimationTrackKeyEdit::notify_change() {676notify_property_list_changed();677}678679Node *AnimationTrackKeyEdit::get_root_path() {680return root_path;681}682683void AnimationTrackKeyEdit::set_use_fps(bool p_enable) {684use_fps = p_enable;685notify_property_list_changed();686}687688void AnimationMultiTrackKeyEdit::_bind_methods() {689ClassDB::bind_method(D_METHOD("_update_obj"), &AnimationMultiTrackKeyEdit::_update_obj);690ClassDB::bind_method(D_METHOD("_key_ofs_changed"), &AnimationMultiTrackKeyEdit::_key_ofs_changed);691ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMultiTrackKeyEdit::_hide_script_from_inspector);692ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMultiTrackKeyEdit::_hide_metadata_from_inspector);693ClassDB::bind_method(D_METHOD("get_root_path"), &AnimationMultiTrackKeyEdit::get_root_path);694ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMultiTrackKeyEdit::_dont_undo_redo);695ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMultiTrackKeyEdit::_is_read_only);696}697698void AnimationMultiTrackKeyEdit::_fix_node_path(Variant &value, NodePath &base) {699NodePath np = value;700701if (np == NodePath()) {702return;703}704705Node *root = EditorNode::get_singleton()->get_tree()->get_root();706707Node *np_node = root->get_node_or_null(np);708ERR_FAIL_NULL(np_node);709710Node *edited_node = root->get_node_or_null(base);711ERR_FAIL_NULL(edited_node);712713value = edited_node->get_path_to(np_node);714}715716void AnimationMultiTrackKeyEdit::_update_obj(const Ref<Animation> &p_anim) {717if (setting || animation != p_anim) {718return;719}720721notify_change();722}723724void AnimationMultiTrackKeyEdit::_key_ofs_changed(const Ref<Animation> &p_anim, float from, float to) {725if (animation != p_anim) {726return;727}728729for (const KeyValue<int, List<float>> &E : key_ofs_map) {730int key = 0;731for (const float &key_ofs : E.value) {732if (from != key_ofs) {733key++;734continue;735}736737int track = E.key;738key_ofs_map[track].get(key) = to;739740if (setting) {741return;742}743744notify_change();745746return;747}748}749}750751bool AnimationMultiTrackKeyEdit::_set(const StringName &p_name, const Variant &p_value) {752bool update_obj = false;753bool change_notify_deserved = false;754for (const KeyValue<int, List<float>> &E : key_ofs_map) {755int track = E.key;756for (const float &key_ofs : E.value) {757int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX);758ERR_FAIL_COND_V(key == -1, false);759760String name = p_name;761if (name == "easing") {762float val = p_value;763float prev_val = animation->track_get_key_transition(track, key);764765EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();766if (!setting) {767setting = true;768undo_redo->create_action(TTR("Animation Multi Change Transition"), UndoRedo::MERGE_ENDS);769}770undo_redo->add_do_method(animation.ptr(), "track_set_key_transition", track, key, val);771undo_redo->add_undo_method(animation.ptr(), "track_set_key_transition", track, key, prev_val);772update_obj = true;773}774775EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();776switch (animation->track_get_type(track)) {777case Animation::TYPE_POSITION_3D:778case Animation::TYPE_ROTATION_3D:779case Animation::TYPE_SCALE_3D: {780Variant old = animation->track_get_key_value(track, key);781if (!setting) {782String action_name;783switch (animation->track_get_type(track)) {784case Animation::TYPE_POSITION_3D:785action_name = TTR("Animation Multi Change Position3D");786break;787case Animation::TYPE_ROTATION_3D:788action_name = TTR("Animation Multi Change Rotation3D");789break;790case Animation::TYPE_SCALE_3D:791action_name = TTR("Animation Multi Change Scale3D");792break;793default: {794}795}796797setting = true;798undo_redo->create_action(action_name);799}800undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, p_value);801undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, old);802update_obj = true;803} break;804case Animation::TYPE_BLEND_SHAPE:805case Animation::TYPE_VALUE: {806if (name == "value") {807Variant value = p_value;808809if (value.get_type() == Variant::NODE_PATH) {810_fix_node_path(value, base_map[track]);811}812813if (!setting) {814setting = true;815undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);816}817Variant prev = animation->track_get_key_value(track, key);818undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, value);819undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, prev);820update_obj = true;821}822} break;823case Animation::TYPE_METHOD: {824Dictionary d_old = animation->track_get_key_value(track, key);825Dictionary d_new = d_old.duplicate();826827bool mergeable = false;828829if (name == "name") {830d_new["method"] = p_value;831} else if (name == "arg_count") {832Vector<Variant> args = d_old["args"];833args.resize(p_value);834d_new["args"] = args;835change_notify_deserved = true;836} else if (name.begins_with("args/")) {837Vector<Variant> args = d_old["args"];838int idx = name.get_slicec('/', 1).to_int();839ERR_FAIL_INDEX_V(idx, args.size(), false);840841String what = name.get_slicec('/', 2);842if (what == "type") {843Variant::Type t = Variant::Type(int(p_value));844845if (t != args[idx].get_type()) {846Callable::CallError err;847if (Variant::can_convert_strict(args[idx].get_type(), t)) {848Variant old = args[idx];849Variant *ptrs[1] = { &old };850Variant::construct(t, args.write[idx], (const Variant **)ptrs, 1, err);851} else {852Variant::construct(t, args.write[idx], nullptr, 0, err);853}854change_notify_deserved = true;855d_new["args"] = args;856}857} else if (what == "value") {858Variant value = p_value;859if (value.get_type() == Variant::NODE_PATH) {860_fix_node_path(value, base_map[track]);861}862863args.write[idx] = value;864d_new["args"] = args;865mergeable = true;866}867}868869Variant prev = animation->track_get_key_value(track, key);870871if (!setting) {872if (mergeable) {873undo_redo->create_action(TTR("Animation Multi Change Call"), UndoRedo::MERGE_ENDS);874} else {875undo_redo->create_action(TTR("Animation Multi Change Call"));876}877878setting = true;879}880881undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, d_new);882undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, d_old);883update_obj = true;884} break;885case Animation::TYPE_BEZIER: {886if (name == "value") {887const Variant &value = p_value;888889if (!setting) {890setting = true;891undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);892}893float prev = animation->bezier_track_get_key_value(track, key);894undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_value", track, key, value);895undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_value", track, key, prev);896update_obj = true;897} else if (name == "in_handle") {898const Variant &value = p_value;899900if (!setting) {901setting = true;902undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);903}904Vector2 prev = animation->bezier_track_get_key_in_handle(track, key);905undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, value);906undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, prev);907update_obj = true;908} else if (name == "out_handle") {909const Variant &value = p_value;910911if (!setting) {912setting = true;913undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);914}915Vector2 prev = animation->bezier_track_get_key_out_handle(track, key);916undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, value);917undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, prev);918update_obj = true;919} else if (name == "handle_mode") {920const Variant &value = p_value;921922if (!setting) {923setting = true;924undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS, animation.ptr());925}926int prev_mode = animation->bezier_track_get_key_handle_mode(track, key);927Vector2 prev_in_handle = animation->bezier_track_get_key_in_handle(track, key);928Vector2 prev_out_handle = animation->bezier_track_get_key_out_handle(track, key);929undo_redo->add_do_method(editor, "_bezier_track_set_key_handle_mode", animation.ptr(), track, key, value);930undo_redo->add_undo_method(editor, "_bezier_track_set_key_handle_mode", animation.ptr(), track, key, prev_mode);931undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, prev_in_handle);932undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, prev_out_handle);933update_obj = true;934}935} break;936case Animation::TYPE_AUDIO: {937if (name == "stream") {938Ref<AudioStream> stream = p_value;939940if (!setting) {941setting = true;942undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);943}944Ref<Resource> prev = animation->audio_track_get_key_stream(track, key);945undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_stream", track, key, stream);946undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_stream", track, key, prev);947update_obj = true;948} else if (name == "start_offset") {949float value = p_value;950951if (!setting) {952setting = true;953undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);954}955float prev = animation->audio_track_get_key_start_offset(track, key);956undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_start_offset", track, key, value);957undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_start_offset", track, key, prev);958update_obj = true;959} else if (name == "end_offset") {960float value = p_value;961962if (!setting) {963setting = true;964undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);965}966float prev = animation->audio_track_get_key_end_offset(track, key);967undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_end_offset", track, key, value);968undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_end_offset", track, key, prev);969update_obj = true;970}971} break;972case Animation::TYPE_ANIMATION: {973if (name == "animation") {974StringName anim_name = p_value;975976if (!setting) {977setting = true;978undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);979}980StringName prev = animation->animation_track_get_key_animation(track, key);981undo_redo->add_do_method(animation.ptr(), "animation_track_set_key_animation", track, key, anim_name);982undo_redo->add_undo_method(animation.ptr(), "animation_track_set_key_animation", track, key, prev);983update_obj = true;984}985} break;986}987}988}989990EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();991if (setting) {992if (update_obj) {993undo_redo->add_do_method(this, "_update_obj", animation);994undo_redo->add_undo_method(this, "_update_obj", animation);995}996997undo_redo->commit_action();998setting = false;9991000if (change_notify_deserved) {1001notify_change();1002}10031004return true;1005}10061007return false;1008}10091010bool AnimationMultiTrackKeyEdit::_get(const StringName &p_name, Variant &r_ret) const {1011for (const KeyValue<int, List<float>> &E : key_ofs_map) {1012int track = E.key;1013for (const float &key_ofs : E.value) {1014int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX);1015ERR_CONTINUE(key == -1);10161017String name = p_name;1018if (name == "easing") {1019r_ret = animation->track_get_key_transition(track, key);1020return true;1021}10221023switch (animation->track_get_type(track)) {1024case Animation::TYPE_POSITION_3D:1025case Animation::TYPE_ROTATION_3D:1026case Animation::TYPE_SCALE_3D: {1027if (name == "position" || name == "rotation" || name == "scale") {1028r_ret = animation->track_get_key_value(track, key);1029return true;1030}10311032} break;1033case Animation::TYPE_BLEND_SHAPE:1034case Animation::TYPE_VALUE: {1035if (name == "value") {1036r_ret = animation->track_get_key_value(track, key);1037return true;1038}10391040} break;1041case Animation::TYPE_METHOD: {1042Dictionary d = animation->track_get_key_value(track, key);10431044if (name == "name") {1045ERR_FAIL_COND_V(!d.has("method"), false);1046r_ret = d["method"];1047return true;1048}10491050ERR_FAIL_COND_V(!d.has("args"), false);10511052Vector<Variant> args = d["args"];10531054if (name == "arg_count") {1055r_ret = args.size();1056return true;1057}10581059if (name.begins_with("args/")) {1060int idx = name.get_slicec('/', 1).to_int();1061ERR_FAIL_INDEX_V(idx, args.size(), false);10621063String what = name.get_slicec('/', 2);1064if (what == "type") {1065r_ret = args[idx].get_type();1066return true;1067}10681069if (what == "value") {1070r_ret = args[idx];1071return true;1072}1073}10741075} break;1076case Animation::TYPE_BEZIER: {1077if (name == "value") {1078r_ret = animation->bezier_track_get_key_value(track, key);1079return true;1080}10811082if (name == "in_handle") {1083r_ret = animation->bezier_track_get_key_in_handle(track, key);1084return true;1085}10861087if (name == "out_handle") {1088r_ret = animation->bezier_track_get_key_out_handle(track, key);1089return true;1090}10911092if (name == "handle_mode") {1093r_ret = animation->bezier_track_get_key_handle_mode(track, key);1094return true;1095}10961097} break;1098case Animation::TYPE_AUDIO: {1099if (name == "stream") {1100r_ret = animation->audio_track_get_key_stream(track, key);1101return true;1102}11031104if (name == "start_offset") {1105r_ret = animation->audio_track_get_key_start_offset(track, key);1106return true;1107}11081109if (name == "end_offset") {1110r_ret = animation->audio_track_get_key_end_offset(track, key);1111return true;1112}11131114} break;1115case Animation::TYPE_ANIMATION: {1116if (name == "animation") {1117r_ret = animation->animation_track_get_key_animation(track, key);1118return true;1119}11201121} break;1122}1123}1124}11251126return false;1127}11281129void AnimationMultiTrackKeyEdit::_get_property_list(List<PropertyInfo> *p_list) const {1130if (animation.is_null()) {1131return;1132}11331134int first_track = -1;1135float first_key = -1.0;11361137bool same_track_type = true;1138bool same_key_type = true;1139for (const KeyValue<int, List<float>> &E : key_ofs_map) {1140int track = E.key;1141ERR_FAIL_INDEX(track, animation->get_track_count());11421143if (first_track < 0) {1144first_track = track;1145}11461147if (same_track_type) {1148if (animation->track_get_type(first_track) != animation->track_get_type(track)) {1149same_track_type = false;1150same_key_type = false;1151}11521153for (const float &F : E.value) {1154int key = animation->track_find_key(track, F, Animation::FIND_MODE_APPROX);1155ERR_FAIL_COND(key == -1);1156if (first_key < 0) {1157first_key = key;1158}11591160if (animation->track_get_key_value(first_track, first_key).get_type() != animation->track_get_key_value(track, key).get_type()) {1161same_key_type = false;1162}1163}1164}1165}11661167if (same_track_type) {1168switch (animation->track_get_type(first_track)) {1169case Animation::TYPE_POSITION_3D: {1170p_list->push_back(PropertyInfo(Variant::VECTOR3, "position"));1171} break;1172case Animation::TYPE_ROTATION_3D: {1173p_list->push_back(PropertyInfo(Variant::QUATERNION, "rotation"));1174} break;1175case Animation::TYPE_SCALE_3D: {1176p_list->push_back(PropertyInfo(Variant::VECTOR3, "scale"));1177} break;1178case Animation::TYPE_BLEND_SHAPE: {1179p_list->push_back(PropertyInfo(Variant::FLOAT, "value"));1180} break;1181case Animation::TYPE_VALUE: {1182if (same_key_type) {1183Variant v = animation->track_get_key_value(first_track, first_key);11841185if (hint.type != Variant::NIL) {1186PropertyInfo pi = hint;1187pi.name = "value";1188p_list->push_back(pi);1189} else {1190PropertyHint val_hint = PROPERTY_HINT_NONE;1191String val_hint_string;11921193if (v.get_type() == Variant::OBJECT) {1194// Could actually check the object property if exists..? Yes I will!1195Ref<Resource> res = v;1196if (res.is_valid()) {1197val_hint = PROPERTY_HINT_RESOURCE_TYPE;1198val_hint_string = res->get_class();1199}1200}12011202if (v.get_type() != Variant::NIL) {1203p_list->push_back(PropertyInfo(v.get_type(), "value", val_hint, val_hint_string));1204}1205}1206}12071208p_list->push_back(PropertyInfo(Variant::FLOAT, "easing", PROPERTY_HINT_EXP_EASING));1209} break;1210case Animation::TYPE_METHOD: {1211p_list->push_back(PropertyInfo(Variant::STRING_NAME, "name"));12121213p_list->push_back(PropertyInfo(Variant::INT, "arg_count", PROPERTY_HINT_RANGE, "0,32,1,or_greater"));12141215Dictionary d = animation->track_get_key_value(first_track, first_key);1216ERR_FAIL_COND(!d.has("args"));1217Vector<Variant> args = d["args"];1218String vtypes;1219for (int i = 0; i < Variant::VARIANT_MAX; i++) {1220if (i > 0) {1221vtypes += ",";1222}1223vtypes += Variant::get_type_name(Variant::Type(i));1224}12251226for (int i = 0; i < args.size(); i++) {1227p_list->push_back(PropertyInfo(Variant::INT, "args/" + itos(i) + "/type", PROPERTY_HINT_ENUM, vtypes));1228if (args[i].get_type() != Variant::NIL) {1229p_list->push_back(PropertyInfo(args[i].get_type(), "args/" + itos(i) + "/value"));1230}1231}1232} break;1233case Animation::TYPE_BEZIER: {1234p_list->push_back(PropertyInfo(Variant::FLOAT, "value"));1235p_list->push_back(PropertyInfo(Variant::VECTOR2, "in_handle"));1236p_list->push_back(PropertyInfo(Variant::VECTOR2, "out_handle"));1237p_list->push_back(PropertyInfo(Variant::INT, "handle_mode", PROPERTY_HINT_ENUM, "Free,Linear,Balanced,Mirrored"));1238} break;1239case Animation::TYPE_AUDIO: {1240p_list->push_back(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, AudioStream::get_class_static()));1241p_list->push_back(PropertyInfo(Variant::FLOAT, "start_offset", PROPERTY_HINT_RANGE, "0,3600,0.0001,or_greater"));1242p_list->push_back(PropertyInfo(Variant::FLOAT, "end_offset", PROPERTY_HINT_RANGE, "0,3600,0.0001,or_greater"));1243} break;1244case Animation::TYPE_ANIMATION: {1245if (key_ofs_map.size() > 1) {1246break;1247}12481249String animations;12501251if (root_path) {1252AnimationPlayer *ap = Object::cast_to<AnimationPlayer>(root_path->get_node_or_null(animation->track_get_path(first_track)));1253if (ap) {1254List<StringName> anims;1255ap->get_animation_list(&anims);1256for (const StringName &anim : anims) {1257if (!animations.is_empty()) {1258animations += ",";1259}12601261animations += String(anim);1262}1263}1264}12651266if (!animations.is_empty()) {1267animations += ",";1268}1269animations += "[stop]";12701271p_list->push_back(PropertyInfo(Variant::STRING_NAME, "animation", PROPERTY_HINT_ENUM, animations));1272} break;1273}1274}1275}12761277void AnimationMultiTrackKeyEdit::notify_change() {1278notify_property_list_changed();1279}12801281Node *AnimationMultiTrackKeyEdit::get_root_path() {1282return root_path;1283}12841285void AnimationMultiTrackKeyEdit::set_use_fps(bool p_enable) {1286use_fps = p_enable;1287notify_property_list_changed();1288}12891290void AnimationTimelineEdit::_zoom_changed(double) {1291double zoom_pivot = 0; // Point on timeline to stay fixed.1292double zoom_pivot_delta = 0; // Delta seconds from left-most point on timeline to zoom pivot.12931294int timeline_width_pixels = get_size().width - get_buttons_width() - get_name_limit();1295double timeline_width_seconds = timeline_width_pixels / last_zoom_scale; // Length (in seconds) of visible part of timeline before zoom.1296double updated_timeline_width_seconds = timeline_width_pixels / get_zoom_scale(); // Length after zoom.1297double updated_timeline_half_width = updated_timeline_width_seconds / 2.0;1298bool zooming = updated_timeline_width_seconds < timeline_width_seconds;12991300double timeline_left = get_value();1301double timeline_right = timeline_left + timeline_width_seconds;1302double timeline_center = timeline_left + timeline_width_seconds / 2.0;13031304if (zoom_callback_occurred) { // Zooming with scroll wheel will focus on the position of the mouse.1305double zoom_scroll_origin_norm = (zoom_scroll_origin.x - get_name_limit()) / timeline_width_pixels;1306zoom_scroll_origin_norm = MAX(zoom_scroll_origin_norm, 0);1307zoom_pivot = timeline_left + timeline_width_seconds * zoom_scroll_origin_norm;1308zoom_pivot_delta = updated_timeline_width_seconds * zoom_scroll_origin_norm;1309zoom_callback_occurred = false;1310} else { // Zooming with slider will depend on the current play position.1311// If the play position is not in range, or exactly in the center, zoom in on the center.1312if (get_play_position() < timeline_left || get_play_position() > timeline_left + timeline_width_seconds || get_play_position() == timeline_center) {1313zoom_pivot = timeline_center;1314zoom_pivot_delta = updated_timeline_half_width;1315}1316// Zoom from right if play position is right of center,1317// and shrink from right if play position is left of center.1318else if ((get_play_position() > timeline_center) == zooming) {1319// If play position crosses to other side of center, center it.1320bool center_passed = (get_play_position() < timeline_right - updated_timeline_half_width) == zooming;1321zoom_pivot = center_passed ? get_play_position() : timeline_right;1322double center_offset = CMP_EPSILON * (zooming ? 1 : -1); // Small offset to prevent crossover.1323zoom_pivot_delta = center_passed ? updated_timeline_half_width + center_offset : updated_timeline_width_seconds;1324}1325// Zoom from left if play position is left of center,1326// and shrink from left if play position is right of center.1327else if ((get_play_position() <= timeline_center) == zooming) {1328// If play position crosses to other side of center, center it.1329bool center_passed = (get_play_position() > timeline_left + updated_timeline_half_width) == zooming;1330zoom_pivot = center_passed ? get_play_position() : timeline_left;1331double center_offset = CMP_EPSILON * (zooming ? -1 : 1); // Small offset to prevent crossover.1332zoom_pivot_delta = center_passed ? updated_timeline_half_width + center_offset : 0;1333}1334}13351336double hscroll_pos = zoom_pivot - zoom_pivot_delta;1337hscroll_pos = CLAMP(hscroll_pos, hscroll->get_min(), hscroll->get_max());13381339hscroll->set_value(hscroll_pos);1340hscroll_on_zoom_buffer = hscroll_pos; // In case of page update.1341last_zoom_scale = get_zoom_scale();13421343queue_redraw();1344play_position->queue_redraw();1345emit_signal(SNAME("zoom_changed"));1346}13471348float AnimationTimelineEdit::get_zoom_scale() const {1349return _get_zoom_scale(zoom->get_value());1350}13511352float AnimationTimelineEdit::_get_zoom_scale(double p_zoom_value) const {1353float zv = zoom->get_max() - p_zoom_value;1354if (zv < 1) {1355zv = 1.0 - zv;1356return Math::pow(1.0f + zv, 8.0f) * 100;1357} else {1358return 1.0 / Math::pow(zv, 8.0f) * 100;1359}1360}13611362void AnimationTimelineEdit::_anim_length_changed(double p_new_len) {1363if (editing) {1364return;1365}13661367p_new_len = MAX(SECOND_DECIMAL, p_new_len);1368if (use_fps && animation->get_step() > 0) {1369p_new_len *= animation->get_step();1370}13711372editing = true;1373EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();1374undo_redo->create_action(TTR("Change Animation Length"), UndoRedo::MERGE_ENDS);1375undo_redo->add_do_method(animation.ptr(), "set_length", p_new_len);1376undo_redo->add_undo_method(animation.ptr(), "set_length", animation->get_length());1377undo_redo->commit_action();1378editing = false;1379queue_redraw();13801381emit_signal(SNAME("length_changed"), p_new_len);1382}13831384void AnimationTimelineEdit::_anim_loop_pressed() {1385if (!read_only) {1386EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();1387undo_redo->create_action(TTR("Change Animation Loop"));1388switch (animation->get_loop_mode()) {1389case Animation::LOOP_NONE: {1390undo_redo->add_do_method(animation.ptr(), "set_loop_mode", Animation::LOOP_LINEAR);1391} break;1392case Animation::LOOP_LINEAR: {1393undo_redo->add_do_method(animation.ptr(), "set_loop_mode", Animation::LOOP_PINGPONG);1394} break;1395case Animation::LOOP_PINGPONG: {1396undo_redo->add_do_method(animation.ptr(), "set_loop_mode", Animation::LOOP_NONE);1397} break;1398default:1399break;1400}1401undo_redo->add_do_method(this, "update_values");1402undo_redo->add_undo_method(animation.ptr(), "set_loop_mode", animation->get_loop_mode());1403undo_redo->add_undo_method(this, "update_values");1404undo_redo->commit_action();1405} else {1406String base = animation->get_path();1407int srpos = base.find("::");1408if (srpos != -1) {1409base = animation->get_path().substr(0, srpos);1410}14111412if (FileAccess::exists(base + ".import")) {1413if (ResourceLoader::get_resource_type(base) == "PackedScene") {1414EditorNode::get_singleton()->show_warning(TTR("Can't change loop mode on animation instanced from an imported scene.\n\nTo change this animation's loop mode, navigate to the scene's Advanced Import settings and select the animation.\nYou can then change the loop mode from the inspector menu."));1415} else {1416EditorNode::get_singleton()->show_warning(TTR("Can't change loop mode on animation instanced from an imported resource."));1417}1418} else {1419if (ResourceLoader::get_resource_type(base) == "PackedScene") {1420EditorNode::get_singleton()->show_warning(TTR("Can't change loop mode on animation embedded in another scene.\n\nYou must open this scene and change the animation's loop mode from there."));1421} else {1422EditorNode::get_singleton()->show_warning(TTR("Can't change loop mode on animation embedded in another resource."));1423}1424}14251426update_values();1427}1428}14291430int AnimationTimelineEdit::get_buttons_width() const {1431const Ref<Texture2D> interp_mode = get_editor_theme_icon(SNAME("TrackContinuous"));1432const Ref<Texture2D> interp_type = get_editor_theme_icon(SNAME("InterpRaw"));1433const Ref<Texture2D> loop_type = get_editor_theme_icon(SNAME("InterpWrapClamp"));1434const Ref<Texture2D> remove_icon = get_editor_theme_icon(SNAME("Remove"));1435const Ref<Texture2D> down_icon = get_theme_icon(SNAME("select_arrow"), SNAME("Tree"));14361437const int h_separation = get_theme_constant(SNAME("h_separation"), SNAME("AnimationTrackEdit"));1438const int outer_margin = get_theme_constant(SNAME("outer_margin"), SNAME("AnimationTrackEdit"));14391440int total_w = interp_mode->get_width() + interp_type->get_width() + loop_type->get_width() + remove_icon->get_width() + outer_margin;1441total_w += (down_icon->get_width() + h_separation) * 4;14421443return total_w;1444}14451446int AnimationTimelineEdit::get_name_limit() const {1447Ref<Texture2D> hsize_icon = get_editor_theme_icon(SNAME("Hsize"));14481449int filter_track_width = filter_track->is_visible() ? filter_track->get_custom_minimum_size().width : 0;1450int limit = MAX(name_limit, add_track->get_minimum_size().width + hsize_icon->get_width() + filter_track_width + 16 * EDSCALE);14511452limit = MIN(limit, get_size().width - get_buttons_width() - 1);14531454return limit;1455}14561457void AnimationTimelineEdit::_notification(int p_what) {1458switch (p_what) {1459case NOTIFICATION_THEME_CHANGED: {1460add_track->set_button_icon(get_editor_theme_icon(SNAME("Add")));1461loop->set_button_icon(get_editor_theme_icon(SNAME("Loop")));1462time_icon->set_texture(get_editor_theme_icon(SNAME("Time")));1463filter_track->set_right_icon(get_editor_theme_icon(SNAME("Search")));14641465add_track->get_popup()->clear();1466add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyValue")), TTR("Property Track..."));1467add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyXPosition")), TTR("3D Position Track..."));1468add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyXRotation")), TTR("3D Rotation Track..."));1469add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyXScale")), TTR("3D Scale Track..."));1470add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyBlendShape")), TTR("Blend Shape Track..."));1471add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyCall")), TTR("Call Method Track..."));1472add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyBezier")), TTR("Bezier Curve Track..."));1473add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyAudio")), TTR("Audio Playback Track..."));1474add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyAnimation")), TTR("Animation Playback Track..."));14751476timeline_resize_rect.size = get_editor_theme_icon(SNAME("TimelineHandle"))->get_size();1477} break;14781479case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {1480if (!EditorSettings::get_singleton()->check_changed_settings_in_group("editors/panning")) {1481break;1482}1483[[fallthrough]];1484}1485case NOTIFICATION_READY: {1486panner->setup((ViewPanner::ControlScheme)EDITOR_GET("editors/panning/animation_editors_panning_scheme").operator int(), ED_GET_SHORTCUT("canvas_item_editor/pan_view"), bool(EDITOR_GET("editors/panning/simple_panning")));1487panner->setup_warped_panning(get_viewport(), EDITOR_GET("editors/panning/warped_mouse_panning"));1488} break;14891490case NOTIFICATION_RESIZED: {1491len_hb->set_position(Vector2(get_size().width - get_buttons_width(), 0));1492len_hb->set_size(Size2(get_buttons_width(), get_size().height));1493int hsize_icon_width = get_editor_theme_icon(SNAME("Hsize"))->get_width();1494add_track_hb->set_size(Size2(name_limit - ((hsize_icon_width + 16) * EDSCALE), 0));1495} break;14961497case NOTIFICATION_DRAW: {1498if (animation.is_null()) {1499return;1500}15011502const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));1503const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));15041505const Ref<StyleBox> &stylebox_time_unavailable = get_theme_stylebox(SNAME("time_unavailable"), SNAME("AnimationTimelineEdit"));1506const Ref<StyleBox> &stylebox_time_available = get_theme_stylebox(SNAME("time_available"), SNAME("AnimationTimelineEdit"));15071508const Color v_line_primary_color = get_theme_color(SNAME("v_line_primary_color"), SNAME("AnimationTimelineEdit"));1509const Color v_line_secondary_color = get_theme_color(SNAME("v_line_secondary_color"), SNAME("AnimationTimelineEdit"));1510const Color h_line_color = get_theme_color(SNAME("h_line_color"), SNAME("AnimationTimelineEdit"));1511const Color font_primary_color = get_theme_color(SNAME("font_primary_color"), SNAME("AnimationTimelineEdit"));1512const Color font_secondary_color = get_theme_color(SNAME("font_secondary_color"), SNAME("AnimationTimelineEdit"));15131514const int v_line_primary_margin = get_theme_constant(SNAME("v_line_primary_margin"), SNAME("AnimationTimelineEdit"));1515const int v_line_secondary_margin = get_theme_constant(SNAME("v_line_secondary_margin"), SNAME("AnimationTimelineEdit"));1516const int v_line_primary_width = get_theme_constant(SNAME("v_line_primary_width"), SNAME("AnimationTimelineEdit"));1517const int v_line_secondary_width = get_theme_constant(SNAME("v_line_secondary_width"), SNAME("AnimationTimelineEdit"));1518const int text_primary_margin = get_theme_constant(SNAME("text_primary_margin"), SNAME("AnimationTimelineEdit"));1519const int text_secondary_margin = get_theme_constant(SNAME("text_secondary_margin"), SNAME("AnimationTimelineEdit"));15201521int zoomw = get_size().width - get_buttons_width() - get_name_limit();1522float scale = get_zoom_scale();1523int h = get_size().height;15241525float l = animation->get_length();1526if (l <= 0) {1527l = SECOND_DECIMAL; // Avoid crashor.1528}15291530Ref<Texture2D> hsize_icon = get_editor_theme_icon(SNAME("Hsize"));1531hsize_rect = Rect2(get_name_limit() - hsize_icon->get_width() - 8 * EDSCALE, (get_size().height - hsize_icon->get_height()) / 2, hsize_icon->get_width(), hsize_icon->get_height());1532draw_texture(hsize_icon, hsize_rect.position);15331534{1535float time_min = 0;1536float time_max = animation->get_length();1537for (int i = 0; i < animation->get_track_count(); i++) {1538if (animation->track_get_key_count(i) > 0) {1539float beg = animation->track_get_key_time(i, 0);15401541if (beg < time_min) {1542time_min = beg;1543}15441545float end = animation->track_get_key_time(i, animation->track_get_key_count(i) - 1);15461547if (end > time_max) {1548time_max = end;1549}1550}1551}15521553PackedStringArray markers = animation->get_marker_names();1554if (markers.size() > 0) {1555float min_marker = animation->get_marker_time(markers[0]);1556float max_marker = animation->get_marker_time(markers[markers.size() - 1]);1557if (min_marker < time_min) {1558time_min = min_marker;1559}1560if (max_marker > time_max) {1561time_max = max_marker;1562}1563}15641565float extra = (zoomw / scale) * 0.5;15661567time_max += extra;1568set_min(time_min);1569set_max(time_max);15701571if (zoomw / scale < (time_max - time_min)) {1572hscroll->show();15731574} else {1575hscroll->hide();1576}1577}15781579set_page(zoomw / scale);15801581if (hscroll->is_visible() && hscroll_on_zoom_buffer >= 0) {1582hscroll->set_value(hscroll_on_zoom_buffer);1583hscroll_on_zoom_buffer = -1.0;1584}15851586int end_px = (l - get_value()) * scale;1587int begin_px = -get_value() * scale;15881589draw_style_box(stylebox_time_unavailable, Rect2(Point2(get_name_limit(), 0), Point2(zoomw - 1, h)));15901591if (begin_px < zoomw && end_px > 0) {1592if (begin_px < 0) {1593begin_px = 0;1594}1595if (end_px > zoomw) {1596end_px = zoomw;1597}15981599draw_style_box(stylebox_time_available, Rect2(Point2(get_name_limit() + begin_px, 0), Point2(end_px - begin_px, h)));1600}16011602#define SC_ADJ 1001603int dec = 1;1604int step = 1;1605int decimals = 2;1606bool step_found = false;16071608const float period_width = font->get_char_size('.', font_size).width;1609float max_digit_width = font->get_char_size('0', font_size).width;1610for (int i = 1; i <= 9; i++) {1611const float digit_width = font->get_char_size('0' + i, font_size).width;1612max_digit_width = MAX(digit_width, max_digit_width);1613}1614const int max_sc = int(Math::ceil(zoomw / scale));1615const int max_sc_width = String::num(max_sc).length() * Math::ceil(max_digit_width);16161617const int min_margin = MAX(text_secondary_margin, text_primary_margin);16181619while (!step_found) {1620int min = max_sc_width;1621if (decimals > 0) {1622min += Math::ceil(period_width + max_digit_width * decimals);1623}16241625min += (min_margin * 2);16261627static const int _multp[3] = { 1, 2, 5 };1628for (int i = 0; i < 3; i++) {1629step = (_multp[i] * dec);1630if (step * scale / SC_ADJ > min) {1631step_found = true;1632break;1633}1634}1635if (step_found) {1636break;1637}1638dec *= 10;1639decimals--;1640if (decimals < 0) {1641decimals = 0;1642}1643}16441645if (use_fps) {1646float step_size = animation->get_step();1647if (step_size > 0) {1648int prev_frame_ofs = -10000000;16491650for (int i = 0; i < zoomw; i++) {1651float pos = get_value() + double(i) / scale;1652float prev = get_value() + (double(i) - 1.0) / scale;16531654int frame = pos / step_size;1655int prev_frame = prev / step_size;16561657bool sub = Math::floor(prev) == Math::floor(pos);16581659if (frame != prev_frame && i >= prev_frame_ofs) {1660int line_margin = sub ? v_line_secondary_margin : v_line_primary_margin;1661int line_width = sub ? v_line_secondary_width : v_line_primary_width;1662Color line_color = sub ? v_line_secondary_color : v_line_primary_color;16631664draw_line(Point2(get_name_limit() + i, 0 + line_margin), Point2(get_name_limit() + i, h - line_margin), line_color, line_width);16651666int text_margin = sub ? text_secondary_margin : text_primary_margin;16671668draw_string(font, Point2(get_name_limit() + i + text_margin, (h - font->get_height(font_size)) / 2 + font->get_ascent(font_size)).floor(), itos(frame), HORIZONTAL_ALIGNMENT_LEFT, zoomw - i, font_size, sub ? font_secondary_color : font_primary_color);16691670prev_frame_ofs = i + font->get_string_size(itos(frame), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x + text_margin;1671}1672}1673}16741675} else {1676for (int i = 0; i < zoomw; i++) {1677float pos = get_value() + double(i) / scale;1678float prev = get_value() + (double(i) - 1.0) / scale;16791680int sc = int(Math::floor(pos * SC_ADJ));1681int prev_sc = int(Math::floor(prev * SC_ADJ));16821683if ((sc / step) != (prev_sc / step) || (prev_sc < 0 && sc >= 0)) {1684int scd = sc < 0 ? prev_sc : sc;1685bool sub = (((scd - (scd % step)) % (dec * 10)) != 0);16861687int line_margin = sub ? v_line_secondary_margin : v_line_primary_margin;1688int line_width = sub ? v_line_secondary_width : v_line_primary_width;1689Color line_color = sub ? v_line_secondary_color : v_line_primary_color;16901691draw_line(Point2(get_name_limit() + i, 0 + line_margin), Point2(get_name_limit() + i, h - line_margin), line_color, line_width);16921693int text_margin = sub ? text_secondary_margin : text_primary_margin;16941695draw_string(font, Point2(get_name_limit() + i + text_margin, (h - font->get_height(font_size)) / 2 + font->get_ascent(font_size)).floor(), String::num((scd - (scd % step)) / double(SC_ADJ), decimals), HORIZONTAL_ALIGNMENT_LEFT, zoomw - i, font_size, sub ? font_secondary_color : font_primary_color);1696}1697}1698}16991700draw_line(Vector2(0, get_size().height), get_size(), h_line_color, Math::round(EDSCALE));17011702int px = get_name_limit() + end_px - timeline_resize_rect.size.width * 0.5;1703timeline_resize_rect.position.x = px;1704if (px >= get_name_limit() && px < zoomw + get_buttons_width()) {1705draw_texture(get_editor_theme_icon(SNAME("TimelineHandle")), Point2(timeline_resize_rect.position.x, 0));1706}17071708update_values();1709} break;17101711case NOTIFICATION_WM_WINDOW_FOCUS_OUT: {1712if (resizing_timeline) {1713_commit_timeline_resize();1714} else if (dragging_hsize || dragging_timeline) {1715_stop_dragging();1716}1717} break;1718}1719}17201721void AnimationTimelineEdit::set_animation(const Ref<Animation> &p_animation, bool p_read_only) {1722animation = p_animation;1723read_only = p_read_only;17241725length->set_read_only(read_only);17261727if (animation.is_valid()) {1728len_hb->show();1729filter_track->show();1730if (read_only) {1731add_track->hide();1732} else {1733add_track->show();1734}1735play_position->show();1736} else {1737len_hb->hide();1738filter_track->hide();1739add_track->hide();1740play_position->hide();1741}1742queue_redraw();1743}17441745Size2 AnimationTimelineEdit::get_minimum_size() const {1746Size2 ms = filter_track->get_minimum_size();1747const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));1748const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));1749ms.height = MAX(ms.height, font->get_height(font_size));1750ms.width = get_buttons_width() + add_track->get_minimum_size().width + get_editor_theme_icon(SNAME("Hsize"))->get_width() + 2 + 8 * EDSCALE;1751return ms;1752}17531754void AnimationTimelineEdit::set_zoom(Range *p_zoom) {1755zoom = p_zoom;1756zoom->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTimelineEdit::_zoom_changed));1757}17581759void AnimationTimelineEdit::auto_fit() {1760if (animation.is_null()) {1761return;1762}17631764float anim_end = animation->get_length();1765float anim_start = 0;17661767// Search for keyframe outside animation boundaries to include keyframes before animation start and after animation length.1768int track_count = animation->get_track_count();1769for (int track = 0; track < track_count; ++track) {1770for (int i = 0; i < animation->track_get_key_count(track); i++) {1771float key_time = animation->track_get_key_time(track, i);1772if (key_time > anim_end) {1773anim_end = key_time;1774}1775if (key_time < anim_start) {1776anim_start = key_time;1777}1778}1779}17801781float anim_length = anim_end - anim_start;1782int timeline_width_pixels = get_size().width - get_buttons_width() - get_name_limit();17831784// I want a little buffer at the end... (5% looks nice and we should keep some space for the bezier handles)1785timeline_width_pixels *= 0.95;17861787// The technique is to reuse the _get_zoom_scale function directly to be sure that the auto_fit is always calculated1788// the same way as the zoom slider. It's a little bit more calculation then doing the inverse of get_zoom_scale but1789// it's really easier to understand and should always be accurate.1790float new_zoom = zoom->get_max();1791while (true) {1792double test_zoom_scale = _get_zoom_scale(new_zoom);17931794if (anim_length * test_zoom_scale <= timeline_width_pixels) {1795// It fits...1796break;1797}17981799new_zoom -= zoom->get_step();18001801if (new_zoom <= zoom->get_min()) {1802new_zoom = zoom->get_min();1803break;1804}1805}18061807// Horizontal scroll to get_min which should include keyframes that are before the animation start.1808hscroll->set_value(hscroll->get_min());1809// Set the zoom value... the signal value_changed will be emitted and the timeline will be refreshed correctly!1810zoom->set_value(new_zoom);1811// The new zoom value must be applied correctly so the scrollbar are updated before we move the scrollbar to1812// the beginning of the animation, hence the call deferred.1813callable_mp(this, &AnimationTimelineEdit::_scroll_to_start).call_deferred();1814}18151816void AnimationTimelineEdit::_scroll_to_start() {1817// Horizontal scroll to get_min which should include keyframes that are before the animation start.1818hscroll->set_value(hscroll->get_min());1819}18201821void AnimationTimelineEdit::set_track_edit(AnimationTrackEdit *p_track_edit) {1822track_edit = p_track_edit;1823}18241825void AnimationTimelineEdit::set_editor(AnimationTrackEditor *p_editor) {1826editor = p_editor;1827}18281829void AnimationTimelineEdit::set_play_position(float p_pos) {1830play_position_pos = p_pos;1831play_position->queue_redraw();1832}18331834float AnimationTimelineEdit::get_play_position() const {1835return play_position_pos;1836}18371838void AnimationTimelineEdit::update_play_position() {1839play_position->queue_redraw();1840}18411842void AnimationTimelineEdit::update_values() {1843if (animation.is_null() || editing) {1844return;1845}18461847editing = true;1848if (use_fps && animation->get_step() > 0.0) {1849length->set_step(FPS_DECIMAL);1850length->set_value(animation->get_length() / animation->get_step());1851length->set_tooltip_text(TTR("Animation length (frames)"));1852time_icon->set_tooltip_text(TTR("Animation length (frames)"));1853if (track_edit) {1854track_edit->editor->_update_key_edit();1855track_edit->editor->marker_edit->_update_key_edit();1856}1857} else {1858length->set_step(SECOND_DECIMAL);1859length->set_value(animation->get_length());1860length->set_tooltip_text(TTR("Animation length (seconds)"));1861time_icon->set_tooltip_text(TTR("Animation length (seconds)"));1862}18631864switch (animation->get_loop_mode()) {1865case Animation::LOOP_NONE: {1866loop->set_button_icon(get_editor_theme_icon(SNAME("Loop")));1867loop->set_pressed(false);1868} break;1869case Animation::LOOP_LINEAR: {1870loop->set_button_icon(get_editor_theme_icon(SNAME("Loop")));1871loop->set_pressed(true);1872} break;1873case Animation::LOOP_PINGPONG: {1874loop->set_button_icon(get_editor_theme_icon(SNAME("PingPongLoop")));1875loop->set_pressed(true);1876} break;1877default:1878break;1879}18801881editing = false;1882}18831884void AnimationTimelineEdit::_play_position_draw() {1885if (animation.is_null() || play_position_pos < 0) {1886return;1887}18881889float scale = get_zoom_scale();1890int px = (-get_value() + play_position_pos) * scale + get_name_limit();18911892if (px >= get_name_limit() && px < (play_position->get_size().width - get_buttons_width())) {1893int h = editor->box_selection_container->get_global_position().y - get_global_position().y;1894Color color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));18951896play_position->draw_line(Point2(px, 0), Point2(px, h), color, Math::round(2 * EDSCALE));1897play_position->draw_texture(1898get_editor_theme_icon(SNAME("TimelineIndicator")),1899Point2(px - get_editor_theme_icon(SNAME("TimelineIndicator"))->get_width() * 0.5, 0),1900color);1901}1902}19031904void AnimationTimelineEdit::gui_input(const Ref<InputEvent> &p_event) {1905ERR_FAIL_COND(p_event.is_null());19061907if (panner->gui_input(p_event, get_global_rect())) {1908accept_event();1909return;1910}19111912const Ref<InputEventMouseButton> mb = p_event;19131914if (mb.is_valid()) {1915if (mb->is_pressed() && mb->is_alt_pressed() && mb->get_button_index() == MouseButton::WHEEL_UP) {1916if (track_edit) {1917track_edit->get_editor()->goto_prev_step(true);1918}1919accept_event();1920}19211922if (mb->is_pressed() && mb->is_alt_pressed() && mb->get_button_index() == MouseButton::WHEEL_DOWN) {1923if (track_edit) {1924track_edit->get_editor()->goto_next_step(true);1925}1926accept_event();1927}19281929if (!resizing_timeline && !dragging_hsize && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {1930if (timeline_resize_rect.has_point(mb->get_position())) {1931resizing_timeline = true;1932timeline_resize_from = timeline_resize_rect.position.x + timeline_resize_rect.size.width / 2 - mb->get_position().x;1933timeline_resize_at = animation->get_length();1934} else if (hsize_rect.has_point(mb->get_position())) {1935dragging_hsize = true;1936dragging_hsize_from = mb->get_position().x;1937dragging_hsize_at = name_limit;1938}1939}19401941if (!resizing_timeline && !panner->is_panning() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT &&1942mb->get_position().x > get_name_limit() && mb->get_position().x < (get_size().width - get_buttons_width())) {1943int x = mb->get_position().x - get_name_limit();19441945float ofs = x / get_zoom_scale() + get_value();1946emit_signal(SNAME("timeline_changed"), ofs, mb->is_alt_pressed());1947dragging_timeline = true;1948}19491950if (!mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {1951if (resizing_timeline) {1952_commit_timeline_resize();1953} else if (dragging_hsize || dragging_timeline) {1954_stop_dragging();1955}1956}19571958if (resizing_timeline && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {1959resizing_timeline = false;1960// Undo changes.1961animation->set_length(timeline_resize_at);1962}1963}19641965Ref<InputEventMouseMotion> mm = p_event;19661967if (mm.is_valid()) {1968if (dragging_hsize) {1969int ofs = mm->get_position().x - dragging_hsize_from;1970name_limit = dragging_hsize_at + ofs;1971// Make sure name_limit is clamped to the range that UI allows.1972name_limit = get_name_limit();1973int hsize_icon_width = get_editor_theme_icon(SNAME("Hsize"))->get_width();1974add_track_hb->set_size(Size2(name_limit - ((hsize_icon_width + 16) * EDSCALE), 0));1975queue_redraw();1976emit_signal(SNAME("name_limit_changed"));1977play_position->queue_redraw();1978} else if (resizing_timeline) {1979int x = mm->get_position().x + timeline_resize_from - get_name_limit();1980float ofs = x / get_zoom_scale() + get_value();1981animation->set_length(editor->snap_time(ofs));1982} else if (dragging_timeline) {1983int x = mm->get_position().x - get_name_limit();1984float ofs = x / get_zoom_scale() + get_value();1985emit_signal(SNAME("timeline_changed"), ofs, mm->is_alt_pressed());1986}1987}1988}19891990void AnimationTimelineEdit::_commit_timeline_resize() {1991if (animation->get_length() != timeline_resize_at) {1992EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();1993undo_redo->create_action(TTR("Change Animation Length"));1994undo_redo->add_do_method(animation.ptr(), "set_length", animation->get_length());1995undo_redo->add_undo_method(animation.ptr(), "set_length", timeline_resize_at);1996undo_redo->commit_action(false);1997}1998resizing_timeline = false;1999}20002001void AnimationTimelineEdit::_stop_dragging() {2002dragging_hsize = false;2003dragging_timeline = false;2004}20052006Control::CursorShape AnimationTimelineEdit::get_cursor_shape(const Point2 &p_pos) const {2007if (dragging_hsize || resizing_timeline || hsize_rect.has_point(p_pos) || timeline_resize_rect.has_point(p_pos)) {2008// Indicate that the track name column's width and the timeline length can be adjusted.2009return Control::CURSOR_HSIZE;2010} else {2011return get_default_cursor_shape();2012}2013}20142015void AnimationTimelineEdit::_pan_callback(Vector2 p_scroll_vec, Ref<InputEvent> p_event) {2016set_value(get_value() - p_scroll_vec.x / get_zoom_scale());2017}20182019void AnimationTimelineEdit::_zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref<InputEvent> p_event) {2020double current_zoom_value = get_zoom()->get_value();2021zoom_scroll_origin = p_origin;2022zoom_callback_occurred = true;2023get_zoom()->set_value(MAX(0.01, current_zoom_value - (1.0 - p_zoom_factor)));2024}20252026void AnimationTimelineEdit::set_use_fps(bool p_use_fps) {2027use_fps = p_use_fps;2028queue_redraw();2029}20302031bool AnimationTimelineEdit::is_using_fps() const {2032return use_fps;2033}20342035void AnimationTimelineEdit::set_hscroll(HScrollBar *p_hscroll) {2036hscroll = p_hscroll;2037}20382039void AnimationTimelineEdit::_track_added(int p_track) {2040emit_signal(SNAME("track_added"), p_track);2041}20422043void AnimationTimelineEdit::_bind_methods() {2044ADD_SIGNAL(MethodInfo("zoom_changed"));2045ADD_SIGNAL(MethodInfo("name_limit_changed"));2046ADD_SIGNAL(MethodInfo("timeline_changed", PropertyInfo(Variant::FLOAT, "position"), PropertyInfo(Variant::BOOL, "timeline_only")));2047ADD_SIGNAL(MethodInfo("track_added", PropertyInfo(Variant::INT, "track")));2048ADD_SIGNAL(MethodInfo("length_changed", PropertyInfo(Variant::FLOAT, "size")));2049ADD_SIGNAL(MethodInfo("filter_changed"));20502051ClassDB::bind_method(D_METHOD("update_values"), &AnimationTimelineEdit::update_values);2052}20532054AnimationTimelineEdit::AnimationTimelineEdit() {2055name_limit = 150 * EDSCALE;20562057play_position = memnew(Control);2058play_position->set_mouse_filter(MOUSE_FILTER_PASS);2059add_child(play_position);2060play_position->set_anchors_and_offsets_preset(PRESET_FULL_RECT);2061play_position->connect(SceneStringName(draw), callable_mp(this, &AnimationTimelineEdit::_play_position_draw));20622063add_track_hb = memnew(HBoxContainer);2064add_child(add_track_hb);20652066add_track = memnew(MenuButton);2067add_track->set_tooltip_text(TTR("Select a new track by type to add to this animation."));2068add_track->set_position(Vector2(0, 0));2069add_track_hb->add_child(add_track);2070filter_track = memnew(LineEdit);2071filter_track->set_h_size_flags(SIZE_EXPAND_FILL);2072filter_track->set_custom_minimum_size(Vector2(120 * EDSCALE, 0));2073filter_track->set_placeholder(TTR("Filter Tracks"));2074filter_track->set_tooltip_text(TTR("Filter tracks by entering part of their node name or property."));2075filter_track->connect(SceneStringName(text_changed), callable_mp((AnimationTrackEditor *)this, &AnimationTrackEditor::_on_filter_updated));2076filter_track->set_clear_button_enabled(true);2077filter_track->hide();2078add_track_hb->add_child(filter_track);20792080len_hb = memnew(HBoxContainer);20812082Control *expander = memnew(Control);2083expander->set_h_size_flags(SIZE_EXPAND_FILL);2084expander->set_mouse_filter(MOUSE_FILTER_IGNORE);2085len_hb->add_child(expander);20862087time_icon = memnew(TextureRect);2088time_icon->set_v_size_flags(SIZE_SHRINK_CENTER);2089time_icon->set_tooltip_text(TTR("Animation length (seconds)"));2090len_hb->add_child(time_icon);20912092length = memnew(EditorSpinSlider);2093length->set_min(SECOND_DECIMAL);2094length->set_max(36000);2095length->set_step(SECOND_DECIMAL);2096length->set_allow_greater(true);2097length->set_custom_minimum_size(Vector2(70 * EDSCALE, 0));2098length->set_control_state(EditorSpinSlider::CONTROL_STATE_HIDE);2099length->set_tooltip_text(TTR("Animation length (seconds)"));2100length->set_accessibility_name(TTRC("Animation length (seconds)"));2101length->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTimelineEdit::_anim_length_changed));2102len_hb->add_child(length);21032104loop = memnew(Button);2105loop->set_flat(true);2106loop->set_tooltip_text(TTR("Animation Looping"));2107loop->connect(SceneStringName(pressed), callable_mp(this, &AnimationTimelineEdit::_anim_loop_pressed));2108loop->set_toggle_mode(true);2109len_hb->add_child(loop);2110add_child(len_hb);21112112add_track->hide();2113add_track->get_popup()->connect("index_pressed", callable_mp(this, &AnimationTimelineEdit::_track_added));2114len_hb->hide();21152116panner.instantiate();2117panner->set_scroll_zoom_factor(SCROLL_ZOOM_FACTOR_IN);2118panner->set_callbacks(callable_mp(this, &AnimationTimelineEdit::_pan_callback), callable_mp(this, &AnimationTimelineEdit::_zoom_callback));2119panner->set_pan_axis(ViewPanner::PAN_AXIS_HORIZONTAL);21202121set_layout_direction(Control::LAYOUT_DIRECTION_LTR);2122}21232124////////////////////////////////////21252126void AnimationTrackEdit::_notification(int p_what) {2127switch (p_what) {2128case NOTIFICATION_THEME_CHANGED: {2129if (animation.is_null()) {2130return;2131}2132ERR_FAIL_INDEX(track, animation->get_track_count());21332134type_icon = _get_key_type_icon();2135selected_icon = get_editor_theme_icon(SNAME("KeySelected"));2136} break;21372138case NOTIFICATION_DRAW: {2139if (animation.is_null() || animation->get_track_count() == 0) {2140return;2141}2142ERR_FAIL_INDEX(track, animation->get_track_count());21432144int limit = timeline->get_name_limit();21452146const Ref<StyleBox> &stylebox_odd = get_theme_stylebox(SNAME("odd"), SNAME("AnimationTrackEdit"));2147const Ref<StyleBox> &stylebox_focus = get_theme_stylebox(SNAME("focus"), SNAME("AnimationTrackEdit"));2148const Ref<StyleBox> &stylebox_hover = get_theme_stylebox(SceneStringName(hover), SNAME("AnimationTrackEdit"));21492150const Color h_line_color = get_theme_color(SNAME("h_line_color"), SNAME("AnimationTrackEdit"));2151const int h_separation = get_theme_constant(SNAME("h_separation"), SNAME("AnimationTrackEdit"));2152const int outer_margin = get_theme_constant(SNAME("outer_margin"), SNAME("AnimationTrackEdit"));21532154if (track % 2 == 1) {2155// Draw a background over odd lines to make long lists of tracks easier to read.2156draw_style_box(stylebox_odd, Rect2(Point2(1 * EDSCALE, 0), get_size() - Size2(1 * EDSCALE, 0)));2157}21582159if (hovered) {2160// Draw hover feedback.2161draw_style_box(stylebox_hover, Rect2(Point2(1 * EDSCALE, 0), get_size() - Size2(1 * EDSCALE, 0)));2162}21632164if (has_focus()) {2165// Offside so the horizontal sides aren't cutoff.2166draw_style_box(stylebox_focus, Rect2(Point2(1 * EDSCALE, 0), get_size() - Size2(1 * EDSCALE, 0)));2167}21682169int limit_end = get_size().width - timeline->get_buttons_width();21702171// Unavailable timeline.21722173{2174int px = (animation->get_length() - timeline->get_value()) * timeline->get_zoom_scale() + timeline->get_name_limit();2175px = MAX(px, timeline->get_name_limit());2176Rect2 rect = Rect2(px, 0, limit_end - px, get_size().height);2177if (rect.size.width > 0) {2178draw_rect(rect, Color(0, 0, 0, 0.2));2179}2180}21812182const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));2183const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));2184const Color color = get_theme_color(SceneStringName(font_color), SNAME("Label"));21852186const Color dc = get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor));21872188// Names and icons.21892190{2191Ref<Texture2D> check = animation->track_is_enabled(track) ? get_theme_icon(SNAME("checked"), SNAME("CheckBox")) : get_theme_icon(SNAME("unchecked"), SNAME("CheckBox"));21922193int ofs = in_group ? outer_margin : 0;21942195check_rect = Rect2(Point2(ofs, (get_size().height - check->get_height()) / 2).round(), check->get_size());2196draw_texture(check, check_rect.position);2197ofs += check->get_width() + h_separation;21982199Ref<Texture2D> key_type_icon = _get_key_type_icon();2200draw_texture(key_type_icon, Point2(ofs, (get_size().height - key_type_icon->get_height()) / 2).round());2201ofs += key_type_icon->get_width() + h_separation;22022203NodePath anim_path = animation->track_get_path(track);2204Node *node = nullptr;2205if (root) {2206node = root->get_node_or_null(anim_path);2207}22082209String text;2210Color text_color = color;2211if (node && EditorNode::get_singleton()->get_editor_selection()->is_selected(node)) {2212text_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));2213}22142215if (in_group) {2216if (animation->track_get_type(track) == Animation::TYPE_METHOD) {2217text = TTR("Functions:");2218} else if (animation->track_get_type(track) == Animation::TYPE_AUDIO) {2219text = TTR("Audio Clips:");2220} else if (animation->track_get_type(track) == Animation::TYPE_ANIMATION) {2221text = TTR("Animation Clips:");2222} else {2223text += anim_path.get_concatenated_subnames();2224}2225text_color.a *= 0.7;2226} else if (node) {2227Ref<Texture2D> icon = EditorNode::get_singleton()->get_object_icon(node);2228const Vector2 icon_size = Vector2(1, 1) * get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor));22292230icon_rect = Rect2(Point2(ofs, (get_size().height - check->get_height()) / 2).round(), icon_size);2231draw_texture_rect(icon, icon_rect);2232icon_cache = icon;22332234text = String() + node->get_name() + ":" + anim_path.get_concatenated_subnames();2235ofs += h_separation;2236ofs += icon_size.x;22372238} else {2239icon_cache = key_type_icon;22402241text = String(anim_path);2242}22432244path_cache = text;22452246path_rect = Rect2(ofs, 0, limit - ofs - h_separation, get_size().height);22472248Vector2 string_pos = Point2(ofs, (get_size().height - font->get_height(font_size)) / 2 + font->get_ascent(font_size));2249string_pos = string_pos.floor();2250draw_string(font, string_pos, text, HORIZONTAL_ALIGNMENT_LEFT, limit - ofs - h_separation, font_size, text_color);22512252draw_line(Point2(limit, 0), Point2(limit, get_size().height), h_line_color, Math::round(EDSCALE));2253}22542255// Marker sections.22562257{2258float scale = timeline->get_zoom_scale();22592260PackedStringArray section = editor->get_selected_section();2261if (section.size() == 2) {2262StringName start_marker = section[0];2263StringName end_marker = section[1];2264double start_time = animation->get_marker_time(start_marker);2265double end_time = animation->get_marker_time(end_marker);22662267// When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section.2268AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();2269if (editor->is_marker_moving_selection() && !(player && player->is_playing())) {2270start_time += editor->get_marker_moving_selection_offset();2271end_time += editor->get_marker_moving_selection_offset();2272}22732274if (start_time < animation->get_length() && end_time >= 0) {2275float start_ofs = MAX(0, start_time) - timeline->get_value();2276float end_ofs = MIN(animation->get_length(), end_time) - timeline->get_value();2277start_ofs = start_ofs * scale + limit;2278end_ofs = end_ofs * scale + limit;2279start_ofs = MAX(start_ofs, limit);2280end_ofs = MIN(end_ofs, limit_end);2281Rect2 rect;2282rect.set_position(Vector2(start_ofs, 0));2283rect.set_size(Vector2(end_ofs - start_ofs, get_size().height));22842285draw_rect(rect, Color(1, 0.1, 0.1, 0.2));2286}2287}2288}22892290// Marker overlays.22912292{2293float scale = timeline->get_zoom_scale();2294PackedStringArray markers = animation->get_marker_names();2295for (const StringName marker : markers) {2296double time = animation->get_marker_time(marker);2297if (editor->is_marker_selected(marker) && editor->is_marker_moving_selection()) {2298time += editor->get_marker_moving_selection_offset();2299}2300if (time >= 0) {2301float offset = time - timeline->get_value();2302offset = offset * scale + limit;2303if (offset >= timeline->get_name_limit() && offset < limit_end) {2304Color marker_color = animation->get_marker_color(marker);2305marker_color.a = 0.2;2306draw_line(Point2(offset, 0), Point2(offset, get_size().height), marker_color, Math::round(EDSCALE));2307}2308}2309}2310}23112312// Keyframes.23132314draw_bg(limit, limit_end);23152316{2317float scale = timeline->get_zoom_scale();23182319// Pre-calculate the actual order of the keys. This is needed as a move might be happening2320// which might cause the keys to be in a different order than their current indices.2321Vector<Pair<float, int>> sorted_keys;23222323for (int i = 0; i < animation->track_get_key_count(track); i++) {2324float time_offset = animation->track_get_key_time(track, i) - timeline->get_value();2325if (editor->is_key_selected(track, i) && editor->is_moving_selection()) {2326time_offset += editor->get_moving_selection_offset();2327}23282329float screen_pos = time_offset * scale + limit;2330sorted_keys.push_back(Pair<float, int>(screen_pos, i));2331}23322333sorted_keys.sort();23342335for (int i = 0; i < sorted_keys.size(); i++) {2336float offset = sorted_keys[i].first;2337int original_index = sorted_keys[i].second;2338bool selected = editor->is_key_selected(track, original_index);23392340if (i < sorted_keys.size() - 1) {2341float offset_n = sorted_keys[i + 1].first;23422343int next_original_index = sorted_keys[i + 1].second;23442345draw_key_link(original_index, next_original_index, scale, int(offset), int(offset_n), limit, limit_end);2346draw_key(original_index, scale, int(offset), selected, limit, MIN(offset_n, limit_end));2347} else {2348draw_key(original_index, scale, int(offset), selected, limit, limit_end);2349}2350}2351}23522353draw_fg(limit, limit_end);23542355// Buttons.23562357{2358Ref<Texture2D> wrap_icon[2] = {2359get_editor_theme_icon(SNAME("InterpWrapClamp")),2360get_editor_theme_icon(SNAME("InterpWrapLoop")),2361};2362Ref<Texture2D> interp_icon[5] = {2363get_editor_theme_icon(SNAME("InterpRaw")),2364get_editor_theme_icon(SNAME("InterpLinear")),2365get_editor_theme_icon(SNAME("InterpCubic")),2366get_editor_theme_icon(SNAME("InterpLinearAngle")),2367get_editor_theme_icon(SNAME("InterpCubicAngle")),2368};2369Ref<Texture2D> cont_icon[3] = {2370get_editor_theme_icon(SNAME("TrackContinuous")),2371get_editor_theme_icon(SNAME("TrackDiscrete")),2372get_editor_theme_icon(SNAME("TrackCapture"))2373};2374Ref<Texture2D> blend_icon[2] = {2375get_editor_theme_icon(SNAME("UseBlendEnable")),2376get_editor_theme_icon(SNAME("UseBlendDisable")),2377};23782379int ofs = limit_end;23802381const Ref<Texture2D> down_icon = get_theme_icon(SNAME("select_arrow"), SNAME("Tree"));23822383draw_line(Point2(ofs, 0), Point2(ofs, get_size().height), h_line_color, Math::round(EDSCALE));23842385ofs += h_separation;2386{2387// Callmode.23882389Animation::UpdateMode update_mode;23902391if (animation->track_get_type(track) == Animation::TYPE_VALUE) {2392update_mode = animation->value_track_get_update_mode(track);2393} else {2394update_mode = Animation::UPDATE_CONTINUOUS;2395}23962397Ref<Texture2D> update_icon = cont_icon[update_mode];23982399update_mode_rect.position.x = ofs;2400update_mode_rect.position.y = Math::round((get_size().height - update_icon->get_height()) / 2);2401update_mode_rect.size = update_icon->get_size();24022403if (!animation->track_is_compressed(track) && animation->track_get_type(track) == Animation::TYPE_VALUE) {2404draw_texture(update_icon, update_mode_rect.position);2405}2406if (animation->track_get_type(track) == Animation::TYPE_AUDIO) {2407Ref<Texture2D> use_blend_icon = blend_icon[animation->audio_track_is_use_blend(track) ? 0 : 1];2408Vector2 use_blend_icon_pos = update_mode_rect.position + (update_mode_rect.size - use_blend_icon->get_size()) / 2;2409draw_texture(use_blend_icon, use_blend_icon_pos);2410}2411// Make it easier to click.2412update_mode_rect.position.y = 0;2413update_mode_rect.size.y = get_size().height;24142415ofs += update_icon->get_width() + h_separation / 2;2416update_mode_rect.size.x += h_separation / 2;24172418if (!read_only) {2419if (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_AUDIO) {2420draw_texture(down_icon, Vector2(ofs, (get_size().height - down_icon->get_height()) / 2).round());2421update_mode_rect.size.x += down_icon->get_width();2422} else if (animation->track_get_type(track) == Animation::TYPE_BEZIER) {2423update_mode_rect.size.x += down_icon->get_width();2424update_mode_rect = Rect2();2425} else {2426update_mode_rect = Rect2();2427}2428} else {2429update_mode_rect = Rect2();2430}24312432ofs += down_icon->get_width();2433draw_line(Point2(ofs + h_separation * 0.5, 0), Point2(ofs + h_separation * 0.5, get_size().height), h_line_color, Math::round(EDSCALE));2434ofs += h_separation;2435}24362437{2438// Interp.24392440Animation::InterpolationType interp_mode = animation->track_get_interpolation_type(track);24412442Ref<Texture2D> icon = interp_icon[interp_mode];24432444interp_mode_rect.position.x = ofs;2445interp_mode_rect.position.y = Math::round((get_size().height - icon->get_height()) / 2);2446interp_mode_rect.size = icon->get_size();24472448if (!animation->track_is_compressed(track) && (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_BLEND_SHAPE || animation->track_get_type(track) == Animation::TYPE_POSITION_3D || animation->track_get_type(track) == Animation::TYPE_SCALE_3D || animation->track_get_type(track) == Animation::TYPE_ROTATION_3D)) {2449draw_texture(icon, interp_mode_rect.position);2450}2451// Make it easier to click.2452interp_mode_rect.position.y = 0;2453interp_mode_rect.size.y = get_size().height;24542455ofs += icon->get_width() + h_separation / 2;2456interp_mode_rect.size.x += h_separation / 2;24572458if (!read_only && !animation->track_is_compressed(track) && (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_BLEND_SHAPE || animation->track_get_type(track) == Animation::TYPE_POSITION_3D || animation->track_get_type(track) == Animation::TYPE_SCALE_3D || animation->track_get_type(track) == Animation::TYPE_ROTATION_3D)) {2459draw_texture(down_icon, Vector2(ofs, (get_size().height - down_icon->get_height()) / 2).round());2460interp_mode_rect.size.x += down_icon->get_width();2461} else {2462interp_mode_rect = Rect2();2463}24642465ofs += down_icon->get_width();2466draw_line(Point2(ofs + h_separation * 0.5, 0), Point2(ofs + h_separation * 0.5, get_size().height), h_line_color, Math::round(EDSCALE));2467ofs += h_separation;2468}24692470{2471// Loop.24722473bool loop_wrap = animation->track_get_interpolation_loop_wrap(track);24742475Ref<Texture2D> icon = wrap_icon[loop_wrap ? 1 : 0];24762477loop_wrap_rect.position.x = ofs;2478loop_wrap_rect.position.y = Math::round((get_size().height - icon->get_height()) / 2);2479loop_wrap_rect.size = icon->get_size();24802481if (!animation->track_is_compressed(track) && (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_BLEND_SHAPE || animation->track_get_type(track) == Animation::TYPE_POSITION_3D || animation->track_get_type(track) == Animation::TYPE_SCALE_3D || animation->track_get_type(track) == Animation::TYPE_ROTATION_3D)) {2482draw_texture(icon, loop_wrap_rect.position);2483}24842485loop_wrap_rect.position.y = 0;2486loop_wrap_rect.size.y = get_size().height;24872488ofs += icon->get_width() + h_separation / 2;2489loop_wrap_rect.size.x += h_separation / 2;24902491if (!read_only && !animation->track_is_compressed(track) && (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_BLEND_SHAPE || animation->track_get_type(track) == Animation::TYPE_POSITION_3D || animation->track_get_type(track) == Animation::TYPE_SCALE_3D || animation->track_get_type(track) == Animation::TYPE_ROTATION_3D)) {2492draw_texture(down_icon, Vector2(ofs, (get_size().height - down_icon->get_height()) / 2).round());2493loop_wrap_rect.size.x += down_icon->get_width();2494} else {2495loop_wrap_rect = Rect2();2496}24972498ofs += down_icon->get_width();2499draw_line(Point2(ofs + h_separation * 0.5, 0), Point2(ofs + h_separation * 0.5, get_size().height), h_line_color, Math::round(EDSCALE));2500ofs += h_separation;2501}25022503{2504// Erase.25052506Ref<Texture2D> icon = get_editor_theme_icon(animation->track_is_compressed(track) ? SNAME("Lock") : SNAME("Remove"));25072508remove_rect.position.x = ofs + ((get_size().width - ofs) - icon->get_width()) - outer_margin;2509remove_rect.position.y = Math::round((get_size().height - icon->get_height()) / 2);2510remove_rect.size = icon->get_size();25112512if (read_only) {2513draw_texture(icon, remove_rect.position, dc);2514} else {2515draw_texture(icon, remove_rect.position);2516}2517}2518}25192520if (in_group) {2521draw_line(Vector2(timeline->get_name_limit(), get_size().height), get_size(), h_line_color, Math::round(EDSCALE));2522} else {2523draw_line(Vector2(0, get_size().height), get_size(), h_line_color, Math::round(EDSCALE));2524}25252526if (dropping_at != 0) {2527Color drop_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));2528if (dropping_at < 0) {2529draw_line(Vector2(0, 0), Vector2(get_size().width, 0), drop_color, Math::round(EDSCALE));2530} else {2531draw_line(Vector2(0, get_size().height), get_size(), drop_color, Math::round(EDSCALE));2532}2533}2534} break;25352536case NOTIFICATION_MOUSE_ENTER:2537hovered = true;2538queue_redraw();2539break;2540case NOTIFICATION_MOUSE_EXIT:2541hovered = false;2542// When the mouse cursor exits the track, we're no longer hovering any keyframe.2543hovering_key_idx = -1;2544queue_redraw();2545[[fallthrough]];2546case NOTIFICATION_DRAG_END: {2547cancel_drop();2548} break;2549}2550}25512552int AnimationTrackEdit::get_key_height() const {2553if (animation.is_null()) {2554return 0;2555}25562557return type_icon->get_height();2558}25592560Rect2 AnimationTrackEdit::get_key_rect(int p_index, float p_pixels_sec) {2561if (animation.is_null()) {2562return Rect2();2563}2564Rect2 rect = Rect2(-type_icon->get_width() / 2, 0, type_icon->get_width(), get_size().height);25652566// Make it a big easier to click.2567rect.position.x -= rect.size.x * 0.5;2568rect.size.x *= 2;2569return rect;2570}25712572bool AnimationTrackEdit::is_key_selectable_by_distance() const {2573return true;2574}25752576void AnimationTrackEdit::draw_key_link(int p_index_from, int p_index_to, float p_pixels_sec, int p_x, int p_next_x, int p_clip_left, int p_clip_right) {2577if (p_next_x < p_clip_left) {2578return;2579}2580if (p_x > p_clip_right) {2581return;2582}25832584Variant current = animation->track_get_key_value(get_track(), p_index_from);2585Variant next = animation->track_get_key_value(get_track(), p_index_to);2586if (current != next || animation->track_get_type(get_track()) == Animation::TrackType::TYPE_METHOD) {2587return;2588}25892590Color color = get_theme_color(SceneStringName(font_color), SNAME("Label"));2591color.a = 0.5;25922593int from_x = MAX(p_x, p_clip_left);2594int to_x = MIN(p_next_x, p_clip_right);25952596draw_line(Point2(from_x + 1, get_size().height / 2), Point2(to_x, get_size().height / 2), color, Math::round(2 * EDSCALE));2597}25982599void AnimationTrackEdit::draw_key(int p_index, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right) {2600if (animation.is_null()) {2601return;2602}26032604if (p_x < p_clip_left || p_x > p_clip_right) {2605return;2606}26072608Ref<Texture2D> icon_to_draw = p_selected ? selected_icon : type_icon;26092610if (animation->track_get_type(track) == Animation::TYPE_VALUE && !Math::is_equal_approx(animation->track_get_key_transition(track, p_index), real_t(1.0))) {2611// Use a different icon for keys with non-linear easing.2612icon_to_draw = get_editor_theme_icon(p_selected ? SNAME("KeyEasedSelected") : SNAME("KeyValueEased"));2613}26142615// Override type icon for invalid value keys, unless selected.2616if (!p_selected && animation->track_get_type(track) == Animation::TYPE_VALUE) {2617const Variant &v = animation->track_get_key_value(track, p_index);2618Variant::Type valid_type = Variant::NIL;2619if (!_is_value_key_valid(v, valid_type)) {2620icon_to_draw = get_editor_theme_icon(SNAME("KeyInvalid"));2621}2622}26232624Vector2 ofs(p_x - icon_to_draw->get_width() / 2, (get_size().height - icon_to_draw->get_height()) / 2);26252626if (animation->track_get_type(track) == Animation::TYPE_METHOD) {2627const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));2628const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));2629Color color = get_theme_color(SceneStringName(font_color), SNAME("Label"));2630color.a = 0.5;26312632Dictionary d = animation->track_get_key_value(track, p_index);2633String text;26342635if (d.has("method")) {2636text += String(d["method"]);2637}2638text += "(";2639Vector<Variant> args;2640if (d.has("args")) {2641args = d["args"];2642}2643for (int i = 0; i < args.size(); i++) {2644if (i > 0) {2645text += ", ";2646}2647text += args[i].get_construct_string();2648}2649text += ")";26502651int limit = editor->is_function_name_pressed() ? 0 : MAX(0, p_clip_right - p_x - icon_to_draw->get_width() * 2);26522653if (limit > 0) {2654draw_string(font, Vector2(p_x + icon_to_draw->get_width(), int(get_size().height - font->get_height(font_size)) / 2 + font->get_ascent(font_size)), text, HORIZONTAL_ALIGNMENT_LEFT, limit, font_size, color);2655}2656}26572658// Use a different color for the currently hovered key.2659// The color multiplier is chosen to work with both dark and light editor themes,2660// and on both unselected and selected key icons.2661draw_texture(2662icon_to_draw,2663ofs,2664p_index == hovering_key_idx ? get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")) : Color(1, 1, 1));2665}26662667// Helper.2668void AnimationTrackEdit::draw_rect_clipped(const Rect2 &p_rect, const Color &p_color, bool p_filled) {2669int clip_left = timeline->get_name_limit();2670int clip_right = get_size().width - timeline->get_buttons_width();26712672if (p_rect.position.x > clip_right) {2673return;2674}2675if (p_rect.position.x + p_rect.size.x < clip_left) {2676return;2677}2678Rect2 clip = Rect2(clip_left, 0, clip_right - clip_left, get_size().height);2679draw_rect(clip.intersection(p_rect), p_color, p_filled);2680}26812682void AnimationTrackEdit::draw_bg(int p_clip_left, int p_clip_right) {2683}26842685void AnimationTrackEdit::draw_fg(int p_clip_left, int p_clip_right) {2686}26872688void AnimationTrackEdit::draw_texture_region_clipped(const Ref<Texture2D> &p_texture, const Rect2 &p_rect, const Rect2 &p_region) {2689int clip_left = timeline->get_name_limit();2690int clip_right = get_size().width - timeline->get_buttons_width();26912692// Clip left and right.2693if (clip_left > p_rect.position.x + p_rect.size.x) {2694return;2695}2696if (clip_right < p_rect.position.x) {2697return;2698}26992700Rect2 rect = p_rect;2701Rect2 region = p_region;27022703if (clip_left > rect.position.x) {2704int rect_pixels = (clip_left - rect.position.x);2705int region_pixels = rect_pixels * region.size.x / rect.size.x;27062707rect.position.x += rect_pixels;2708rect.size.x -= rect_pixels;27092710region.position.x += region_pixels;2711region.size.x -= region_pixels;2712}27132714if (clip_right < rect.position.x + rect.size.x) {2715int rect_pixels = rect.position.x + rect.size.x - clip_right;2716int region_pixels = rect_pixels * region.size.x / rect.size.x;27172718rect.size.x -= rect_pixels;2719region.size.x -= region_pixels;2720}27212722draw_texture_rect_region(p_texture, rect, region);2723}27242725int AnimationTrackEdit::get_track() const {2726return track;2727}27282729Ref<Animation> AnimationTrackEdit::get_animation() const {2730return animation;2731}27322733void AnimationTrackEdit::set_animation_and_track(const Ref<Animation> &p_animation, int p_track, bool p_read_only) {2734animation = p_animation;2735read_only = p_read_only;27362737track = p_track;2738queue_redraw();27392740ERR_FAIL_INDEX(track, animation->get_track_count());27412742node_path = animation->track_get_path(p_track);2743type_icon = _get_key_type_icon();2744selected_icon = get_editor_theme_icon(SNAME("KeySelected"));2745}27462747NodePath AnimationTrackEdit::get_path() const {2748return node_path;2749}27502751Size2 AnimationTrackEdit::get_minimum_size() const {2752Ref<Texture2D> texture = get_editor_theme_icon(SNAME("Object"));2753const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));2754const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));2755const int separation = get_theme_constant(SNAME("v_separation"), SNAME("ItemList"));27562757int max_h = MAX(texture->get_height(), font->get_height(font_size));2758max_h = MAX(max_h, get_key_height());27592760return Vector2(1, max_h + separation);2761}27622763void AnimationTrackEdit::set_timeline(AnimationTimelineEdit *p_timeline) {2764timeline = p_timeline;2765timeline->set_track_edit(this);2766timeline->connect("zoom_changed", callable_mp(this, &AnimationTrackEdit::_zoom_changed));2767timeline->connect("name_limit_changed", callable_mp(this, &AnimationTrackEdit::_zoom_changed));2768}27692770void AnimationTrackEdit::set_editor(AnimationTrackEditor *p_editor) {2771editor = p_editor;2772}27732774void AnimationTrackEdit::_play_position_draw() {2775if (animation.is_null() || play_position_pos < 0) {2776return;2777}27782779float scale = timeline->get_zoom_scale();2780int h = get_size().height;27812782int px = (-timeline->get_value() + play_position_pos) * scale + timeline->get_name_limit();27832784if (px >= timeline->get_name_limit() && px < (get_size().width - timeline->get_buttons_width())) {2785Color color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));2786play_position->draw_line(Point2(px, 0), Point2(px, h), color, Math::round(2 * EDSCALE));2787}2788}27892790void AnimationTrackEdit::set_play_position(float p_pos) {2791play_position_pos = p_pos;2792play_position->queue_redraw();2793}27942795void AnimationTrackEdit::update_play_position() {2796play_position->queue_redraw();2797}27982799void AnimationTrackEdit::set_root(Node *p_root) {2800root = p_root;2801}28022803void AnimationTrackEdit::_zoom_changed() {2804queue_redraw();2805play_position->queue_redraw();2806}28072808void AnimationTrackEdit::_path_submitted(const String &p_text) {2809EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();2810undo_redo->create_action(TTR("Change Track Path"));2811undo_redo->add_do_method(animation.ptr(), "track_set_path", track, p_text);2812undo_redo->add_undo_method(animation.ptr(), "track_set_path", track, animation->track_get_path(track));2813undo_redo->commit_action();2814path_popup->hide();2815}28162817bool AnimationTrackEdit::_is_value_key_valid(const Variant &p_key_value, Variant::Type &r_valid_type) const {2818if (root == nullptr || !root->has_node_and_resource(animation->track_get_path(track))) {2819return false;2820}2821Ref<Resource> res;2822Vector<StringName> leftover_path;2823Node *node = root->get_node_and_resource(animation->track_get_path(track), res, leftover_path);28242825Object *obj = nullptr;2826if (res.is_valid()) {2827obj = res.ptr();2828} else if (node) {2829obj = node;2830}28312832bool prop_exists = false;2833if (obj) {2834r_valid_type = obj->get_static_property_type_indexed(leftover_path, &prop_exists);2835}28362837return (!prop_exists || Variant::can_convert(p_key_value.get_type(), r_valid_type));2838}28392840Ref<Texture2D> AnimationTrackEdit::_get_key_type_icon() const {2841const Ref<Texture2D> type_icons[9] = {2842get_editor_theme_icon(SNAME("KeyValue")),2843get_editor_theme_icon(SNAME("KeyTrackPosition")),2844get_editor_theme_icon(SNAME("KeyTrackRotation")),2845get_editor_theme_icon(SNAME("KeyTrackScale")),2846get_editor_theme_icon(SNAME("KeyTrackBlendShape")),2847get_editor_theme_icon(SNAME("KeyCall")),2848get_editor_theme_icon(SNAME("KeyBezier")),2849get_editor_theme_icon(SNAME("KeyAudio")),2850get_editor_theme_icon(SNAME("KeyAnimation"))2851};2852return type_icons[animation->track_get_type(track)];2853}28542855Control::CursorShape AnimationTrackEdit::get_cursor_shape(const Point2 &p_pos) const {2856if (command_or_control_pressed && animation->track_get_type(track) == Animation::TYPE_METHOD && hovering_key_idx != -1) {2857return Control::CURSOR_POINTING_HAND;2858}2859return get_default_cursor_shape();2860}28612862String AnimationTrackEdit::get_tooltip(const Point2 &p_pos) const {2863if (check_rect.has_point(p_pos)) {2864return TTR("Toggle this track on/off.");2865}28662867if (icon_rect.has_point(p_pos)) {2868return TTR("Select node in scene.");2869}28702871// Don't overlap track keys if they start at 0.2872if (path_rect.has_point(p_pos + Size2(type_icon->get_width(), 0))) {2873return String(animation->track_get_path(track));2874}28752876if (update_mode_rect.has_point(p_pos)) {2877if (animation->track_get_type(track) == Animation::TYPE_AUDIO) {2878return TTR("Use Blend");2879} else {2880return TTR("Update Mode (How this property is set)");2881}2882}28832884if (interp_mode_rect.has_point(p_pos)) {2885return TTR("Interpolation Mode");2886}28872888if (loop_wrap_rect.has_point(p_pos)) {2889return TTR("Loop Wrap Mode (Interpolate end with beginning on loop)");2890}28912892if (remove_rect.has_point(p_pos)) {2893return TTR("Remove this track.");2894}28952896int limit = timeline->get_name_limit();2897int limit_end = get_size().width - timeline->get_buttons_width();2898// Left Border including space occupied by keyframes on t=0.2899int limit_start_hitbox = limit - type_icon->get_width();29002901if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) {2902int key_idx = -1;2903float key_distance = 1e20;29042905// Select should happen in the opposite order of drawing for more accurate overlap select.2906for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) {2907Rect2 rect = const_cast<AnimationTrackEdit *>(this)->get_key_rect(i, timeline->get_zoom_scale());2908float offset = animation->track_get_key_time(track, i) - timeline->get_value();2909offset = offset * timeline->get_zoom_scale() + limit;2910rect.position.x += offset;29112912if (rect.has_point(p_pos)) {2913if (const_cast<AnimationTrackEdit *>(this)->is_key_selectable_by_distance()) {2914float distance = Math::abs(offset - p_pos.x);2915if (key_idx == -1 || distance < key_distance) {2916key_idx = i;2917key_distance = distance;2918}2919} else {2920// First one does it.2921break;2922}2923}2924}29252926if (key_idx != -1) {2927String text = TTR("Time (s):") + " " + TranslationServer::get_singleton()->format_number(rtos(Math::snapped(animation->track_get_key_time(track, key_idx), SECOND_DECIMAL)), _get_locale()) + "\n";2928switch (animation->track_get_type(track)) {2929case Animation::TYPE_POSITION_3D: {2930Vector3 t = animation->track_get_key_value(track, key_idx);2931text += TTR("Position:") + " " + String(t);2932} break;2933case Animation::TYPE_ROTATION_3D: {2934Quaternion t = animation->track_get_key_value(track, key_idx);2935text += TTR("Rotation:") + " " + String(t);2936} break;2937case Animation::TYPE_SCALE_3D: {2938Vector3 t = animation->track_get_key_value(track, key_idx);2939text += TTR("Scale:") + " " + String(t);2940} break;2941case Animation::TYPE_BLEND_SHAPE: {2942float t = animation->track_get_key_value(track, key_idx);2943text += TTR("Blend Shape:") + " " + itos(t);2944} break;2945case Animation::TYPE_VALUE: {2946const Variant &v = animation->track_get_key_value(track, key_idx);2947text += TTR("Type:") + " " + Variant::get_type_name(v.get_type()) + "\n";2948Variant::Type valid_type = Variant::NIL;2949text += TTR("Value:") + " " + String(v);2950if (!_is_value_key_valid(v, valid_type)) {2951text += " " + vformat(TTR("(Invalid, expected type: %s)"), Variant::get_type_name(valid_type));2952}2953text += "\n" + TTR("Easing:") + " " + rtos(animation->track_get_key_transition(track, key_idx));29542955} break;2956case Animation::TYPE_METHOD: {2957Dictionary d = animation->track_get_key_value(track, key_idx);2958if (d.has("method")) {2959text += String(d["method"]);2960}2961text += "(";2962Vector<Variant> args;2963if (d.has("args")) {2964args = d["args"];2965}2966for (int i = 0; i < args.size(); i++) {2967if (i > 0) {2968text += ", ";2969}2970text += args[i].get_construct_string();2971}2972text += ")";29732974} break;2975case Animation::TYPE_BEZIER: {2976float h = animation->bezier_track_get_key_value(track, key_idx);2977text += TTR("Value:") + " " + rtos(h) + "\n";2978Vector2 ih = animation->bezier_track_get_key_in_handle(track, key_idx);2979text += TTR("In-Handle:") + " " + String(ih) + "\n";2980Vector2 oh = animation->bezier_track_get_key_out_handle(track, key_idx);2981text += TTR("Out-Handle:") + " " + String(oh) + "\n";29822983String handle_mode;2984int hm = animation->bezier_track_get_key_handle_mode(track, key_idx);2985switch (hm) {2986case Animation::HANDLE_MODE_FREE: {2987handle_mode = TTR("Free", "Bezier Handle Mode");2988} break;2989case Animation::HANDLE_MODE_LINEAR: {2990handle_mode = TTR("Linear", "Bezier Handle Mode");2991} break;2992case Animation::HANDLE_MODE_BALANCED: {2993handle_mode = TTR("Balanced", "Bezier Handle Mode");2994} break;2995case Animation::HANDLE_MODE_MIRRORED: {2996handle_mode = TTR("Mirrored", "Bezier Handle Mode");2997} break;2998default: {2999// Unknown modes may occur when editing a file from a newer version of Godot.3000handle_mode = itos(hm);3001} break;3002}3003text += vformat(TTR("Handle mode: %s"), handle_mode);3004} break;3005case Animation::TYPE_AUDIO: {3006String stream_name = "null";3007Ref<Resource> stream = animation->audio_track_get_key_stream(track, key_idx);3008if (stream.is_valid()) {3009if (stream->get_path().is_resource_file()) {3010stream_name = stream->get_path().get_file();3011} else if (!stream->get_name().is_empty()) {3012stream_name = stream->get_name();3013} else {3014stream_name = stream->get_class();3015}3016}30173018text += TTR("Stream:") + " " + stream_name + "\n";3019float so = animation->audio_track_get_key_start_offset(track, key_idx);3020text += TTR("Start (s):") + " " + rtos(so) + "\n";3021float eo = animation->audio_track_get_key_end_offset(track, key_idx);3022text += TTR("End (s):") + " " + rtos(eo);3023} break;3024case Animation::TYPE_ANIMATION: {3025String name = animation->animation_track_get_key_animation(track, key_idx);3026text += TTR("Animation Clip:") + " " + name;3027} break;3028}3029return text;3030}3031}30323033return Control::get_tooltip(p_pos);3034}30353036void AnimationTrackEdit::gui_input(const Ref<InputEvent> &p_event) {3037ERR_FAIL_COND(p_event.is_null());30383039if (p_event->is_pressed()) {3040if (ED_IS_SHORTCUT("animation_editor/duplicate_selected_keys", p_event)) {3041if (!read_only) {3042emit_signal(SNAME("duplicate_request"), -1.0, false);3043}3044accept_event();3045}3046if (ED_IS_SHORTCUT("animation_editor/cut_selected_keys", p_event)) {3047if (!read_only) {3048emit_signal(SNAME("cut_request"));3049}3050accept_event();3051}3052if (ED_IS_SHORTCUT("animation_editor/copy_selected_keys", p_event)) {3053if (!read_only) {3054emit_signal(SNAME("copy_request"));3055}3056accept_event();3057}30583059if (ED_IS_SHORTCUT("animation_editor/paste_keys", p_event)) {3060if (!read_only) {3061emit_signal(SNAME("paste_request"), -1.0, false);3062}3063accept_event();3064}30653066if (ED_IS_SHORTCUT("animation_editor/delete_selection", p_event)) {3067if (!read_only) {3068emit_signal(SNAME("delete_request"));3069}3070accept_event();3071}3072}30733074Ref<InputEventMouseButton> mb = p_event;3075if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {3076Point2 pos = mb->get_position();3077bool no_mod_key_pressed = !mb->is_alt_pressed() && !mb->is_shift_pressed() && !mb->is_command_or_control_pressed();3078if (mb->is_double_click() && !moving_selection && no_mod_key_pressed) {3079int x = pos.x - timeline->get_name_limit();3080float ofs = x / timeline->get_zoom_scale() + timeline->get_value();3081emit_signal(SNAME("timeline_changed"), ofs, false);3082}30833084if (!read_only) {3085if (check_rect.has_point(pos)) {3086EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();3087undo_redo->create_action(TTR("Toggle Track Enabled"));3088undo_redo->add_do_method(animation.ptr(), "track_set_enabled", track, !animation->track_is_enabled(track));3089undo_redo->add_undo_method(animation.ptr(), "track_set_enabled", track, animation->track_is_enabled(track));3090undo_redo->commit_action();3091queue_redraw();3092accept_event();3093}30943095if (icon_rect.has_point(pos)) {3096EditorSelection *editor_selection = EditorNode::get_singleton()->get_editor_selection();3097editor_selection->clear();3098Node *n = root->get_node_or_null(node_path);3099if (n) {3100editor_selection->add_node(n);3101}3102}31033104// Don't overlap track keys if they start at 0.3105if (path_rect.has_point(pos + Size2(type_icon->get_width(), 0))) {3106clicking_on_name = true;3107accept_event();3108}31093110if (update_mode_rect.has_point(pos)) {3111if (!menu) {3112menu = memnew(PopupMenu);3113add_child(menu);3114menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationTrackEdit::_menu_selected));3115}3116menu->clear();3117if (animation->track_get_type(track) == Animation::TYPE_AUDIO) {3118menu->add_icon_item(get_editor_theme_icon(SNAME("UseBlendEnable")), TTR("Use Blend"), MENU_USE_BLEND_ENABLED);3119menu->add_icon_item(get_editor_theme_icon(SNAME("UseBlendDisable")), TTR("Don't Use Blend"), MENU_USE_BLEND_DISABLED);3120} else {3121menu->add_icon_item(get_editor_theme_icon(SNAME("TrackContinuous")), TTR("Continuous"), MENU_CALL_MODE_CONTINUOUS);3122menu->add_icon_item(get_editor_theme_icon(SNAME("TrackDiscrete")), TTR("Discrete"), MENU_CALL_MODE_DISCRETE);3123menu->add_icon_item(get_editor_theme_icon(SNAME("TrackCapture")), TTR("Capture"), MENU_CALL_MODE_CAPTURE);3124}3125menu->reset_size();31263127moving_selection_attempt = false;3128moving_selection = false;31293130Vector2 popup_pos = get_screen_position() + update_mode_rect.position + Vector2(0, update_mode_rect.size.height);3131menu->set_position(popup_pos);3132menu->popup();3133accept_event();3134}31353136if (interp_mode_rect.has_point(pos)) {3137if (!menu) {3138menu = memnew(PopupMenu);3139add_child(menu);3140menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationTrackEdit::_menu_selected));3141}3142menu->clear();3143menu->add_icon_item(get_editor_theme_icon(SNAME("InterpRaw")), TTR("Nearest"), MENU_INTERPOLATION_NEAREST);3144menu->add_icon_item(get_editor_theme_icon(SNAME("InterpLinear")), TTR("Linear"), MENU_INTERPOLATION_LINEAR);3145menu->add_icon_item(get_editor_theme_icon(SNAME("InterpCubic")), TTR("Cubic"), MENU_INTERPOLATION_CUBIC);3146// Check whether it is angle property.3147AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();3148if (ape) {3149AnimationPlayer *ap = ape->get_player();3150if (ap) {3151NodePath npath = animation->track_get_path(track);3152Node *a_ap_root_node = ap->get_node_or_null(ap->get_root_node());3153Node *nd = nullptr;3154// We must test that we have a valid a_ap_root_node before trying to access its content to init the nd Node.3155if (a_ap_root_node) {3156nd = a_ap_root_node->get_node_or_null(NodePath(npath.get_concatenated_names()));3157}3158if (nd) {3159StringName prop = npath.get_concatenated_subnames();3160PropertyInfo prop_info;3161ClassDB::get_property_info(nd->get_class(), prop, &prop_info);3162#ifdef DISABLE_DEPRECATED3163bool is_angle = prop_info.type == Variant::FLOAT && prop_info.hint_string.contains("radians_as_degrees");3164#else3165bool is_angle = prop_info.type == Variant::FLOAT && prop_info.hint_string.contains("radians");3166#endif // DISABLE_DEPRECATED3167if (is_angle) {3168menu->add_icon_item(get_editor_theme_icon(SNAME("InterpLinearAngle")), TTR("Linear Angle"), MENU_INTERPOLATION_LINEAR_ANGLE);3169menu->add_icon_item(get_editor_theme_icon(SNAME("InterpCubicAngle")), TTR("Cubic Angle"), MENU_INTERPOLATION_CUBIC_ANGLE);3170}3171}3172}3173}3174menu->reset_size();31753176moving_selection_attempt = false;3177moving_selection = false;31783179Vector2 popup_pos = get_screen_position() + interp_mode_rect.position + Vector2(0, interp_mode_rect.size.height);3180menu->set_position(popup_pos);3181menu->popup();3182accept_event();3183}31843185if (loop_wrap_rect.has_point(pos)) {3186if (!menu) {3187menu = memnew(PopupMenu);3188add_child(menu);3189menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationTrackEdit::_menu_selected));3190}3191menu->clear();3192menu->add_icon_item(get_editor_theme_icon(SNAME("InterpWrapClamp")), TTR("Clamp Loop Interp"), MENU_LOOP_CLAMP);3193menu->add_icon_item(get_editor_theme_icon(SNAME("InterpWrapLoop")), TTR("Wrap Loop Interp"), MENU_LOOP_WRAP);3194menu->reset_size();31953196moving_selection_attempt = false;3197moving_selection = false;31983199Vector2 popup_pos = get_screen_position() + loop_wrap_rect.position + Vector2(0, loop_wrap_rect.size.height);3200menu->set_position(popup_pos);3201menu->popup();3202accept_event();3203}32043205if (remove_rect.has_point(pos)) {3206emit_signal(SNAME("remove_request"), track);3207accept_event();3208return;3209}3210}32113212if (mb->is_command_or_control_pressed() && _lookup_key(hovering_key_idx)) {3213accept_event();3214return;3215}32163217if (_try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), true)) {3218accept_event();3219}3220}32213222if (!moving_selection && mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {3223Point2 pos = mb->get_position();3224if (pos.x >= timeline->get_name_limit() && pos.x <= get_size().width - timeline->get_buttons_width()) {3225// Can do something with menu too! show insert key.3226float offset = (pos.x - timeline->get_name_limit()) / timeline->get_zoom_scale();3227if (!read_only) {3228if (!menu) {3229menu = memnew(PopupMenu);3230add_child(menu);3231menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationTrackEdit::_menu_selected));3232}32333234bool selected = _try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), false);32353236menu->clear();3237if (animation->track_get_type(track) == Animation::TYPE_METHOD) {3238if (hovering_key_idx != -1) {3239lookup_key_idx = hovering_key_idx;3240menu->add_icon_item(get_editor_theme_icon(SNAME("Help")), vformat("%s (%s)", TTR("Go to Definition"), animation->method_track_get_name(track, lookup_key_idx)), MENU_KEY_LOOKUP);3241menu->add_separator();3242}3243}3244menu->add_icon_item(get_editor_theme_icon(SNAME("Key")), TTR("Insert Key..."), MENU_KEY_INSERT);3245if (selected || editor->is_selection_active()) {3246menu->add_separator();3247menu->add_icon_item(get_editor_theme_icon(SNAME("Duplicate")), TTR("Duplicate Key(s)"), MENU_KEY_DUPLICATE);3248menu->add_icon_item(get_editor_theme_icon(SNAME("ActionCut")), TTR("Cut Key(s)"), MENU_KEY_CUT);3249menu->add_icon_item(get_editor_theme_icon(SNAME("ActionCopy")), TTR("Copy Key(s)"), MENU_KEY_COPY);3250}3251if (editor->is_key_clipboard_active()) {3252menu->add_icon_item(get_editor_theme_icon(SNAME("ActionPaste")), TTR("Paste Key(s)"), MENU_KEY_PASTE);3253}3254if (selected || editor->is_selection_active()) {3255AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();3256if (ape) {3257AnimationPlayer *ap = ape->get_player();3258if (ap && editor->can_add_reset_key() && animation != ap->get_animation(SceneStringName(RESET))) {3259menu->add_separator();3260menu->add_icon_item(get_editor_theme_icon(SNAME("MoveUp")), TTR("Send Key(s) to RESET"), MENU_KEY_ADD_RESET);3261}3262}3263menu->add_separator();3264menu->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Delete Key(s)"), MENU_KEY_DELETE);3265}3266menu->reset_size();32673268menu->set_position(get_screen_position() + get_local_mouse_position());3269menu->popup();32703271insert_at_pos = offset + timeline->get_value();3272accept_event();3273}3274}3275}32763277if (mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && clicking_on_name) {3278if (!path) {3279path_popup = memnew(Popup);3280path_popup->set_wrap_controls(true);3281add_child(path_popup);3282path = memnew(LineEdit);3283path_popup->add_child(path);3284path->set_anchors_and_offsets_preset(PRESET_FULL_RECT);3285path->connect(SceneStringName(text_submitted), callable_mp(this, &AnimationTrackEdit::_path_submitted));3286}32873288path->set_text(String(animation->track_get_path(track)));3289const Vector2 theme_ofs = path->get_theme_stylebox(CoreStringName(normal), SNAME("LineEdit"))->get_offset();32903291moving_selection_attempt = false;3292moving_selection = false;32933294path_popup->set_position(get_screen_position() + path_rect.position - theme_ofs);3295path_popup->set_size(path_rect.size);3296path_popup->popup();3297path->grab_focus();3298path->set_caret_column(path->get_text().length());3299clicking_on_name = false;3300}33013302if (mb.is_valid() && moving_selection_attempt) {3303if (!mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {3304moving_selection_attempt = false;3305if (moving_selection && moving_selection_effective) {3306if (std::abs(editor->get_moving_selection_offset()) > CMP_EPSILON) {3307emit_signal(SNAME("move_selection_commit"));3308}3309} else if (select_single_attempt != -1) {3310emit_signal(SNAME("select_key"), select_single_attempt, true);3311}3312moving_selection = false;3313select_single_attempt = -1;3314}33153316if (moving_selection && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {3317moving_selection_attempt = false;3318moving_selection = false;3319emit_signal(SNAME("move_selection_cancel"));3320}3321}33223323Ref<InputEventMouseMotion> mm = p_event;3324if (mm.is_valid()) {3325const int previous_hovering_key_idx = hovering_key_idx;33263327command_or_control_pressed = mm->is_command_or_control_pressed();33283329// Hovering compressed keyframes for editing is not possible.3330if (!animation->track_is_compressed(track)) {3331const float scale = timeline->get_zoom_scale();3332const int limit = timeline->get_name_limit();3333const int limit_end = get_size().width - timeline->get_buttons_width();3334// Left Border including space occupied by keyframes on t=0.3335const int limit_start_hitbox = limit - type_icon->get_width();3336const Point2 pos = mm->get_position();33373338if (pos.x >= limit_start_hitbox && pos.x <= limit_end) {3339// Use the same logic as key selection to ensure that hovering accurately represents3340// which key will be selected when clicking.3341int key_idx = -1;3342float key_distance = 1e20;33433344hovering_key_idx = -1;33453346// Hovering should happen in the opposite order of drawing for more accurate overlap hovering.3347for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) {3348Rect2 rect = get_key_rect(i, scale);3349float offset = animation->track_get_key_time(track, i) - timeline->get_value();3350offset = offset * scale + limit;3351rect.position.x += offset;33523353if (rect.has_point(pos)) {3354if (is_key_selectable_by_distance()) {3355const float distance = Math::abs(offset - pos.x);3356if (key_idx == -1 || distance < key_distance) {3357key_idx = i;3358key_distance = distance;3359hovering_key_idx = i;3360}3361} else {3362// First one does it.3363hovering_key_idx = i;3364break;3365}3366}3367}33683369if (hovering_key_idx != previous_hovering_key_idx) {3370// Required to draw keyframe hover feedback on the correct keyframe.3371queue_redraw();3372}3373}3374}3375}33763377if (mm.is_valid() && mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && moving_selection_attempt) {3378if (!moving_selection) {3379moving_selection = true;3380emit_signal(SNAME("move_selection_begin"));3381}33823383float moving_begin_time = ((moving_selection_mouse_begin_x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value();3384float new_time = ((mm->get_position().x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value();3385float delta = new_time - moving_begin_time;3386float snapped_time = editor->snap_time(moving_selection_pivot + delta);33873388float offset = 0.0;3389if (std::abs(editor->get_moving_selection_offset()) > CMP_EPSILON || (snapped_time > moving_selection_pivot && delta > CMP_EPSILON) || (snapped_time < moving_selection_pivot && delta < -CMP_EPSILON)) {3390offset = snapped_time - moving_selection_pivot;3391moving_selection_effective = true;3392}33933394emit_signal(SNAME("move_selection"), offset);3395}3396}33973398bool AnimationTrackEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable) {3399if (!animation->track_is_compressed(track)) { // Selecting compressed keyframes for editing is not possible.3400float scale = timeline->get_zoom_scale();3401int limit = timeline->get_name_limit();3402int limit_end = get_size().width - timeline->get_buttons_width();3403// Left Border including space occupied by keyframes on t=0.3404int limit_start_hitbox = limit - type_icon->get_width();34053406if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) {3407int key_idx = -1;3408float key_distance = 1e20;34093410// Select should happen in the opposite order of drawing for more accurate overlap select.3411for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) {3412Rect2 rect = get_key_rect(i, scale);3413float offset = animation->track_get_key_time(track, i) - timeline->get_value();3414offset = offset * scale + limit;3415rect.position.x += offset;34163417if (rect.has_point(p_pos)) {3418if (is_key_selectable_by_distance()) {3419float distance = Math::abs(offset - p_pos.x);3420if (key_idx == -1 || distance < key_distance) {3421key_idx = i;3422key_distance = distance;3423}3424} else {3425// First one does it.3426key_idx = i;3427break;3428}3429}3430}34313432if (key_idx != -1) {3433if (p_aggregate) {3434if (editor->is_key_selected(track, key_idx)) {3435if (p_deselectable) {3436emit_signal(SNAME("deselect_key"), key_idx);3437moving_selection_pivot = 0.0f;3438moving_selection_mouse_begin_x = 0.0f;3439}3440} else {3441emit_signal(SNAME("select_key"), key_idx, false);3442moving_selection_attempt = true;3443moving_selection_effective = false;3444select_single_attempt = -1;3445moving_selection_pivot = animation->track_get_key_time(track, key_idx);3446moving_selection_mouse_begin_x = p_pos.x;3447}3448} else {3449if (!editor->is_key_selected(track, key_idx)) {3450emit_signal(SNAME("select_key"), key_idx, true);3451select_single_attempt = -1;3452} else {3453select_single_attempt = key_idx;3454}34553456moving_selection_attempt = true;3457moving_selection_effective = false;3458moving_selection_pivot = animation->track_get_key_time(track, key_idx);3459moving_selection_mouse_begin_x = p_pos.x;3460}34613462if (read_only) {3463moving_selection_attempt = false;3464moving_selection_pivot = 0.0f;3465moving_selection_mouse_begin_x = 0.0f;3466}3467return true;3468}3469}3470}3471return false;3472}34733474bool AnimationTrackEdit::_lookup_key(int p_key_idx) const {3475if (p_key_idx < 0 || p_key_idx >= animation->track_get_key_count(track)) {3476return false;3477}34783479if (animation->track_get_type(track) == Animation::TYPE_METHOD) {3480Node *target = root->get_node_or_null(animation->track_get_path(track));3481if (target) {3482StringName method = animation->method_track_get_name(track, p_key_idx);3483// First, check every script in the inheritance chain.3484bool found_in_script = false;3485Ref<Script> target_script_ref = target->get_script();3486Script *target_script = target_script_ref.ptr();3487while (target_script) {3488if (target_script->has_method(method)) {3489found_in_script = true;3490// Tell ScriptEditor to show the method's line.3491ScriptEditor::get_singleton()->script_goto_method(target_script, animation->method_track_get_name(track, p_key_idx));3492break;3493}3494target_script = target_script->get_base_script().ptr();3495}34963497if (!found_in_script) {3498// Not found in script, so it must be a native method.3499if (ClassDB::has_method(target->get_class_name(), method)) {3500// Show help page instead.3501ScriptEditor::get_singleton()->goto_help(vformat("class_method:%s:%s", target->get_class_name(), method));3502} else {3503// Still not found, which means the target doesn't have this method. Warn the user.3504WARN_PRINT_ED(TTR(vformat("Failed to lookup method: \"%s\"", method)));3505}3506}3507return true;3508}3509}3510return false;3511}35123513Variant AnimationTrackEdit::get_drag_data(const Point2 &p_point) {3514if (!clicking_on_name || (get_editor()->is_sorting_alphabetically() && !get_editor()->is_grouping_tracks())) {3515return Variant();3516}35173518Dictionary drag_data;3519drag_data["type"] = "animation_track";3520String base_path = String(animation->track_get_path(track));3521base_path = base_path.get_slicec(':', 0); // Remove sub-path.3522drag_data["group"] = base_path;3523drag_data["index"] = track;35243525Button *tb = memnew(Button);3526tb->set_flat(true);3527tb->set_text(path_cache);3528tb->set_button_icon(icon_cache);3529tb->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);3530tb->add_theme_constant_override("icon_max_width", get_theme_constant("class_icon_size", EditorStringName(Editor)));3531set_drag_preview(tb);35323533clicking_on_name = false;35343535return drag_data;3536}35373538bool AnimationTrackEdit::can_drop_data(const Point2 &p_point, const Variant &p_data) const {3539Dictionary d = p_data;3540if (!d.has("type")) {3541return false;3542}35433544String type = d["type"];3545if (type != "animation_track") {3546return false;3547}35483549// Don't allow moving tracks outside their groups.3550if (get_editor()->is_grouping_tracks()) {3551String base_path = String(animation->track_get_path(track));3552base_path = base_path.get_slicec(':', 0); // Remove sub-path.3553if (d["group"] != base_path) {3554return false;3555}3556}35573558if (p_point.y < get_size().height / 2) {3559dropping_at = -1;3560} else {3561dropping_at = 1;3562}35633564const_cast<AnimationTrackEdit *>(this)->queue_redraw();35653566return true;3567}35683569void AnimationTrackEdit::drop_data(const Point2 &p_point, const Variant &p_data) {3570Dictionary d = p_data;3571if (!d.has("type")) {3572return;3573}35743575String type = d["type"];3576if (type != "animation_track") {3577return;3578}35793580// Don't allow moving tracks outside their groups.3581if (get_editor()->is_grouping_tracks()) {3582String base_path = String(animation->track_get_path(track));3583base_path = base_path.get_slicec(':', 0); // Remove sub-path.3584if (d["group"] != base_path) {3585return;3586}3587}35883589int from_track = d["index"];35903591if (dropping_at < 0) {3592emit_signal(SNAME("dropped"), from_track, track);3593} else {3594emit_signal(SNAME("dropped"), from_track, track + 1);3595}3596}35973598void AnimationTrackEdit::_menu_selected(int p_index) {3599switch (p_index) {3600case MENU_CALL_MODE_CONTINUOUS:3601case MENU_CALL_MODE_DISCRETE:3602case MENU_CALL_MODE_CAPTURE: {3603Animation::UpdateMode update_mode = Animation::UpdateMode(p_index);3604EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();3605undo_redo->create_action(TTR("Change Animation Update Mode"));3606undo_redo->add_do_method(animation.ptr(), "value_track_set_update_mode", track, update_mode);3607undo_redo->add_undo_method(animation.ptr(), "value_track_set_update_mode", track, animation->value_track_get_update_mode(track));3608AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();3609if (ape) {3610undo_redo->add_do_method(ape, "_animation_update_key_frame");3611undo_redo->add_undo_method(ape, "_animation_update_key_frame");3612}3613undo_redo->commit_action();3614queue_redraw();36153616} break;3617case MENU_INTERPOLATION_NEAREST:3618case MENU_INTERPOLATION_LINEAR:3619case MENU_INTERPOLATION_CUBIC:3620case MENU_INTERPOLATION_LINEAR_ANGLE:3621case MENU_INTERPOLATION_CUBIC_ANGLE: {3622Animation::InterpolationType interp_mode = Animation::InterpolationType(p_index - MENU_INTERPOLATION_NEAREST);3623EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();3624undo_redo->create_action(TTR("Change Animation Interpolation Mode"));3625undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_type", track, interp_mode);3626undo_redo->add_undo_method(animation.ptr(), "track_set_interpolation_type", track, animation->track_get_interpolation_type(track));3627AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();3628if (ape) {3629undo_redo->add_do_method(ape, "_animation_update_key_frame");3630undo_redo->add_undo_method(ape, "_animation_update_key_frame");3631}3632undo_redo->commit_action();3633queue_redraw();3634} break;3635case MENU_LOOP_WRAP:3636case MENU_LOOP_CLAMP: {3637bool loop_wrap = p_index == MENU_LOOP_WRAP;3638EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();3639undo_redo->create_action(TTR("Change Animation Loop Mode"));3640undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_loop_wrap", track, loop_wrap);3641undo_redo->add_undo_method(animation.ptr(), "track_set_interpolation_loop_wrap", track, animation->track_get_interpolation_loop_wrap(track));3642AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();3643if (ape) {3644undo_redo->add_do_method(ape, "_animation_update_key_frame");3645undo_redo->add_undo_method(ape, "_animation_update_key_frame");3646}3647undo_redo->commit_action();3648queue_redraw();36493650} break;3651case MENU_KEY_INSERT: {3652emit_signal(SNAME("insert_key"), insert_at_pos);3653} break;3654case MENU_KEY_DUPLICATE: {3655emit_signal(SNAME("duplicate_request"), insert_at_pos, !editor->is_insert_at_current_time_enabled());3656} break;3657case MENU_KEY_CUT: {3658emit_signal(SNAME("cut_request"));3659} break;3660case MENU_KEY_COPY: {3661emit_signal(SNAME("copy_request"));3662} break;3663case MENU_KEY_PASTE: {3664emit_signal(SNAME("paste_request"), insert_at_pos, !editor->is_insert_at_current_time_enabled());3665} break;3666case MENU_KEY_ADD_RESET: {3667emit_signal(SNAME("create_reset_request"));3668} break;3669case MENU_KEY_DELETE: {3670emit_signal(SNAME("delete_request"));3671} break;3672case MENU_KEY_LOOKUP: {3673_lookup_key(lookup_key_idx);3674} break;3675case MENU_USE_BLEND_ENABLED:3676case MENU_USE_BLEND_DISABLED: {3677bool use_blend = p_index == MENU_USE_BLEND_ENABLED;3678EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();3679undo_redo->create_action(TTR("Change Animation Use Blend"));3680undo_redo->add_do_method(animation.ptr(), "audio_track_set_use_blend", track, use_blend);3681undo_redo->add_undo_method(animation.ptr(), "audio_track_set_use_blend", track, animation->audio_track_is_use_blend(track));3682undo_redo->commit_action();3683queue_redraw();3684} break;3685}3686}36873688void AnimationTrackEdit::cancel_drop() {3689if (dropping_at != 0) {3690dropping_at = 0;3691queue_redraw();3692}3693}36943695void AnimationTrackEdit::set_in_group(bool p_enable) {3696in_group = p_enable;3697queue_redraw();3698}36993700void AnimationTrackEdit::append_to_selection(const Rect2 &p_box, bool p_deselection) {3701if (animation->track_is_compressed(track)) {3702return; // Compressed keyframes can't be edited3703}3704// Left Border including space occupied by keyframes on t=0.3705int limit_start_hitbox = timeline->get_name_limit() - type_icon->get_width();3706Rect2 select_rect(limit_start_hitbox, 0, get_size().width - timeline->get_name_limit() - timeline->get_buttons_width(), get_size().height);3707select_rect = select_rect.intersection(p_box);37083709// Select should happen in the opposite order of drawing for more accurate overlap select.3710for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) {3711Rect2 rect = const_cast<AnimationTrackEdit *>(this)->get_key_rect(i, timeline->get_zoom_scale());3712float offset = animation->track_get_key_time(track, i) - timeline->get_value();3713offset = offset * timeline->get_zoom_scale() + timeline->get_name_limit();3714rect.position.x += offset;37153716if (select_rect.intersects(rect)) {3717if (p_deselection) {3718emit_signal(SNAME("deselect_key"), i);3719} else {3720emit_signal(SNAME("select_key"), i, false);3721}3722}3723}3724}37253726void AnimationTrackEdit::_bind_methods() {3727ADD_SIGNAL(MethodInfo("timeline_changed", PropertyInfo(Variant::FLOAT, "position"), PropertyInfo(Variant::BOOL, "timeline_only")));3728ADD_SIGNAL(MethodInfo("remove_request", PropertyInfo(Variant::INT, "track")));3729ADD_SIGNAL(MethodInfo("dropped", PropertyInfo(Variant::INT, "from_track"), PropertyInfo(Variant::INT, "to_track")));3730ADD_SIGNAL(MethodInfo("insert_key", PropertyInfo(Variant::FLOAT, "offset")));3731ADD_SIGNAL(MethodInfo("select_key", PropertyInfo(Variant::INT, "index"), PropertyInfo(Variant::BOOL, "single")));3732ADD_SIGNAL(MethodInfo("deselect_key", PropertyInfo(Variant::INT, "index")));37333734ADD_SIGNAL(MethodInfo("move_selection_begin"));3735ADD_SIGNAL(MethodInfo("move_selection", PropertyInfo(Variant::FLOAT, "offset")));3736ADD_SIGNAL(MethodInfo("move_selection_commit"));3737ADD_SIGNAL(MethodInfo("move_selection_cancel"));37383739ADD_SIGNAL(MethodInfo("duplicate_request", PropertyInfo(Variant::FLOAT, "offset"), PropertyInfo(Variant::BOOL, "is_offset_valid")));3740ADD_SIGNAL(MethodInfo("create_reset_request"));3741ADD_SIGNAL(MethodInfo("copy_request"));3742ADD_SIGNAL(MethodInfo("cut_request"));3743ADD_SIGNAL(MethodInfo("paste_request", PropertyInfo(Variant::FLOAT, "offset"), PropertyInfo(Variant::BOOL, "is_offset_valid")));3744ADD_SIGNAL(MethodInfo("delete_request"));3745}37463747AnimationTrackEdit::AnimationTrackEdit() {3748play_position = memnew(Control);3749play_position->set_mouse_filter(MOUSE_FILTER_PASS);3750add_child(play_position);3751play_position->set_anchors_and_offsets_preset(PRESET_FULL_RECT);3752play_position->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEdit::_play_position_draw));3753set_focus_mode(FOCUS_CLICK);3754set_mouse_filter(MOUSE_FILTER_PASS); // Scroll has to work too for selection.3755}37563757//////////////////////////////////////37583759AnimationTrackEdit *AnimationTrackEditPlugin::create_value_track_edit(Object *p_object, Variant::Type p_type, const String &p_property, PropertyHint p_hint, const String &p_hint_string, int p_usage) {3760if (get_script_instance()) {3761return Object::cast_to<AnimationTrackEdit>(get_script_instance()->call("create_value_track_edit", p_object, p_type, p_property, p_hint, p_hint_string, p_usage));3762}3763return nullptr;3764}37653766AnimationTrackEdit *AnimationTrackEditPlugin::create_audio_track_edit() {3767if (get_script_instance()) {3768return Object::cast_to<AnimationTrackEdit>(get_script_instance()->call("create_audio_track_edit").operator Object *());3769}3770return nullptr;3771}37723773AnimationTrackEdit *AnimationTrackEditPlugin::create_animation_track_edit(Object *p_object) {3774if (get_script_instance()) {3775return Object::cast_to<AnimationTrackEdit>(get_script_instance()->call("create_animation_track_edit", p_object).operator Object *());3776}3777return nullptr;3778}37793780///////////////////////////////////////37813782void AnimationTrackEditGroup::_notification(int p_what) {3783switch (p_what) {3784case NOTIFICATION_THEME_CHANGED: {3785icon_size = Vector2(1, 1) * get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor));3786} break;37873788case NOTIFICATION_DRAW: {3789const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));3790const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));3791Color color = get_theme_color(SceneStringName(font_color), SNAME("Label"));37923793const Ref<StyleBox> &stylebox_header = get_theme_stylebox(SNAME("header"), SNAME("AnimationTrackEditGroup"));3794float v_margin_offset = stylebox_header->get_content_margin(SIDE_TOP) - stylebox_header->get_content_margin(SIDE_BOTTOM);37953796const Color h_line_color = get_theme_color(SNAME("h_line_color"), SNAME("AnimationTrackEditGroup"));3797const Color v_line_color = get_theme_color(SNAME("v_line_color"), SNAME("AnimationTrackEditGroup"));3798const int h_separation = get_theme_constant(SNAME("h_separation"), SNAME("AnimationTrackEditGroup"));37993800const Ref<StyleBox> &stylebox_hover = get_theme_stylebox(SceneStringName(hover), SNAME("AnimationTrackEditGroup"));38013802if (root) {3803Node *n = root->get_node_or_null(node);3804if (n && EditorNode::get_singleton()->get_editor_selection()->is_selected(n)) {3805color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));3806}3807}38083809draw_style_box(stylebox_header, Rect2(Point2(), get_size()));38103811if (hovered) {3812// Draw hover feedback for AnimationTrackEditGroup.3813// Add a limit to just show hover over portion with text.3814int limit = timeline->get_name_limit();3815draw_style_box(stylebox_hover, Rect2(Point2(1 * EDSCALE, 0), Size2(limit - 1 * EDSCALE, get_size().height)));3816}38173818int limit = timeline->get_name_limit();3819int limit_end = get_size().width - timeline->get_buttons_width();38203821// Unavailable timeline.38223823{3824int px = (editor->get_current_animation()->get_length() - timeline->get_value()) * timeline->get_zoom_scale() + timeline->get_name_limit();3825px = MAX(px, timeline->get_name_limit());3826Rect2 rect = Rect2(px, 0, limit_end - px, get_size().height);3827if (rect.size.width > 0) {3828draw_rect(rect, Color(0, 0, 0, 0.2));3829}3830}38313832// Section preview.38333834{3835float scale = timeline->get_zoom_scale();38363837PackedStringArray section = editor->get_selected_section();3838if (section.size() == 2) {3839StringName start_marker = section[0];3840StringName end_marker = section[1];3841double start_time = editor->get_current_animation()->get_marker_time(start_marker);3842double end_time = editor->get_current_animation()->get_marker_time(end_marker);38433844AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();3845// When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section.3846if (editor->is_marker_moving_selection() && !(player && player->is_playing())) {3847start_time += editor->get_marker_moving_selection_offset();3848end_time += editor->get_marker_moving_selection_offset();3849}38503851if (start_time < editor->get_current_animation()->get_length() && end_time >= 0) {3852float start_ofs = MAX(0, start_time) - timeline->get_value();3853float end_ofs = MIN(editor->get_current_animation()->get_length(), end_time) - timeline->get_value();3854start_ofs = start_ofs * scale + limit;3855end_ofs = end_ofs * scale + limit;3856start_ofs = MAX(start_ofs, limit);3857end_ofs = MIN(end_ofs, limit_end);3858Rect2 rect;3859rect.set_position(Vector2(start_ofs, 0));3860rect.set_size(Vector2(end_ofs - start_ofs, get_size().height));38613862draw_rect(rect, Color(1, 0.1, 0.1, 0.2));3863}3864}3865}38663867// Marker overlays.38683869{3870float scale = timeline->get_zoom_scale();3871PackedStringArray markers = editor->get_current_animation()->get_marker_names();3872for (const StringName marker : markers) {3873double time = editor->get_current_animation()->get_marker_time(marker);3874if (editor->is_marker_selected(marker) && editor->is_marker_moving_selection()) {3875time += editor->get_marker_moving_selection_offset();3876}3877if (time >= 0) {3878float offset = time - timeline->get_value();3879offset = offset * scale + limit;3880if (offset >= timeline->get_name_limit() && offset < limit_end) {3881Color marker_color = editor->get_current_animation()->get_marker_color(marker);3882marker_color.a = 0.2;3883draw_line(Point2(offset, 0), Point2(offset, get_size().height), marker_color, Math::round(EDSCALE));3884}3885}3886}3887}38883889draw_line(Point2(), Point2(get_size().width, 0), h_line_color, Math::round(EDSCALE));3890draw_line(Point2(timeline->get_name_limit(), 0), Point2(timeline->get_name_limit(), get_size().height), v_line_color, Math::round(EDSCALE));3891draw_line(Point2(get_size().width - timeline->get_buttons_width(), 0), Point2(get_size().width - timeline->get_buttons_width(), get_size().height), v_line_color, Math::round(EDSCALE));38923893int ofs = stylebox_header->get_margin(SIDE_LEFT);3894draw_texture_rect(icon, Rect2(Point2(ofs, (get_size().height - icon_size.y) / 2 + v_margin_offset).round(), icon_size));3895ofs += h_separation + icon_size.x;3896draw_string(font, Point2(ofs, (get_size().height - font->get_height(font_size)) / 2 + font->get_ascent(font_size) + v_margin_offset).round(), node_name, HORIZONTAL_ALIGNMENT_LEFT, timeline->get_name_limit() - ofs, font_size, color);38973898int px = (-timeline->get_value() + timeline->get_play_position()) * timeline->get_zoom_scale() + timeline->get_name_limit();3899if (px >= timeline->get_name_limit() && px < limit_end) {3900const Color accent = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));3901draw_line(Point2(px, 0), Point2(px, get_size().height), accent, Math::round(2 * EDSCALE));3902}3903} break;39043905case NOTIFICATION_MOUSE_EXIT: {3906if (hovered) {3907hovered = false;3908// When the mouse cursor exits the AnimationTrackEditGroup, we're no longer hovering the group.3909queue_redraw();3910}3911} break;3912}3913}39143915void AnimationTrackEditGroup::gui_input(const Ref<InputEvent> &p_event) {3916ERR_FAIL_COND(p_event.is_null());39173918Ref<InputEventMouseButton> mb = p_event;3919if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {3920Point2 pos = mb->get_position();3921Rect2 node_name_rect = Rect2(0, 0, timeline->get_name_limit(), get_size().height);39223923if (node_name_rect.has_point(pos)) {3924EditorSelection *editor_selection = EditorNode::get_singleton()->get_editor_selection();3925editor_selection->clear();3926Node *n = root->get_node_or_null(node);3927if (n) {3928editor_selection->add_node(n);3929}3930}3931}3932Ref<InputEventMouseMotion> mm = p_event;3933if (mm.is_valid()) {3934Point2 pos = mm->get_position();3935Rect2 node_name_rect = Rect2(0, 0, timeline->get_name_limit(), get_size().height);39363937bool was_hovered = hovered;3938hovered = node_name_rect.has_point(pos);39393940if (was_hovered != hovered) {3941queue_redraw();3942}3943}3944}39453946void AnimationTrackEditGroup::set_type_and_name(const Ref<Texture2D> &p_type, const String &p_name, const NodePath &p_node) {3947icon = p_type;3948node_name = p_name;3949node = p_node;3950queue_redraw();3951update_minimum_size();3952}39533954Size2 AnimationTrackEditGroup::get_minimum_size() const {3955const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));3956const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));3957const int separation = get_theme_constant(SNAME("v_separation"), SNAME("ItemList"));39583959const Ref<StyleBox> &header_style = get_theme_stylebox(SNAME("header"), SNAME("AnimationTrackEditGroup"));3960const int content_margin = header_style->get_content_margin(SIDE_TOP) + header_style->get_content_margin(SIDE_BOTTOM);39613962return Vector2(0, MAX(font->get_height(font_size), icon_size.y) + separation + content_margin);3963}39643965String AnimationTrackEditGroup::get_node_name() const {3966return node_name;3967}39683969void AnimationTrackEditGroup::set_timeline(AnimationTimelineEdit *p_timeline) {3970timeline = p_timeline;3971timeline->connect("zoom_changed", callable_mp(this, &AnimationTrackEditGroup::_zoom_changed));3972timeline->connect("name_limit_changed", callable_mp(this, &AnimationTrackEditGroup::_zoom_changed));3973}39743975void AnimationTrackEditGroup::set_root(Node *p_root) {3976root = p_root;3977queue_redraw();3978}39793980void AnimationTrackEditGroup::set_editor(AnimationTrackEditor *p_editor) {3981editor = p_editor;3982}39833984void AnimationTrackEditGroup::_zoom_changed() {3985queue_redraw();3986}39873988AnimationTrackEditGroup::AnimationTrackEditGroup() {3989set_mouse_filter(MOUSE_FILTER_PASS);3990}39913992//////////////////////////////////////39933994void AnimationTrackEditor::add_track_edit_plugin(const Ref<AnimationTrackEditPlugin> &p_plugin) {3995if (track_edit_plugins.has(p_plugin)) {3996return;3997}3998track_edit_plugins.push_back(p_plugin);3999}40004001void AnimationTrackEditor::remove_track_edit_plugin(const Ref<AnimationTrackEditPlugin> &p_plugin) {4002track_edit_plugins.erase(p_plugin);4003}40044005void AnimationTrackEditor::set_animation(const Ref<Animation> &p_anim, bool p_read_only) {4006if (animation != p_anim) {4007for (int i = 0; i < track_edits.size(); i++) {4008if (track_edits[i]->has_focus()) {4009track_edits[i]->release_focus();4010break;4011}4012}4013}4014if (animation.is_valid()) {4015animation->disconnect_changed(callable_mp(this, &AnimationTrackEditor::_animation_changed));4016_clear_selection();4017}4018animation = p_anim;4019read_only = p_read_only;4020timeline->set_animation(p_anim, read_only);40214022marker_edit->set_animation(p_anim, read_only);4023marker_edit->set_play_position(timeline->get_play_position());40244025_check_bezier_exist();4026_update_tracks();40274028if (animation.is_valid()) {4029animation->connect_changed(callable_mp(this, &AnimationTrackEditor::_animation_changed));40304031hscroll->show();4032edit->set_disabled(read_only);4033step->set_block_signals(true);40344035_update_step_spinbox();4036step->set_block_signals(false);4037step->set_read_only(false);4038snap_keys->set_disabled(false);4039snap_timeline->set_disabled(false);4040insert_at_current_time->set_disabled(false);4041fps_compat->set_disabled(false);4042snap_mode->set_disabled(false);4043auto_fit->set_disabled(false);4044auto_fit_bezier->set_disabled(false);40454046imported_anim_warning->hide();4047for (int i = 0; i < animation->get_track_count(); i++) {4048if (animation->track_is_imported(i)) {4049imported_anim_warning->show();4050break;4051}4052}40534054if (bezier_edit->is_visible()) {4055for (int i = 0; i < animation->get_track_count(); ++i) {4056if (animation->track_get_type(i) == Animation::TrackType::TYPE_BEZIER) {4057_bezier_edit(i);4058break;4059}4060}4061}4062} else {4063hscroll->hide();4064edit->set_disabled(true);4065step->set_block_signals(true);4066step->set_value(0);4067step->set_block_signals(false);4068step->set_read_only(true);4069snap_keys->set_disabled(true);4070snap_timeline->set_disabled(true);4071insert_at_current_time->set_disabled(true);4072fps_compat->set_disabled(true);4073snap_mode->set_disabled(true);4074bezier_edit_icon->set_disabled(true);4075auto_fit->set_disabled(true);4076auto_fit_bezier->set_disabled(true);4077}4078}40794080void AnimationTrackEditor::_check_bezier_exist() {4081bool is_exist = false;4082if (animation.is_valid()) {4083for (int i = 0; i < animation->get_track_count(); i++) {4084if (animation->track_get_type(i) == Animation::TrackType::TYPE_BEZIER) {4085is_exist = true;4086break;4087}4088}4089}4090if (is_exist) {4091bezier_edit_icon->set_disabled(false);4092} else {4093if (bezier_edit->is_visible()) {4094_cancel_bezier_edit();4095}4096bezier_edit_icon->set_disabled(true);4097}4098}40994100Ref<Animation> AnimationTrackEditor::get_current_animation() const {4101return animation;4102}41034104void AnimationTrackEditor::_root_removed() {4105root = nullptr;4106}41074108void AnimationTrackEditor::set_root(Node *p_root) {4109if (root) {4110root->disconnect(SceneStringName(tree_exiting), callable_mp(this, &AnimationTrackEditor::_root_removed));4111}41124113root = p_root;41144115if (root) {4116root->connect(SceneStringName(tree_exiting), callable_mp(this, &AnimationTrackEditor::_root_removed), CONNECT_ONE_SHOT);4117}41184119_update_tracks();4120}41214122Node *AnimationTrackEditor::get_root() const {4123return root;4124}41254126void AnimationTrackEditor::update_keying() {4127bool keying_enabled = false;41284129EditorSelectionHistory *editor_history = EditorNode::get_singleton()->get_editor_selection_history();4130if (is_visible_in_tree() && animation.is_valid() && editor_history->get_path_size() > 0) {4131Object *obj = ObjectDB::get_instance(editor_history->get_path_object(0));4132keying_enabled = Object::cast_to<Node>(obj) != nullptr || Object::cast_to<MultiNodeEdit>(obj) != nullptr;4133}41344135if (keying_enabled == keying) {4136return;4137}41384139keying = keying_enabled;41404141emit_signal(SNAME("keying_changed"));4142}41434144bool AnimationTrackEditor::has_keying() const {4145return keying;4146}41474148Dictionary AnimationTrackEditor::get_state() const {4149Dictionary state;4150state["fps_mode"] = timeline->is_using_fps();4151state["fps_compat"] = fps_compat->is_pressed();4152state["zoom"] = zoom->get_value();4153state["offset"] = timeline->get_value();4154state["v_scroll"] = scroll->get_v_scroll_bar()->get_value();4155return state;4156}41574158void AnimationTrackEditor::set_state(const Dictionary &p_state) {4159if (p_state.has("fps_mode")) {4160bool fps_mode = p_state["fps_mode"];4161if (fps_mode) {4162snap_mode->select(1);4163} else {4164snap_mode->select(0);4165}4166_snap_mode_changed(snap_mode->get_selected());4167}41684169if (p_state.has("fps_compat")) {4170fps_compat->set_pressed(p_state["fps_compat"]);4171}41724173if (p_state.has("zoom")) {4174zoom->set_value(p_state["zoom"]);4175}41764177if (p_state.has("offset")) {4178timeline->set_value(p_state["offset"]);4179}41804181if (p_state.has("v_scroll")) {4182scroll->get_v_scroll_bar()->set_value(p_state["v_scroll"]);4183}4184}41854186void AnimationTrackEditor::clear() {4187snap_mode->select(EDITOR_GET("editors/animation/default_fps_mode"));4188_snap_mode_changed(snap_mode->get_selected());4189fps_compat->set_pressed(EDITOR_GET("editors/animation/default_fps_compatibility"));4190zoom->set_value(1.0);4191timeline->set_value(0);4192scroll->get_v_scroll_bar()->set_value(0);4193}41944195void AnimationTrackEditor::cleanup() {4196set_animation(Ref<Animation>(), read_only);4197}41984199void AnimationTrackEditor::_name_limit_changed() {4200_redraw_tracks();4201}42024203void AnimationTrackEditor::_timeline_changed(float p_new_pos, bool p_timeline_only) {4204emit_signal(SNAME("timeline_changed"), p_new_pos, p_timeline_only, false);4205}42064207void AnimationTrackEditor::_track_remove_request(int p_track) {4208_animation_track_remove_request(p_track, animation);4209}42104211void AnimationTrackEditor::_animation_track_remove_request(int p_track, Ref<Animation> p_from_animation) {4212if (p_from_animation->track_is_compressed(p_track)) {4213EditorNode::get_singleton()->show_warning(TTR("Compressed tracks can't be edited or removed. Re-import the animation with compression disabled in order to edit."));4214return;4215}4216int idx = p_track;4217if (idx >= 0 && idx < p_from_animation->get_track_count()) {4218EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();4219undo_redo->create_action(TTR("Remove Anim Track"), UndoRedo::MERGE_DISABLE, p_from_animation.ptr());42204221// Remove corresponding reset tracks if they are no longer needed.4222AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();4223if (player->has_animation(SceneStringName(RESET))) {4224Ref<Animation> reset = player->get_animation(SceneStringName(RESET));4225if (reset != p_from_animation) {4226for (int i = 0; i < reset->get_track_count(); i++) {4227if (reset->track_get_path(i) == p_from_animation->track_get_path(p_track)) {4228// Check if the reset track isn't used by other animations.4229bool used = false;4230List<StringName> animation_list;4231player->get_animation_list(&animation_list);42324233for (const StringName &anim_name : animation_list) {4234Ref<Animation> anim = player->get_animation(anim_name);4235if (anim == p_from_animation || anim == reset) {4236continue;4237}42384239for (int j = 0; j < anim->get_track_count(); j++) {4240if (anim->track_get_path(j) == reset->track_get_path(i)) {4241used = true;4242break;4243}4244}42454246if (used) {4247break;4248}4249}42504251if (!used) {4252_animation_track_remove_request(i, reset);4253}4254break;4255}4256}4257}4258}42594260undo_redo->add_do_method(this, "_clear_selection", false);4261undo_redo->add_do_method(p_from_animation.ptr(), "remove_track", idx);4262undo_redo->add_undo_method(p_from_animation.ptr(), "add_track", p_from_animation->track_get_type(idx), idx);4263undo_redo->add_undo_method(p_from_animation.ptr(), "track_set_path", idx, p_from_animation->track_get_path(idx));42644265// TODO interpolation.4266for (int i = 0; i < p_from_animation->track_get_key_count(idx); i++) {4267Variant v = p_from_animation->track_get_key_value(idx, i);4268float time = p_from_animation->track_get_key_time(idx, i);4269float trans = p_from_animation->track_get_key_transition(idx, i);42704271undo_redo->add_undo_method(p_from_animation.ptr(), "track_insert_key", idx, time, v);4272undo_redo->add_undo_method(p_from_animation.ptr(), "track_set_key_transition", idx, i, trans);4273}42744275undo_redo->add_undo_method(p_from_animation.ptr(), "track_set_interpolation_type", idx, p_from_animation->track_get_interpolation_type(idx));4276if (p_from_animation->track_get_type(idx) == Animation::TYPE_VALUE) {4277undo_redo->add_undo_method(p_from_animation.ptr(), "value_track_set_update_mode", idx, p_from_animation->value_track_get_update_mode(idx));4278}4279if (animation->track_get_type(idx) == Animation::TYPE_AUDIO) {4280undo_redo->add_undo_method(animation.ptr(), "audio_track_set_use_blend", idx, animation->audio_track_is_use_blend(idx));4281}42824283undo_redo->commit_action();4284}4285}42864287void AnimationTrackEditor::_track_grab_focus(int p_track) {4288// Don't steal focus if not working with the track editor.4289if (Object::cast_to<AnimationTrackEdit>(get_viewport()->gui_get_focus_owner())) {4290for (int i = 0; i < track_edits.size(); i++) {4291if (track_edits[i]->get_track() == p_track) {4292track_edits[i]->grab_focus();4293}4294}4295}4296}42974298void AnimationTrackEditor::set_anim_pos(float p_pos) {4299timeline->set_play_position(p_pos);4300marker_edit->set_play_position(p_pos);4301for (int i = 0; i < track_edits.size(); i++) {4302track_edits[i]->set_play_position(p_pos);4303}4304_redraw_groups();4305bezier_edit->set_play_position(p_pos);4306emit_signal(SNAME("timeline_changed"), p_pos, true, true);4307}43084309static bool track_type_is_resettable(Animation::TrackType p_type) {4310switch (p_type) {4311case Animation::TYPE_VALUE:4312[[fallthrough]];4313case Animation::TYPE_BLEND_SHAPE:4314[[fallthrough]];4315case Animation::TYPE_BEZIER:4316[[fallthrough]];4317case Animation::TYPE_POSITION_3D:4318[[fallthrough]];4319case Animation::TYPE_ROTATION_3D:4320[[fallthrough]];4321case Animation::TYPE_SCALE_3D:4322return true;4323default:4324return false;4325}4326}43274328bool AnimationTrackEditor::is_read_only() const {4329return read_only;4330}43314332void AnimationTrackEditor::make_insert_queue() {4333insert_data.clear();4334insert_queue = true;4335}43364337void AnimationTrackEditor::commit_insert_queue() {4338bool reset_allowed = true;4339AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();4340if (is_global_library_read_only() || (player->has_animation(SceneStringName(RESET)) && player->get_animation(SceneStringName(RESET)) == animation)) {4341// Avoid messing with the reset animation itself.4342reset_allowed = false;4343} else {4344bool some_resettable = false;4345for (const AnimationTrackEditor::InsertData &E : insert_data) {4346if (track_type_is_resettable(E.type)) {4347some_resettable = true;4348break;4349}4350}4351if (!some_resettable) {4352reset_allowed = false;4353}4354}43554356// Organize insert data.4357int num_tracks = 0;4358String last_track_query;4359bool all_bezier = true;4360for (const AnimationTrackEditor::InsertData &E : insert_data) {4361if (E.type != Animation::TYPE_VALUE && E.type != Animation::TYPE_BEZIER) {4362all_bezier = false;4363}43644365if (E.track_idx == -1) {4366++num_tracks;4367last_track_query = E.query;4368}43694370if (E.type != Animation::TYPE_VALUE) {4371continue;4372}43734374switch (E.value.get_type()) {4375case Variant::INT:4376case Variant::FLOAT:4377case Variant::VECTOR2:4378case Variant::VECTOR3:4379case Variant::QUATERNION:4380case Variant::PLANE:4381case Variant::COLOR: {4382// Valid.4383} break;4384default: {4385all_bezier = false;4386}4387}4388}43894390// Skip the confirmation dialog if the user holds Shift while clicking the key icon.4391// If `confirm_insert_track` editor setting is disabled, the behavior is reversed.4392bool confirm_insert = EDITOR_GET("editors/animation/confirm_insert_track");4393if ((Input::get_singleton()->is_key_pressed(Key::SHIFT) != confirm_insert) && num_tracks > 0) {4394String dialog_text;43954396// Potentially a new key, does not exist.4397if (num_tracks == 1) {4398// TRANSLATORS: %s will be replaced by a phrase describing the target of track.4399dialog_text = vformat(TTR("Create new track for %s and insert key?"), last_track_query);4400} else {4401dialog_text = vformat(TTR("Create %d new tracks and insert keys?"), num_tracks);4402}44034404if (confirm_insert) {4405dialog_text += +"\n\n" + TTR("Hold Shift when clicking the key icon to skip this dialog.");4406}4407insert_confirm_text->set_text(dialog_text);44084409insert_confirm_bezier->set_visible(all_bezier);4410insert_confirm_reset->set_visible(reset_allowed);44114412insert_confirm->set_ok_button_text(TTR("Create"));4413insert_confirm->popup_centered();4414} else {4415_insert_track(reset_allowed && EDITOR_GET("editors/animation/default_create_reset_tracks"), all_bezier && EDITOR_GET("editors/animation/default_create_bezier_tracks"));4416}44174418insert_queue = false;4419}44204421void AnimationTrackEditor::_query_insert(const InsertData &p_id) {4422ERR_FAIL_COND_EDMSG(read_only, "Animation is read-only."); // Should have been prevented by dialog, but for safety.44234424if (!insert_queue) {4425insert_data.clear();4426}44274428for (const InsertData &E : insert_data) {4429// Prevent insertion of multiple tracks.4430if (E.path == p_id.path && E.type == p_id.type) {4431return; // Already inserted a track this frame.4432}4433}44344435insert_data.push_back(p_id);44364437// Without queue, commit immediately.4438if (!insert_queue) {4439commit_insert_queue();4440}4441}44424443void AnimationTrackEditor::_insert_track(bool p_reset_wanted, bool p_create_beziers) {4444EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();4445undo_redo->create_action(TTR("Animation Insert Key"));44464447Ref<Animation> reset_anim;4448if (p_reset_wanted) {4449reset_anim = _create_and_get_reset_animation();4450}44514452TrackIndices next_tracks(animation.ptr(), reset_anim.ptr());4453bool advance = false;4454while (insert_data.size()) {4455if (insert_data.front()->get().advance) {4456advance = true;4457}4458next_tracks = _confirm_insert(insert_data.front()->get(), next_tracks, p_reset_wanted, reset_anim, p_create_beziers);4459insert_data.pop_front();4460}44614462undo_redo->commit_action();44634464if (advance) {4465_edit_menu_pressed(EDIT_GOTO_NEXT_STEP_TIMELINE_ONLY);4466}4467}44684469void AnimationTrackEditor::insert_transform_key(Node3D *p_node, const String &p_sub, const Animation::TrackType p_type, const Variant &p_value) {4470if (read_only) {4471popup_read_only_dialog();4472return;4473}44744475ERR_FAIL_NULL(root);4476ERR_FAIL_COND_MSG(4477(p_type != Animation::TYPE_POSITION_3D && p_type != Animation::TYPE_ROTATION_3D && p_type != Animation::TYPE_SCALE_3D),4478"Track type must be Position/Rotation/Scale 3D.");4479if (!keying) {4480return;4481}4482if (animation.is_null()) {4483return;4484}44854486// Let's build a node path.4487String path = String(root->get_path_to(p_node, true));4488if (!p_sub.is_empty()) {4489path += ":" + p_sub;4490}44914492NodePath np = path;44934494int track_idx = -1;44954496for (int i = 0; i < animation->get_track_count(); i++) {4497if (animation->track_get_path(i) != np) {4498continue;4499}4500if (animation->track_get_type(i) != p_type) {4501continue;4502}4503track_idx = i;4504}45054506InsertData id;4507id.path = np;4508// TRANSLATORS: This describes the target of new animation track, will be inserted into another string.4509id.query = vformat(TTR("node '%s'"), p_node->get_name());4510id.advance = false;4511id.track_idx = track_idx;4512id.value = p_value;4513id.type = p_type;4514_query_insert(id);4515}45164517bool AnimationTrackEditor::has_track(Node3D *p_node, const String &p_sub, const Animation::TrackType p_type) {4518ERR_FAIL_NULL_V(root, false);4519if (!keying) {4520return false;4521}4522if (animation.is_null()) {4523return false;4524}45254526// Let's build a node path.4527String path = String(root->get_path_to(p_node, true));4528if (!p_sub.is_empty()) {4529path += ":" + p_sub;4530}45314532int track_id = animation->find_track(path, p_type);4533if (track_id >= 0) {4534return true;4535}4536return false;4537}45384539void AnimationTrackEditor::_insert_animation_key(NodePath p_path, const Variant &p_value) {4540if (read_only) {4541popup_read_only_dialog();4542return;4543}45444545String path = String(p_path);45464547// Animation property is a special case, always creates an animation track.4548for (int i = 0; i < animation->get_track_count(); i++) {4549String np = String(animation->track_get_path(i));45504551if (path == np && animation->track_get_type(i) == Animation::TYPE_ANIMATION) {4552// Exists.4553InsertData id;4554id.path = path;4555id.track_idx = i;4556id.value = p_value;4557id.type = Animation::TYPE_ANIMATION;4558// TRANSLATORS: This describes the target of new animation track, will be inserted into another string.4559id.query = TTR("animation");4560id.advance = false;4561// Dialog insert.4562_query_insert(id);4563return;4564}4565}45664567InsertData id;4568id.path = path;4569id.track_idx = -1;4570id.value = p_value;4571id.type = Animation::TYPE_ANIMATION;4572id.query = TTR("animation");4573id.advance = false;4574// Dialog insert.4575_query_insert(id);4576}45774578void AnimationTrackEditor::insert_node_value_key(Node *p_node, const String &p_property, bool p_only_if_exists, bool p_advance) {4579if (read_only) {4580popup_read_only_dialog();4581return;4582}45834584ERR_FAIL_NULL(root);45854586// Let's build a node path.4587String path = String(root->get_path_to(p_node, true));45884589// Get the value from the subpath.4590Vector<StringName> subpath = NodePath(p_property).get_as_property_path().get_subnames();4591Variant value = p_node->get_indexed(subpath);45924593if (Object::cast_to<AnimationPlayer>(p_node) && p_property == "current_animation") {4594if (p_node == AnimationPlayerEditor::get_singleton()->get_player()) {4595EditorNode::get_singleton()->show_warning(TTR("AnimationPlayer can't animate itself, only other players."));4596return;4597}4598_insert_animation_key(path, value);4599return;4600}46014602EditorSelectionHistory *history = EditorNode::get_singleton()->get_editor_selection_history();4603for (int i = 1; i < history->get_path_size(); i++) {4604String prop = history->get_path_property(i);4605ERR_FAIL_COND(prop.is_empty());4606path += ":" + prop;4607}46084609path += ":" + p_property;46104611NodePath np = path;46124613// Locate track.46144615bool inserted = false;46164617for (int i = 0; i < animation->get_track_count(); i++) {4618if (animation->track_get_type(i) == Animation::TYPE_VALUE) {4619if (animation->track_get_path(i) != np) {4620continue;4621}46224623InsertData id;4624id.path = np;4625id.track_idx = i;4626id.value = value;4627id.type = Animation::TYPE_VALUE;4628// TRANSLATORS: This describes the target of new animation track, will be inserted into another string.4629id.query = vformat(TTR("property '%s'"), p_property);4630id.advance = p_advance;4631// Dialog insert.4632_query_insert(id);4633inserted = true;4634} else if (animation->track_get_type(i) == Animation::TYPE_BEZIER) {4635Variant actual_value;4636String track_path = String(animation->track_get_path(i));4637if (track_path == String(np)) {4638actual_value = value; // All good.4639} else {4640int sep = track_path.rfind_char(':');4641if (sep != -1) {4642String base_path = track_path.substr(0, sep);4643if (base_path == String(np)) {4644String value_name = track_path.substr(sep + 1);4645actual_value = value.get(value_name);4646} else {4647continue;4648}4649} else {4650continue;4651}4652}46534654InsertData id;4655id.path = animation->track_get_path(i);4656id.track_idx = i;4657id.value = actual_value;4658id.type = Animation::TYPE_BEZIER;4659id.query = vformat(TTR("property '%s'"), p_property);4660id.advance = p_advance;4661// Dialog insert.4662_query_insert(id);4663inserted = true;4664}4665}46664667if (inserted || p_only_if_exists) {4668return;4669}4670InsertData id;4671id.path = np;4672id.track_idx = -1;4673id.value = value;4674id.type = Animation::TYPE_VALUE;4675id.query = vformat(TTR("property '%s'"), p_property);4676id.advance = p_advance;4677// Dialog insert.4678_query_insert(id);4679}46804681PackedStringArray AnimationTrackEditor::get_selected_section() const {4682return marker_edit->get_selected_section();4683}46844685bool AnimationTrackEditor::is_marker_selected(const StringName &p_marker) const {4686return marker_edit->is_marker_selected(p_marker);4687}46884689bool AnimationTrackEditor::is_marker_moving_selection() const {4690return marker_edit->is_moving_selection();4691}46924693float AnimationTrackEditor::get_marker_moving_selection_offset() const {4694return marker_edit->get_moving_selection_offset();4695}46964697void AnimationTrackEditor::insert_value_key(const String &p_property, bool p_advance) {4698if (read_only) {4699popup_read_only_dialog();4700return;4701}47024703EditorSelectionHistory *history = EditorNode::get_singleton()->get_editor_selection_history();47044705ERR_FAIL_NULL(root);4706ERR_FAIL_COND(history->get_path_size() == 0);4707Object *obj = ObjectDB::get_instance(history->get_path_object(0));47084709Ref<MultiNodeEdit> multi_node_edit(obj);4710if (multi_node_edit.is_valid()) {4711Node *edited_scene = EditorNode::get_singleton()->get_edited_scene();4712ERR_FAIL_NULL(edited_scene);47134714make_insert_queue();47154716for (int i = 0; i < multi_node_edit->get_node_count(); ++i) {4717Node *node = edited_scene->get_node(multi_node_edit->get_node(i));4718insert_node_value_key(node, p_property, false, p_advance);4719}47204721commit_insert_queue();4722} else {4723Node *node = Object::cast_to<Node>(obj);4724ERR_FAIL_NULL(node);47254726make_insert_queue();4727insert_node_value_key(node, p_property, false, p_advance);4728commit_insert_queue();4729}4730}47314732bool AnimationTrackEditor::is_global_library_read_only() const {4733Ref<AnimationLibrary> al;4734AnimationMixer *mixer = AnimationPlayerEditor::get_singleton()->fetch_mixer_for_library();4735if (mixer) {4736if (!mixer->has_animation_library("")) {4737return false;4738} else {4739al = mixer->get_animation_library("");4740}4741}4742if (al.is_valid()) {4743String base = al->get_path();4744int srpos = base.find("::");4745if (srpos != -1) {4746base = base.substr(0, srpos);4747}4748if (FileAccess::exists(base + ".import")) {4749return true;4750}4751}4752return false;4753}47544755Ref<Animation> AnimationTrackEditor::_create_and_get_reset_animation() {4756AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();4757if (player->has_animation(SceneStringName(RESET))) {4758return player->get_animation(SceneStringName(RESET));4759} else {4760Ref<AnimationLibrary> al;4761AnimationMixer *mixer = AnimationPlayerEditor::get_singleton()->fetch_mixer_for_library();4762if (mixer) {4763if (!mixer->has_animation_library("")) {4764al.instantiate();4765mixer->add_animation_library("", al);4766} else {4767al = mixer->get_animation_library("");4768}4769}4770Ref<Animation> reset_anim;4771reset_anim.instantiate();4772reset_anim->set_length(ANIM_MIN_LENGTH);4773EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();4774undo_redo->add_do_method(al.ptr(), "add_animation", SceneStringName(RESET), reset_anim);4775undo_redo->add_do_method(AnimationPlayerEditor::get_singleton(), "_animation_player_changed", player);4776undo_redo->add_undo_method(al.ptr(), "remove_animation", SceneStringName(RESET));4777undo_redo->add_undo_method(AnimationPlayerEditor::get_singleton(), "_animation_player_changed", player);4778return reset_anim;4779}4780}47814782void AnimationTrackEditor::_confirm_insert_list() {4783EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();4784undo_redo->create_action(TTR("Animation Insert Key"));47854786bool create_reset = insert_confirm_reset->is_visible() && insert_confirm_reset->is_pressed();4787Ref<Animation> reset_anim;4788if (create_reset) {4789reset_anim = _create_and_get_reset_animation();4790}47914792TrackIndices next_tracks(animation.ptr(), reset_anim.ptr());4793bool advance = false;4794while (insert_data.size()) {4795if (insert_data.front()->get().advance) {4796advance = true;4797}4798next_tracks = _confirm_insert(insert_data.front()->get(), next_tracks, create_reset, reset_anim, insert_confirm_bezier->is_pressed());4799insert_data.pop_front();4800}48014802undo_redo->commit_action();48034804if (advance) {4805_edit_menu_pressed(EDIT_GOTO_NEXT_STEP_TIMELINE_ONLY);4806}4807}48084809PropertyInfo AnimationTrackEditor::_find_hint_for_track(int p_idx, NodePath &r_base_path, Variant *r_current_val) {4810r_base_path = NodePath();4811ERR_FAIL_COND_V(animation.is_null(), PropertyInfo());4812ERR_FAIL_INDEX_V(p_idx, animation->get_track_count(), PropertyInfo());48134814if (!root) {4815return PropertyInfo();4816}48174818NodePath path = animation->track_get_path(p_idx);48194820if (!root->has_node_and_resource(path)) {4821return PropertyInfo();4822}48234824Ref<Resource> res;4825Vector<StringName> leftover_path;4826Node *node = root->get_node_and_resource(path, res, leftover_path, true);48274828if (node) {4829r_base_path = node->get_path();4830}48314832if (leftover_path.is_empty()) {4833if (r_current_val) {4834if (res.is_valid()) {4835*r_current_val = res;4836} else if (node) {4837*r_current_val = node;4838}4839}4840return PropertyInfo();4841}48424843Variant property_info_base;4844if (res.is_valid()) {4845property_info_base = res;4846if (r_current_val) {4847*r_current_val = res->get_indexed(leftover_path);4848}4849} else if (node) {4850property_info_base = node;4851if (r_current_val) {4852*r_current_val = node->get_indexed(leftover_path);4853}4854}48554856if (property_info_base.is_null()) {4857WARN_PRINT(vformat("Could not determine track hint for '%s:%s' because its base property is null.",4858String(path.get_concatenated_names()), String(path.get_concatenated_subnames())));4859return PropertyInfo();4860}48614862List<PropertyInfo> pinfo;4863property_info_base.get_property_list(&pinfo);48644865for (const PropertyInfo &E : pinfo) {4866if (E.name == leftover_path[leftover_path.size() - 1]) {4867return E;4868}4869}48704871return PropertyInfo();4872}48734874static Vector<String> _get_bezier_subindices_for_type(Variant::Type p_type, bool *r_valid = nullptr) {4875Vector<String> subindices;4876if (r_valid) {4877*r_valid = true;4878}4879switch (p_type) {4880case Variant::INT: {4881subindices.push_back("");4882} break;4883case Variant::FLOAT: {4884subindices.push_back("");4885} break;4886case Variant::VECTOR2: {4887subindices.push_back(":x");4888subindices.push_back(":y");4889} break;4890case Variant::VECTOR3: {4891subindices.push_back(":x");4892subindices.push_back(":y");4893subindices.push_back(":z");4894} break;4895case Variant::QUATERNION: {4896subindices.push_back(":x");4897subindices.push_back(":y");4898subindices.push_back(":z");4899subindices.push_back(":w");4900} break;4901case Variant::COLOR: {4902subindices.push_back(":r");4903subindices.push_back(":g");4904subindices.push_back(":b");4905subindices.push_back(":a");4906} break;4907case Variant::PLANE: {4908subindices.push_back(":x");4909subindices.push_back(":y");4910subindices.push_back(":z");4911subindices.push_back(":d");4912} break;4913case Variant::NIL: {4914subindices.push_back(""); // Hack: it is probably float since non-numeric types are filtered in the selection window.4915} break;4916default: {4917if (r_valid) {4918*r_valid = false;4919}4920}4921}49224923return subindices;4924}49254926AnimationTrackEditor::TrackIndices AnimationTrackEditor::_confirm_insert(InsertData p_id, TrackIndices p_next_tracks, bool p_reset_wanted, Ref<Animation> p_reset_anim, bool p_create_beziers) {4927bool created = false;49284929bool create_normal_track = p_id.track_idx < 0;4930bool create_reset_track = p_reset_wanted && track_type_is_resettable(p_id.type);49314932Animation::UpdateMode update_mode = Animation::UPDATE_DISCRETE;4933Animation::InterpolationType interp_type = Animation::INTERPOLATION_LINEAR;4934bool loop_wrap = true;4935if (create_normal_track || create_reset_track) {4936if (p_id.type == Animation::TYPE_VALUE || p_id.type == Animation::TYPE_BEZIER) {4937_fetch_value_track_options(p_id.path, &update_mode, &interp_type, &loop_wrap);4938}4939}49404941EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();4942if (create_normal_track) {4943if (p_create_beziers) {4944bool valid;4945Vector<String> subindices = _get_bezier_subindices_for_type(p_id.value.get_type(), &valid);4946if (valid) {4947for (int i = 0; i < subindices.size(); i++) {4948InsertData id = p_id;4949id.type = Animation::TYPE_BEZIER;4950id.value = subindices[i].is_empty() ? p_id.value : p_id.value.get(subindices[i].substr(1));4951id.path = String(p_id.path) + subindices[i];4952p_next_tracks = _confirm_insert(id, p_next_tracks, p_reset_wanted, p_reset_anim, false);4953}49544955return p_next_tracks;4956}4957}4958created = true;49594960p_id.track_idx = p_next_tracks.normal;49614962undo_redo->add_do_method(animation.ptr(), "add_track", p_id.type);4963undo_redo->add_do_method(animation.ptr(), "track_set_path", p_id.track_idx, p_id.path);4964undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_type", p_id.track_idx, interp_type);4965undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_loop_wrap", p_id.track_idx, loop_wrap);4966if (p_id.type == Animation::TYPE_VALUE) {4967undo_redo->add_do_method(animation.ptr(), "value_track_set_update_mode", p_id.track_idx, update_mode);4968}4969}49704971float time = p_id.time == FLT_MAX ? timeline->get_play_position() : p_id.time;4972Variant value;49734974switch (p_id.type) {4975case Animation::TYPE_POSITION_3D:4976case Animation::TYPE_ROTATION_3D:4977case Animation::TYPE_SCALE_3D:4978case Animation::TYPE_BLEND_SHAPE:4979case Animation::TYPE_VALUE:4980case Animation::TYPE_AUDIO:4981case Animation::TYPE_ANIMATION: {4982value = p_id.value;49834984} break;4985case Animation::TYPE_BEZIER: {4986int existing = -1;4987if (p_id.track_idx < animation->get_track_count()) {4988existing = animation->track_find_key(p_id.track_idx, time, Animation::FIND_MODE_APPROX);4989}49904991if (existing != -1) {4992Array arr = animation->track_get_key_value(p_id.track_idx, existing);4993arr[0] = p_id.value;4994value = arr;4995} else {4996value = animation->make_default_bezier_key(p_id.value);4997}4998bezier_edit_icon->set_disabled(false);4999} break;5000default: {5001// Other track types shouldn't use this code path.5002DEV_ASSERT(false);5003}5004}50055006undo_redo->add_do_method(animation.ptr(), "track_insert_key", p_id.track_idx, time, value);5007if (!created && p_id.type == Animation::TYPE_BEZIER) {5008undo_redo->add_do_method(this, "_bezier_track_set_key_handle_mode_at_time", animation.ptr(), p_id.track_idx, time, (Animation::HandleMode)bezier_key_mode->get_selected_id(), Animation::HANDLE_SET_MODE_AUTO);5009}50105011if (created) {5012// Just remove the track.5013undo_redo->add_undo_method(this, "_clear_selection", false);5014undo_redo->add_undo_method(animation.ptr(), "remove_track", animation->get_track_count());5015p_next_tracks.normal++;5016} else {5017undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", p_id.track_idx, time);5018int existing = animation->track_find_key(p_id.track_idx, time, Animation::FIND_MODE_APPROX);5019if (existing != -1) {5020Variant v = animation->track_get_key_value(p_id.track_idx, existing);5021float trans = animation->track_get_key_transition(p_id.track_idx, existing);5022undo_redo->add_undo_method(animation.ptr(), "track_insert_key", p_id.track_idx, time, v, trans);5023}5024}50255026if (create_reset_track) {5027Animation *reset_anim = p_reset_anim.ptr();5028for (int i = 0; i < reset_anim->get_track_count(); i++) {5029if (reset_anim->track_get_path(i) == p_id.path) {5030create_reset_track = false;5031break;5032}5033}5034if (create_reset_track) {5035undo_redo->add_do_method(reset_anim, "add_track", p_id.type);5036undo_redo->add_do_method(reset_anim, "track_set_path", p_next_tracks.reset, p_id.path);5037if (p_id.type == Animation::TYPE_VALUE) {5038undo_redo->add_do_method(reset_anim, "value_track_set_update_mode", p_next_tracks.reset, update_mode);5039}5040undo_redo->add_do_method(reset_anim, "track_set_interpolation_type", p_next_tracks.reset, interp_type);5041undo_redo->add_do_method(reset_anim, "track_insert_key", p_next_tracks.reset, 0.0f, value);5042undo_redo->add_undo_method(reset_anim, "remove_track", reset_anim->get_track_count());5043p_next_tracks.reset++;5044}5045}50465047return p_next_tracks;5048}50495050void AnimationTrackEditor::show_select_node_warning(bool p_show) {5051info_message_vbox->set_visible(p_show);5052}50535054void AnimationTrackEditor::show_dummy_player_warning(bool p_show) {5055dummy_player_warning->set_visible(p_show);5056}50575058void AnimationTrackEditor::show_inactive_player_warning(bool p_show) {5059inactive_player_warning->set_visible(p_show);5060}50615062bool AnimationTrackEditor::is_key_selected(int p_track, int p_key) const {5063SelectedKey sk;5064sk.key = p_key;5065sk.track = p_track;50665067return selection.has(sk);5068}50695070bool AnimationTrackEditor::is_selection_active() const {5071return selection.size();5072}50735074bool AnimationTrackEditor::is_key_clipboard_active() const {5075return key_clipboard.keys.size();5076}50775078bool AnimationTrackEditor::is_snap_timeline_enabled() const {5079return snap_timeline->is_pressed() ^ Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL);5080}50815082bool AnimationTrackEditor::is_snap_keys_enabled() const {5083return snap_keys->is_pressed() ^ Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL);5084}50855086bool AnimationTrackEditor::is_insert_at_current_time_enabled() const {5087return insert_at_current_time->is_pressed();5088}50895090void AnimationTrackEditor::resolve_insertion_offset(float &r_offset) const {5091if (is_insert_at_current_time_enabled()) {5092r_offset = timeline->get_play_position();5093}5094}50955096bool AnimationTrackEditor::is_bezier_editor_active() const {5097return bezier_edit->is_visible();5098}50995100bool AnimationTrackEditor::can_add_reset_key() const {5101if (is_global_library_read_only()) {5102return false;5103}5104for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {5105const Animation::TrackType track_type = animation->track_get_type(E.key.track);5106if (track_type != Animation::TYPE_ANIMATION && track_type != Animation::TYPE_AUDIO && track_type != Animation::TYPE_METHOD) {5107return true;5108}5109}5110return false;5111}51125113void AnimationTrackEditor::_on_filter_updated(const String &p_filter) {5114emit_signal(SNAME("filter_changed"));5115}51165117void AnimationTrackEditor::_update_tracks() {5118int selected = _get_track_selected();51195120while (track_vbox->get_child_count()) {5121memdelete(track_vbox->get_child(0));5122}51235124timeline->set_track_edit(nullptr);51255126track_edits.clear();5127groups.clear();51285129if (animation.is_null()) {5130return;5131}51325133bool file_read_only = false;5134if (!animation->get_path().is_resource_file()) {5135int srpos = animation->get_path().find("::");5136if (srpos != -1) {5137String base = animation->get_path().substr(0, srpos);5138if (ResourceLoader::get_resource_type(base) == "PackedScene") {5139if (!get_tree()->get_edited_scene_root() || get_tree()->get_edited_scene_root()->get_scene_file_path() != base) {5140file_read_only = true;5141}5142} else {5143if (FileAccess::exists(base + ".import")) {5144file_read_only = true;5145}5146}5147}5148} else {5149if (FileAccess::exists(animation->get_path() + ".import")) {5150file_read_only = true;5151}5152}51535154RBMap<String, VBoxContainer *> group_sort;5155LocalVector<VBoxContainer *> group_containers;51565157bool use_grouping = !view_group->is_pressed();5158bool use_filter = selected_filter->is_pressed();5159bool use_alphabetic_sorting = alphabetic_sorting->is_pressed();51605161AnimationTrackEdit *selected_track_edit = nullptr;51625163for (int i = 0; i < animation->get_track_count(); i++) {5164AnimationTrackEdit *track_edit = nullptr;51655166// Find hint and info for plugin.51675168if (use_filter) {5169NodePath path = animation->track_get_path(i);51705171if (root) {5172Node *node = root->get_node_or_null(path);5173if (!node) {5174continue; // No node, no filter.5175}5176if (!EditorNode::get_singleton()->get_editor_selection()->is_selected(node)) {5177continue; // Skip track due to not selected.5178}5179}5180}51815182String filter_text = timeline->filter_track->get_text();51835184if (!filter_text.is_empty()) {5185String target = String(animation->track_get_path(i));5186if (!target.containsn(filter_text)) {5187continue;5188}5189}51905191if (animation->track_get_type(i) == Animation::TYPE_VALUE) {5192NodePath path = animation->track_get_path(i);51935194if (root && root->has_node_and_resource(path)) {5195Ref<Resource> res;5196NodePath base_path;5197Vector<StringName> leftover_path;5198Node *node = root->get_node_and_resource(path, res, leftover_path, true);5199PropertyInfo pinfo = _find_hint_for_track(i, base_path);52005201Object *object = node;5202if (res.is_valid()) {5203object = res.ptr();5204}52055206if (object && !leftover_path.is_empty()) {5207if (pinfo.name.is_empty()) {5208pinfo.name = leftover_path[leftover_path.size() - 1];5209}52105211for (int j = 0; j < track_edit_plugins.size(); j++) {5212track_edit = track_edit_plugins.write[j]->create_value_track_edit(object, pinfo.type, pinfo.name, pinfo.hint, pinfo.hint_string, pinfo.usage);5213if (track_edit) {5214break;5215}5216}5217}5218}5219}5220if (animation->track_get_type(i) == Animation::TYPE_AUDIO) {5221for (int j = 0; j < track_edit_plugins.size(); j++) {5222track_edit = track_edit_plugins.write[j]->create_audio_track_edit();5223if (track_edit) {5224break;5225}5226}5227}52285229if (animation->track_get_type(i) == Animation::TYPE_ANIMATION) {5230NodePath path = animation->track_get_path(i);52315232Node *node = nullptr;5233if (root) {5234node = root->get_node_or_null(path);5235}52365237if (node && Object::cast_to<AnimationPlayer>(node)) {5238for (int j = 0; j < track_edit_plugins.size(); j++) {5239track_edit = track_edit_plugins.write[j]->create_animation_track_edit(node);5240if (track_edit) {5241break;5242}5243}5244}5245}52465247if (track_edit == nullptr) {5248// No valid plugin_found.5249track_edit = memnew(AnimationTrackEdit);5250}52515252track_edits.push_back(track_edit);52535254if (use_grouping) {5255String base_path = String(animation->track_get_path(i));5256base_path = base_path.get_slicec(':', 0); // Remove sub-path.52575258if (!group_sort.has(base_path)) {5259AnimationTrackEditGroup *g = memnew(AnimationTrackEditGroup);5260Ref<Texture2D> icon = get_editor_theme_icon(SNAME("Node"));5261String name = base_path;5262String tooltip;5263if (root) {5264Node *n = root->get_node_or_null(base_path);5265if (n) {5266icon = EditorNode::get_singleton()->get_object_icon(n);5267name = n->get_name();5268tooltip = String(root->get_path_to(n));5269}5270}52715272g->set_type_and_name(icon, name, animation->track_get_path(i));5273g->set_root(root);5274g->set_tooltip_text(tooltip);5275g->set_timeline(timeline);5276g->set_editor(this);5277groups.push_back(g);5278VBoxContainer *vb = memnew(VBoxContainer);5279vb->add_theme_constant_override("separation", 0);5280vb->add_child(g);5281group_sort[base_path] = vb;5282group_containers.push_back(vb);5283}52845285track_edit->set_in_group(true);5286group_sort[base_path]->add_child(track_edit);52875288} else {5289track_edit->set_in_group(false);5290}52915292track_edit->set_timeline(timeline);5293track_edit->set_root(root);5294track_edit->set_animation_and_track(animation, i, file_read_only);5295track_edit->set_play_position(timeline->get_play_position());5296track_edit->set_editor(this);52975298if (selected == i) {5299selected_track_edit = track_edit;5300}53015302track_edit->connect("timeline_changed", callable_mp(this, &AnimationTrackEditor::_timeline_changed));5303track_edit->connect("remove_request", callable_mp(this, &AnimationTrackEditor::_track_remove_request), CONNECT_DEFERRED);5304track_edit->connect("dropped", callable_mp(this, &AnimationTrackEditor::_dropped_track), CONNECT_DEFERRED);5305track_edit->connect("insert_key", callable_mp(this, &AnimationTrackEditor::_insert_key_from_track).bind(i), CONNECT_DEFERRED);5306track_edit->connect("select_key", callable_mp(this, &AnimationTrackEditor::_key_selected).bind(i), CONNECT_DEFERRED);5307track_edit->connect("deselect_key", callable_mp(this, &AnimationTrackEditor::_key_deselected).bind(i), CONNECT_DEFERRED);5308track_edit->connect("move_selection_begin", callable_mp(this, &AnimationTrackEditor::_move_selection_begin));5309track_edit->connect("move_selection", callable_mp(this, &AnimationTrackEditor::_move_selection));5310track_edit->connect("move_selection_commit", callable_mp(this, &AnimationTrackEditor::_move_selection_commit));5311track_edit->connect("move_selection_cancel", callable_mp(this, &AnimationTrackEditor::_move_selection_cancel));53125313track_edit->connect("duplicate_request", callable_mp(this, &AnimationTrackEditor::_anim_duplicate_keys).bind(i), CONNECT_DEFERRED);5314track_edit->connect("cut_request", callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_CUT_KEYS), CONNECT_DEFERRED);5315track_edit->connect("copy_request", callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_COPY_KEYS), CONNECT_DEFERRED);5316track_edit->connect("paste_request", callable_mp(this, &AnimationTrackEditor::_anim_paste_keys).bind(i), CONNECT_DEFERRED);5317track_edit->connect("create_reset_request", callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_ADD_RESET_KEY), CONNECT_DEFERRED);5318track_edit->connect("delete_request", callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_DELETE_SELECTION), CONNECT_DEFERRED);5319}53205321if (use_grouping) {5322if (use_alphabetic_sorting) {5323struct GroupAlphaCompare {5324bool operator()(const VBoxContainer *p_lhs, const VBoxContainer *p_rhs) const {5325String lhs_node_name = Object::cast_to<AnimationTrackEditGroup>(p_lhs->get_child(0))->get_node_name();5326String rhs_node_name = Object::cast_to<AnimationTrackEditGroup>(p_rhs->get_child(0))->get_node_name();5327return lhs_node_name < rhs_node_name;5328}5329};53305331group_containers.sort_custom<GroupAlphaCompare>();5332}53335334for (VBoxContainer *vb : group_containers) {5335track_vbox->add_child(vb);5336}53375338} else {5339if (use_alphabetic_sorting) {5340struct TrackAlphaCompare {5341bool operator()(const AnimationTrackEdit *p_lhs, const AnimationTrackEdit *p_rhs) const {5342String lhs_leaf = (String)p_lhs->get_path().slice(-p_lhs->get_path().get_subname_count() - 1);5343String rhs_leaf = (String)p_rhs->get_path().slice(-p_rhs->get_path().get_subname_count() - 1);5344return lhs_leaf < rhs_leaf;5345}5346};53475348track_edits.sort_custom<TrackAlphaCompare>();5349}53505351for (AnimationTrackEdit *track_edit : track_edits) {5352track_vbox->add_child(track_edit);5353}5354}53555356if (selected_track_edit != nullptr) {5357selected_track_edit->grab_focus();5358}5359}53605361void AnimationTrackEditor::_redraw_tracks() {5362for (int i = 0; i < track_edits.size(); i++) {5363track_edits[i]->queue_redraw();5364}5365}53665367void AnimationTrackEditor::_redraw_groups() {5368for (int i = 0; i < groups.size(); i++) {5369groups[i]->queue_redraw();5370}5371}53725373void AnimationTrackEditor::_animation_changed() {5374if (animation_changing_awaiting_update) {5375return; // All will be updated, don't bother with anything.5376}53775378_check_bezier_exist();53795380if (key_edit) {5381if (key_edit->setting) {5382// If editing a key, just redraw the edited track, makes refresh less costly.5383if (key_edit->track < track_edits.size()) {5384if (animation->track_get_type(key_edit->track) == Animation::TYPE_BEZIER) {5385bezier_edit->queue_redraw();5386} else {5387track_edits[key_edit->track]->queue_redraw();5388}5389}5390return;5391} else {5392_update_key_edit();5393}5394}53955396animation_changing_awaiting_update = true;5397callable_mp(this, &AnimationTrackEditor::_animation_update).call_deferred();5398}53995400void AnimationTrackEditor::_snap_mode_changed(int p_mode) {5401bool use_fps = p_mode == 1;5402timeline->set_use_fps(use_fps);5403if (key_edit) {5404key_edit->set_use_fps(use_fps);5405}5406marker_edit->set_use_fps(use_fps);5407// To ensure that the conversion results are consistent between serialization and load, the value is snapped with 0.0625 to be a rational number when FPS mode is used.5408step->set_step(use_fps ? FPS_DECIMAL : SECOND_DECIMAL);5409if (use_fps) {5410fps_compat->hide();5411} else {5412fps_compat->show();5413}5414_update_step_spinbox();5415}54165417void AnimationTrackEditor::_update_step_spinbox() {5418if (animation.is_null()) {5419return;5420}5421step->set_block_signals(true);54225423if (timeline->is_using_fps()) {5424if (animation->get_step() == 0.0) {5425step->set_value(0.0);5426} else {5427step->set_value(1.0 / animation->get_step());5428}5429} else {5430step->set_value(animation->get_step());5431}54325433step->set_block_signals(false);5434_update_snap_unit();5435}54365437void AnimationTrackEditor::_store_snap_states() {5438EditorSettings::get_singleton()->set_project_metadata("animation_track_editor", "snap_timeline", snap_timeline->is_pressed());5439EditorSettings::get_singleton()->set_project_metadata("animation_track_editor", "snap_keys", snap_keys->is_pressed());5440}54415442void AnimationTrackEditor::_update_fps_compat_mode(bool p_enabled) {5443_update_snap_unit();5444}54455446void AnimationTrackEditor::_update_nearest_fps_label() {5447bool is_fps_invalid = nearest_fps == 0;5448if (is_fps_invalid) {5449nearest_fps_label->hide();5450} else {5451nearest_fps_label->show();5452nearest_fps_label->set_text(vformat(TTR("Nearest FPS: %d"), nearest_fps));5453}5454}54555456void AnimationTrackEditor::_animation_update() {5457timeline->queue_redraw();5458timeline->update_values();54595460bool same = true;54615462if (animation.is_null()) {5463return;5464}54655466if (track_edits.size() == animation->get_track_count()) {5467// Check tracks are the same.54685469for (int i = 0; i < track_edits.size(); i++) {5470if (track_edits[i]->get_path() != animation->track_get_path(i)) {5471same = false;5472break;5473}5474}5475} else {5476same = false;5477}54785479if (same) {5480_redraw_tracks();5481_redraw_groups();5482} else {5483_update_tracks();5484}54855486bezier_edit->queue_redraw();54875488_update_step_spinbox();5489emit_signal(SNAME("animation_step_changed"), animation->get_step());5490emit_signal(SNAME("animation_len_changed"), animation->get_length());54915492animation_changing_awaiting_update = false;5493}54945495MenuButton *AnimationTrackEditor::get_edit_menu() {5496return edit;5497}54985499void AnimationTrackEditor::_notification(int p_what) {5500switch (p_what) {5501case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {5502if (!EditorSettings::get_singleton()->check_changed_settings_in_group("editors/panning")) {5503break;5504}55055506panner->setup((ViewPanner::ControlScheme)EDITOR_GET("editors/panning/animation_editors_panning_scheme").operator int(), ED_GET_SHORTCUT("canvas_item_editor/pan_view"), bool(EDITOR_GET("editors/panning/simple_panning")));5507panner->setup_warped_panning(get_viewport(), EDITOR_GET("editors/panning/warped_mouse_panning"));5508} break;55095510case NOTIFICATION_THEME_CHANGED: {5511add_animation_player->set_button_icon(get_editor_theme_icon(SNAME("Add")));5512zoom_icon->set_texture(get_editor_theme_icon(SNAME("Zoom")));5513bezier_edit_icon->set_button_icon(get_editor_theme_icon(SNAME("EditBezier")));5514snap_timeline->set_button_icon(get_editor_theme_icon(SNAME("SnapTimeline")));5515snap_keys->set_button_icon(get_editor_theme_icon(SNAME("SnapKeys")));5516insert_at_current_time->set_button_icon(get_editor_theme_icon(SNAME("InsertAtCurrentTime")));5517fps_compat->set_button_icon(get_editor_theme_icon(SNAME("FPS")));5518view_group->set_button_icon(get_editor_theme_icon(view_group->is_pressed() ? SNAME("AnimationTrackList") : SNAME("AnimationTrackGroup")));5519function_name_toggler->set_button_icon(get_editor_theme_icon(SNAME("MemberMethod")));5520selected_filter->set_button_icon(get_editor_theme_icon(SNAME("AnimationFilter")));5521alphabetic_sorting->set_button_icon(get_editor_theme_icon(SNAME("Sort")));5522imported_anim_warning->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning")));5523dummy_player_warning->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning")));5524inactive_player_warning->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning")));55255526Ref<StyleBox> panel_style = get_theme_stylebox(SceneStringName(panel), SNAME("Tree"))->duplicate();5527panel_style->set_content_margin(SIDE_TOP, get_theme_constant("base_margin", EditorStringName(Editor)) * EDSCALE);5528main_panel->add_theme_style_override(SceneStringName(panel), panel_style);55295530edit->get_popup()->set_item_icon(edit->get_popup()->get_item_index(EDIT_ADD_RESET_KEY), get_editor_theme_icon(SNAME("MoveUp")));5531edit->get_popup()->set_item_icon(edit->get_popup()->get_item_index(EDIT_APPLY_RESET), get_editor_theme_icon(SNAME("Reload")));5532auto_fit->set_button_icon(get_editor_theme_icon(SNAME("AnimationAutoFit")));5533auto_fit_bezier->set_button_icon(get_editor_theme_icon(SNAME("AnimationAutoFitBezier")));55345535const int timeline_separation = get_theme_constant(SNAME("timeline_v_separation"), SNAME("AnimationTrackEditor"));5536timeline_vbox->add_theme_constant_override("separation", timeline_separation);55375538const int track_separation = get_theme_constant(SNAME("track_v_separation"), SNAME("AnimationTrackEditor"));5539track_vbox->add_theme_constant_override("separation", track_separation);55405541function_name_toggler->add_theme_color_override("icon_pressed_color", get_theme_color("icon_disabled_color", EditorStringName(Editor)));55425543bezier_key_mode->set_item_icon(bezier_key_mode->get_item_index(Animation::HANDLE_MODE_FREE), get_editor_theme_icon(SNAME("BezierHandlesFree")));5544bezier_key_mode->set_item_icon(bezier_key_mode->get_item_index(Animation::HANDLE_MODE_LINEAR), get_editor_theme_icon(SNAME("BezierHandlesLinear")));5545bezier_key_mode->set_item_icon(bezier_key_mode->get_item_index(Animation::HANDLE_MODE_BALANCED), get_editor_theme_icon(SNAME("BezierHandlesBalanced")));5546bezier_key_mode->set_item_icon(bezier_key_mode->get_item_index(Animation::HANDLE_MODE_MIRRORED), get_editor_theme_icon(SNAME("BezierHandlesMirror")));55475548_update_timeline_margins();5549} break;55505551case NOTIFICATION_READY: {5552Node *scene_root = EditorNode::get_singleton()->get_scene_root();5553scene_root->connect("child_entered_tree", callable_mp(this, &AnimationTrackEditor::_root_node_changed).bind(false));5554scene_root->connect("child_exiting_tree", callable_mp(this, &AnimationTrackEditor::_root_node_changed).bind(true));55555556EditorNode::get_singleton()->connect("scene_changed", callable_mp(this, &AnimationTrackEditor::_scene_changed));5557EditorNode::get_singleton()->get_editor_selection()->connect("selection_changed", callable_mp(this, &AnimationTrackEditor::_selection_changed));55585559panner->setup((ViewPanner::ControlScheme)EDITOR_GET("editors/panning/animation_editors_panning_scheme").operator int(), ED_GET_SHORTCUT("canvas_item_editor/pan_view"), bool(EDITOR_GET("editors/panning/simple_panning")));5560panner->setup_warped_panning(get_viewport(), EDITOR_GET("editors/panning/warped_mouse_panning"));5561} break;55625563case NOTIFICATION_VISIBILITY_CHANGED: {5564update_keying();5565} break;55665567case NOTIFICATION_TRANSLATION_CHANGED: {5568bezier_key_mode->set_item_text(bezier_key_mode->get_item_index(Animation::HANDLE_MODE_FREE), TTR("Free", "Bezier Handle Mode"));5569bezier_key_mode->set_item_text(bezier_key_mode->get_item_index(Animation::HANDLE_MODE_LINEAR), TTR("Linear", "Bezier Handle Mode"));5570bezier_key_mode->set_item_text(bezier_key_mode->get_item_index(Animation::HANDLE_MODE_BALANCED), TTR("Balanced", "Bezier Handle Mode"));5571bezier_key_mode->set_item_text(bezier_key_mode->get_item_index(Animation::HANDLE_MODE_MIRRORED), TTR("Mirrored", "Bezier Handle Mode"));5572bezier_key_mode->set_tooltip_text(TTR("Bezier Default Mode") + "\n" + TTR("Set the default handle mode of new bezier keys."));5573} break;5574}5575}55765577void AnimationTrackEditor::_update_scroll(double) {5578_redraw_tracks();5579_redraw_groups();5580marker_edit->queue_redraw();5581}55825583void AnimationTrackEditor::_update_step(double p_new_step) {5584if (animation.is_null()) {5585return;5586}55875588_update_snap_unit();55895590EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();5591undo_redo->create_action(TTR("Change Animation Step"));5592double step_value = p_new_step;5593if (timeline->is_using_fps()) {5594if (step_value != 0.0) {5595// A step_value should be less than or equal to 1000 to ensure that no error accumulates due to interactions with retrieving values from inner range.5596step_value = 1.0 / MIN(1000.0, p_new_step);5597}5598timeline->queue_redraw();5599}5600undo_redo->add_do_method(animation.ptr(), "set_step", step_value);5601undo_redo->add_undo_method(animation.ptr(), "set_step", animation->get_step());5602step->set_block_signals(true);5603undo_redo->commit_action();5604step->set_block_signals(false);5605emit_signal(SNAME("animation_step_changed"), step_value);5606}56075608void AnimationTrackEditor::_update_length(double p_new_len) {5609emit_signal(SNAME("animation_len_changed"), p_new_len);5610}56115612void AnimationTrackEditor::_dropped_track(int p_from_track, int p_to_track) {5613if (p_from_track == p_to_track || p_from_track == p_to_track - 1) {5614return;5615}56165617_clear_selection(true);5618EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();5619undo_redo->create_action(TTR("Rearrange Tracks"));5620undo_redo->add_do_method(animation.ptr(), "track_move_to", p_from_track, p_to_track);5621// Take into account that the position of the tracks that come after the one removed will change.5622int to_track_real = p_to_track > p_from_track ? p_to_track - 1 : p_to_track;5623undo_redo->add_undo_method(animation.ptr(), "track_move_to", to_track_real, p_to_track > p_from_track ? p_from_track : p_from_track + 1);5624undo_redo->add_do_method(this, "_track_grab_focus", to_track_real);5625undo_redo->add_undo_method(this, "_track_grab_focus", p_from_track);5626undo_redo->commit_action();5627}56285629void AnimationTrackEditor::_new_track_node_selected(NodePath p_path) {5630ERR_FAIL_NULL(root);5631Node *node = get_node_or_null(p_path);5632ERR_FAIL_NULL(node);5633NodePath path_to = root->get_path_to(node, true);56345635if (adding_track_type == Animation::TYPE_BLEND_SHAPE && !node->is_class("MeshInstance3D")) {5636EditorNode::get_singleton()->show_warning(TTR("Blend Shape tracks only apply to MeshInstance3D nodes."));5637return;5638}56395640if ((adding_track_type == Animation::TYPE_POSITION_3D || adding_track_type == Animation::TYPE_ROTATION_3D || adding_track_type == Animation::TYPE_SCALE_3D) && !node->is_class("Node3D")) {5641EditorNode::get_singleton()->show_warning(TTR("Position/Rotation/Scale 3D tracks only apply to 3D-based nodes."));5642return;5643}56445645switch (adding_track_type) {5646case Animation::TYPE_VALUE: {5647adding_track_path = path_to;5648prop_selector->set_type_filter(Vector<Variant::Type>());5649prop_selector->select_property_from_instance(node);5650} break;5651case Animation::TYPE_BLEND_SHAPE: {5652adding_track_path = path_to;5653Vector<Variant::Type> filter;5654filter.push_back(Variant::FLOAT);5655prop_selector->set_type_filter(filter);5656prop_selector->select_property_from_instance(node);5657} break;5658case Animation::TYPE_POSITION_3D:5659case Animation::TYPE_ROTATION_3D:5660case Animation::TYPE_SCALE_3D:5661case Animation::TYPE_METHOD: {5662EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();5663undo_redo->create_action(TTR("Add Track"));5664undo_redo->add_do_method(animation.ptr(), "add_track", adding_track_type);5665undo_redo->add_do_method(animation.ptr(), "track_set_path", animation->get_track_count(), path_to);5666undo_redo->add_undo_method(animation.ptr(), "remove_track", animation->get_track_count());5667undo_redo->commit_action();56685669} break;5670case Animation::TYPE_BEZIER: {5671Vector<Variant::Type> filter;5672filter.push_back(Variant::INT);5673filter.push_back(Variant::FLOAT);5674filter.push_back(Variant::VECTOR2);5675filter.push_back(Variant::VECTOR3);5676filter.push_back(Variant::QUATERNION);5677filter.push_back(Variant::PLANE);5678filter.push_back(Variant::COLOR);56795680adding_track_path = path_to;5681prop_selector->set_type_filter(filter);5682prop_selector->select_property_from_instance(node);5683} break;5684case Animation::TYPE_AUDIO: {5685if (!node->is_class("AudioStreamPlayer") && !node->is_class("AudioStreamPlayer2D") && !node->is_class("AudioStreamPlayer3D")) {5686EditorNode::get_singleton()->show_warning(TTR("Audio tracks can only point to nodes of type:\n-AudioStreamPlayer\n-AudioStreamPlayer2D\n-AudioStreamPlayer3D"));5687return;5688}56895690EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();5691undo_redo->create_action(TTR("Add Track"));5692undo_redo->add_do_method(animation.ptr(), "add_track", adding_track_type);5693undo_redo->add_do_method(animation.ptr(), "track_set_path", animation->get_track_count(), path_to);5694undo_redo->add_undo_method(animation.ptr(), "remove_track", animation->get_track_count());5695undo_redo->commit_action();56965697} break;5698case Animation::TYPE_ANIMATION: {5699if (!node->is_class("AnimationPlayer")) {5700EditorNode::get_singleton()->show_warning(TTR("Animation tracks can only point to AnimationPlayer nodes."));5701return;5702}57035704if (node == AnimationPlayerEditor::get_singleton()->get_player()) {5705EditorNode::get_singleton()->show_warning(TTR("AnimationPlayer can't animate itself, only other players."));5706return;5707}57085709EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();5710undo_redo->create_action(TTR("Add Track"));5711undo_redo->add_do_method(animation.ptr(), "add_track", adding_track_type);5712undo_redo->add_do_method(animation.ptr(), "track_set_path", animation->get_track_count(), path_to);5713undo_redo->add_undo_method(animation.ptr(), "remove_track", animation->get_track_count());5714undo_redo->commit_action();57155716} break;5717}5718}57195720void AnimationTrackEditor::_add_track(int p_type) {5721AnimationPlayer *ap = AnimationPlayerEditor::get_singleton()->get_player();5722if (!ap) {5723ERR_FAIL_EDMSG("No AnimationPlayer is currently being edited.");5724}5725Node *root_node = ap->get_node_or_null(ap->get_root_node());5726if (!root_node) {5727EditorNode::get_singleton()->show_warning(TTR("Not possible to add a new track without a root"));5728return;5729}5730adding_track_type = p_type;5731Vector<StringName> valid_types;5732switch (adding_track_type) {5733case Animation::TYPE_BLEND_SHAPE: {5734// Blend Shape is a property of MeshInstance3D.5735valid_types.push_back(SNAME("MeshInstance3D"));5736} break;5737case Animation::TYPE_POSITION_3D:5738case Animation::TYPE_ROTATION_3D:5739case Animation::TYPE_SCALE_3D: {5740// 3D Properties come from nodes inheriting Node3D.5741valid_types.push_back(SNAME("Node3D"));5742} break;5743case Animation::TYPE_AUDIO: {5744valid_types.push_back(SNAME("AudioStreamPlayer"));5745valid_types.push_back(SNAME("AudioStreamPlayer2D"));5746valid_types.push_back(SNAME("AudioStreamPlayer3D"));5747} break;5748case Animation::TYPE_ANIMATION: {5749valid_types.push_back(SNAME("AnimationPlayer"));5750} break;5751}5752pick_track->set_valid_types(valid_types);5753pick_track->popup_scenetree_dialog(nullptr, root_node);5754pick_track->get_filter_line_edit()->clear();5755pick_track->get_filter_line_edit()->grab_focus();5756}57575758void AnimationTrackEditor::_fetch_value_track_options(const NodePath &p_path, Animation::UpdateMode *r_update_mode, Animation::InterpolationType *r_interpolation_type, bool *r_loop_wrap) {5759AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();5760if (player->has_animation(SceneStringName(RESET))) {5761Ref<Animation> reset_anim = player->get_animation(SceneStringName(RESET));5762int rt = reset_anim->find_track(p_path, Animation::TrackType::TYPE_VALUE);5763if (rt >= 0) {5764*r_update_mode = reset_anim->value_track_get_update_mode(rt);5765*r_interpolation_type = reset_anim->track_get_interpolation_type(rt);5766*r_loop_wrap = reset_anim->track_get_interpolation_loop_wrap(rt);5767return;5768}5769rt = reset_anim->find_track(p_path, Animation::TrackType::TYPE_BEZIER);5770if (rt >= 0) {5771*r_interpolation_type = reset_anim->track_get_interpolation_type(rt);5772*r_loop_wrap = reset_anim->track_get_interpolation_loop_wrap(rt);5773return;5774}5775}57765777// Hack.5778NodePath np;5779animation->add_track(Animation::TYPE_VALUE);5780animation->track_set_path(animation->get_track_count() - 1, p_path);5781PropertyInfo h = _find_hint_for_track(animation->get_track_count() - 1, np);5782animation->remove_track(animation->get_track_count() - 1); // Hack.5783switch (h.type) {5784case Variant::FLOAT:5785case Variant::VECTOR2:5786case Variant::RECT2:5787case Variant::VECTOR3:5788case Variant::TRANSFORM2D:5789case Variant::VECTOR4:5790case Variant::PLANE:5791case Variant::QUATERNION:5792case Variant::AABB:5793case Variant::BASIS:5794case Variant::TRANSFORM3D:5795case Variant::PROJECTION:5796case Variant::COLOR:5797case Variant::PACKED_FLOAT32_ARRAY:5798case Variant::PACKED_FLOAT64_ARRAY:5799case Variant::PACKED_VECTOR2_ARRAY:5800case Variant::PACKED_VECTOR3_ARRAY:5801case Variant::PACKED_COLOR_ARRAY:5802case Variant::PACKED_VECTOR4_ARRAY: {5803*r_update_mode = Animation::UPDATE_CONTINUOUS;5804} break;5805default: {5806}5807}5808}58095810void AnimationTrackEditor::_new_track_property_selected(const String &p_name) {5811String full_path = String(adding_track_path) + ":" + p_name;58125813EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();58145815Animation::UpdateMode update_mode = Animation::UPDATE_DISCRETE;5816Animation::InterpolationType interp_type = Animation::INTERPOLATION_LINEAR;5817bool loop_wrap = true;5818_fetch_value_track_options(full_path, &update_mode, &interp_type, &loop_wrap);5819if (adding_track_type == Animation::TYPE_BEZIER) {5820Vector<String> subindices;5821{5822// Hack.5823NodePath np;5824animation->add_track(Animation::TYPE_VALUE);5825animation->track_set_path(animation->get_track_count() - 1, full_path);5826PropertyInfo h = _find_hint_for_track(animation->get_track_count() - 1, np);5827animation->remove_track(animation->get_track_count() - 1); // Hack.5828bool valid;5829subindices = _get_bezier_subindices_for_type(h.type, &valid);5830if (!valid) {5831EditorNode::get_singleton()->show_warning(TTR("Invalid track for Bezier (no suitable sub-properties)"));5832return;5833}5834}58355836undo_redo->create_action(TTR("Add Bezier Track"));5837int base_track = animation->get_track_count();5838for (int i = 0; i < subindices.size(); i++) {5839int track_idx = base_track + i;5840undo_redo->add_do_method(animation.ptr(), "add_track", adding_track_type);5841undo_redo->add_do_method(animation.ptr(), "track_set_path", track_idx, full_path + subindices[i]);5842undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_type", track_idx, interp_type);5843undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_loop_wrap", track_idx, loop_wrap);5844undo_redo->add_undo_method(animation.ptr(), "remove_track", base_track);5845}5846undo_redo->commit_action();5847} else {5848bool is_blend_shape = adding_track_type == Animation::TYPE_BLEND_SHAPE;5849if (is_blend_shape) {5850PackedStringArray split = p_name.split("/");5851if (!split.is_empty()) {5852full_path = String(adding_track_path) + ":" + split[split.size() - 1];5853}5854}5855undo_redo->create_action(TTR("Add Track"));5856undo_redo->add_do_method(animation.ptr(), "add_track", adding_track_type);5857undo_redo->add_do_method(animation.ptr(), "track_set_path", animation->get_track_count(), full_path);5858undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_type", animation->get_track_count(), interp_type);5859undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_loop_wrap", animation->get_track_count(), loop_wrap);5860if (!is_blend_shape) {5861undo_redo->add_do_method(animation.ptr(), "value_track_set_update_mode", animation->get_track_count(), update_mode);5862}5863undo_redo->add_undo_method(animation.ptr(), "remove_track", animation->get_track_count());5864undo_redo->commit_action();5865}5866}58675868void AnimationTrackEditor::_timeline_value_changed(double) {5869timeline->update_play_position();58705871_redraw_tracks();5872for (int i = 0; i < track_edits.size(); i++) {5873track_edits[i]->update_play_position();5874}5875_redraw_groups();58765877bezier_edit->queue_redraw();5878bezier_edit->update_play_position();58795880marker_edit->update_play_position();5881}58825883int AnimationTrackEditor::_get_track_selected() {5884for (int i = 0; i < track_edits.size(); i++) {5885if (track_edits[i]->has_focus()) {5886return track_edits[i]->get_track();5887}5888}58895890return -1;5891}58925893void AnimationTrackEditor::_insert_key_from_track(float p_ofs, int p_track) {5894if (read_only) {5895popup_read_only_dialog();5896return;5897}58985899ERR_FAIL_INDEX(p_track, animation->get_track_count());59005901if (snap_keys->is_pressed() && step->get_value() != 0) {5902p_ofs = snap_time(p_ofs);5903}59045905resolve_insertion_offset(p_ofs);59065907while (animation->track_find_key(p_track, p_ofs, Animation::FIND_MODE_APPROX) != -1) { // Make sure insertion point is valid.5908p_ofs += SECOND_DECIMAL;5909}59105911Node *node = root->get_node_or_null(animation->track_get_path(p_track));5912if (!node) {5913EditorNode::get_singleton()->show_warning(TTR("Track path is invalid, so can't add a key."));5914return;5915}59165917// Special handling for this one.5918if (animation->track_get_type(p_track) == Animation::TYPE_METHOD) {5919method_selector->select_method_from_instance(node);59205921insert_key_from_track_call_ofs = p_ofs;5922insert_key_from_track_call_track = p_track;5923return;5924}59255926InsertData id;5927id.path = animation->track_get_path(p_track);5928id.advance = false;5929id.track_idx = p_track;5930id.type = animation->track_get_type(p_track);5931// TRANSLATORS: This describes the target of new animation track, will be inserted into another string.5932id.query = vformat(TTR("node '%s'"), node->get_name());5933id.time = p_ofs;5934// id.value is filled in each case handled below.59355936switch (animation->track_get_type(p_track)) {5937case Animation::TYPE_POSITION_3D: {5938Node3D *base = Object::cast_to<Node3D>(node);59395940if (!base) {5941EditorNode::get_singleton()->show_warning(TTR("Track is not of type Node3D, can't insert key"));5942return;5943}59445945id.value = base->get_position();5946} break;5947case Animation::TYPE_ROTATION_3D: {5948Node3D *base = Object::cast_to<Node3D>(node);59495950if (!base) {5951EditorNode::get_singleton()->show_warning(TTR("Track is not of type Node3D, can't insert key"));5952return;5953}59545955id.value = base->get_transform().basis.operator Quaternion();5956} break;5957case Animation::TYPE_SCALE_3D: {5958Node3D *base = Object::cast_to<Node3D>(node);59595960if (!base) {5961EditorNode::get_singleton()->show_warning(TTR("Track is not of type Node3D, can't insert key"));5962return;5963}59645965id.value = base->get_scale();5966} break;5967case Animation::TYPE_BLEND_SHAPE: {5968MeshInstance3D *base = Object::cast_to<MeshInstance3D>(node);59695970if (!base) {5971EditorNode::get_singleton()->show_warning(TTR("Track is not of type MeshInstance3D, can't insert key"));5972return;5973}59745975id.value = base->get_blend_shape_value(base->find_blend_shape_by_name(id.path.get_subname(0)));5976} break;5977case Animation::TYPE_VALUE: {5978NodePath bp;5979_find_hint_for_track(p_track, bp, &id.value);5980} break;5981case Animation::TYPE_METHOD: {5982Node *base = root->get_node_or_null(animation->track_get_path(p_track));5983ERR_FAIL_NULL(base);59845985method_selector->select_method_from_instance(base);59865987insert_key_from_track_call_ofs = p_ofs;5988insert_key_from_track_call_track = p_track;59895990} break;5991case Animation::TYPE_BEZIER: {5992NodePath bp;5993Variant value;5994_find_hint_for_track(p_track, bp, &value);5995id.value = animation->make_default_bezier_key(value);5996} break;5997case Animation::TYPE_AUDIO: {5998Dictionary ak;5999ak["stream"] = Ref<Resource>();6000ak["start_offset"] = 0;6001ak["end_offset"] = 0;60026003id.value = ak;6004} break;6005case Animation::TYPE_ANIMATION: {6006id.value = StringName("[stop]");6007} break;6008default: {6009// All track types should be handled by now.6010DEV_ASSERT(false);6011}6012}60136014_query_insert(id);6015}60166017void AnimationTrackEditor::_add_method_key(const String &p_method) {6018if (!root->has_node(animation->track_get_path(insert_key_from_track_call_track))) {6019EditorNode::get_singleton()->show_warning(TTR("Track path is invalid, so can't add a method key."));6020return;6021}6022Node *base = root->get_node_or_null(animation->track_get_path(insert_key_from_track_call_track));6023ERR_FAIL_NULL(base);60246025List<MethodInfo> minfo;6026base->get_method_list(&minfo);60276028for (const MethodInfo &E : minfo) {6029if (E.name == p_method) {6030Dictionary d;6031d["method"] = p_method;6032Array params;6033int64_t first_defarg = E.arguments.size() - E.default_arguments.size();60346035for (int64_t i = 0; i < E.arguments.size(); ++i) {6036if (i >= first_defarg) {6037Variant arg = E.default_arguments[i - first_defarg];6038params.push_back(arg);6039} else {6040Callable::CallError ce;6041Variant arg;6042Variant::construct(E.arguments[i].type, arg, nullptr, 0, ce);6043params.push_back(arg);6044}6045}6046d["args"] = params;60476048EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();6049undo_redo->create_action(TTR("Add Method Track Key"));6050undo_redo->add_do_method(animation.ptr(), "track_insert_key", insert_key_from_track_call_track, insert_key_from_track_call_ofs, d);6051undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);6052undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", insert_key_from_track_call_track, insert_key_from_track_call_ofs);6053undo_redo->commit_action();60546055return;6056}6057}60586059EditorNode::get_singleton()->show_warning(TTR("Method not found in object:") + " " + p_method);6060}60616062void AnimationTrackEditor::_key_selected(int p_key, bool p_single, int p_track) {6063ERR_FAIL_INDEX(p_track, animation->get_track_count());6064ERR_FAIL_INDEX(p_key, animation->track_get_key_count(p_track));60656066SelectedKey sk;6067sk.key = p_key;6068sk.track = p_track;60696070if (p_single) {6071_clear_selection();6072}60736074KeyInfo ki;6075ki.pos = animation->track_get_key_time(p_track, p_key);6076selection[sk] = ki;60776078_redraw_tracks();6079_update_key_edit();60806081marker_edit->_clear_selection(marker_edit->is_selection_active());6082}60836084void AnimationTrackEditor::_key_deselected(int p_key, int p_track) {6085ERR_FAIL_INDEX(p_track, animation->get_track_count());6086ERR_FAIL_INDEX(p_key, animation->track_get_key_count(p_track));60876088SelectedKey sk;6089sk.key = p_key;6090sk.track = p_track;60916092selection.erase(sk);60936094_redraw_tracks();6095_update_key_edit();6096}60976098void AnimationTrackEditor::_move_selection_begin() {6099moving_selection = true;6100moving_selection_offset = 0;6101}61026103void AnimationTrackEditor::_move_selection(float p_offset) {6104moving_selection_offset = p_offset;6105_redraw_tracks();6106}61076108struct _AnimMoveRestore {6109int track = 0;6110float time = 0;6111Variant key;6112float transition = 0;6113};6114// Used for undo/redo.61156116void AnimationTrackEditor::_clear_key_edit() {6117if (key_edit) {6118// If key edit is the object being inspected, remove it first.6119if (InspectorDock::get_inspector_singleton()->get_edited_object() == key_edit) {6120EditorNode::get_singleton()->push_item(nullptr);6121}61226123// Then actually delete it.6124memdelete(key_edit);6125key_edit = nullptr;6126}61276128if (multi_key_edit) {6129if (InspectorDock::get_inspector_singleton()->get_edited_object() == multi_key_edit) {6130EditorNode::get_singleton()->push_item(nullptr);6131}61326133memdelete(multi_key_edit);6134multi_key_edit = nullptr;6135}6136}61376138void AnimationTrackEditor::_clear_selection(bool p_update) {6139selection.clear();61406141if (p_update) {6142_redraw_tracks();6143}61446145_clear_key_edit();6146}61476148void AnimationTrackEditor::_update_key_edit() {6149_clear_key_edit();6150if (animation.is_null()) {6151return;6152}61536154if (selection.size() == 1) {6155key_edit = memnew(AnimationTrackKeyEdit);6156key_edit->animation = animation;6157key_edit->animation_read_only = read_only;6158key_edit->track = selection.front()->key().track;6159key_edit->use_fps = timeline->is_using_fps();6160key_edit->editor = this;61616162int key_id = selection.front()->key().key;6163if (key_id >= animation->track_get_key_count(key_edit->track)) {6164_clear_key_edit();6165return; // Probably in the process of rearranging the keys.6166}6167float ofs = animation->track_get_key_time(key_edit->track, key_id);6168key_edit->key_ofs = ofs;6169key_edit->root_path = root;61706171NodePath np;6172key_edit->hint = _find_hint_for_track(key_edit->track, np);6173key_edit->base = np;61746175EditorNode::get_singleton()->push_item(key_edit);6176} else if (selection.size() > 1) {6177multi_key_edit = memnew(AnimationMultiTrackKeyEdit);6178multi_key_edit->animation = animation;6179multi_key_edit->animation_read_only = read_only;6180multi_key_edit->editor = this;61816182RBMap<int, List<float>> key_ofs_map;6183RBMap<int, NodePath> base_map;6184int first_track = -1;6185for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {6186int track = E.key.track;6187if (first_track < 0) {6188first_track = track;6189}61906191if (!key_ofs_map.has(track)) {6192key_ofs_map[track] = List<float>();6193base_map[track] = NodePath();6194}61956196int key_id = E.key.key;6197if (key_id >= animation->track_get_key_count(track)) {6198_clear_key_edit();6199return; // Probably in the process of rearranging the keys.6200}6201key_ofs_map[track].push_back(animation->track_get_key_time(track, E.key.key));6202}6203multi_key_edit->key_ofs_map = key_ofs_map;6204multi_key_edit->base_map = base_map;6205multi_key_edit->hint = _find_hint_for_track(first_track, base_map[first_track]);6206multi_key_edit->use_fps = timeline->is_using_fps();6207multi_key_edit->root_path = root;62086209EditorNode::get_singleton()->push_item(multi_key_edit);6210}6211}62126213void AnimationTrackEditor::_clear_selection_for_anim(const Ref<Animation> &p_anim) {6214if (animation != p_anim) {6215return;6216}62176218_clear_selection();6219}62206221void AnimationTrackEditor::_select_at_anim(const Ref<Animation> &p_anim, int p_track, float p_pos) {6222if (animation != p_anim) {6223return;6224}62256226int idx = animation->track_find_key(p_track, p_pos, Animation::FIND_MODE_APPROX);6227ERR_FAIL_COND(idx < 0);62286229SelectedKey sk;6230sk.track = p_track;6231sk.key = idx;6232KeyInfo ki;6233ki.pos = p_pos;62346235selection.insert(sk, ki);6236_update_key_edit();62376238marker_edit->_clear_selection(marker_edit->is_selection_active());6239}62406241void AnimationTrackEditor::_move_selection_commit() {6242EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();6243undo_redo->create_action(TTR("Animation Move Keys"));62446245List<_AnimMoveRestore> to_restore;62466247float motion = moving_selection_offset;6248// 1 - remove the keys.6249for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6250undo_redo->add_do_method(animation.ptr(), "track_remove_key", E->key().track, E->key().key);6251}6252// 2 - Remove overlapped keys.6253for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6254float newtime = E->get().pos + motion;6255int idx = animation->track_find_key(E->key().track, newtime, Animation::FIND_MODE_APPROX);6256if (idx == -1) {6257continue;6258}6259SelectedKey sk;6260sk.key = idx;6261sk.track = E->key().track;6262if (selection.has(sk)) {6263continue; // Already in selection, don't save.6264}62656266undo_redo->add_do_method(animation.ptr(), "track_remove_key_at_time", E->key().track, newtime);6267_AnimMoveRestore amr;62686269amr.key = animation->track_get_key_value(E->key().track, idx);6270amr.track = E->key().track;6271amr.time = newtime;6272amr.transition = animation->track_get_key_transition(E->key().track, idx);62736274to_restore.push_back(amr);6275}62766277// 3 - Move the keys (Reinsert them).6278for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6279float newpos = E->get().pos + motion;6280undo_redo->add_do_method(animation.ptr(), "track_insert_key", E->key().track, newpos, animation->track_get_key_value(E->key().track, E->key().key), animation->track_get_key_transition(E->key().track, E->key().key));6281}62826283// 4 - (Undo) Remove inserted keys.6284for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6285float newpos = E->get().pos + motion;6286undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", E->key().track, newpos);6287}62886289// 5 - (Undo) Reinsert keys.6290for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6291undo_redo->add_undo_method(animation.ptr(), "track_insert_key", E->key().track, E->get().pos, animation->track_get_key_value(E->key().track, E->key().key), animation->track_get_key_transition(E->key().track, E->key().key));6292}62936294// 6 - (Undo) Reinsert overlapped keys.6295for (_AnimMoveRestore &amr : to_restore) {6296undo_redo->add_undo_method(animation.ptr(), "track_insert_key", amr.track, amr.time, amr.key, amr.transition);6297}62986299undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);6300undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);63016302// 7 - Reselect.6303for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6304float oldpos = E->get().pos;6305float newpos = oldpos + motion;63066307undo_redo->add_do_method(this, "_select_at_anim", animation, E->key().track, newpos);6308undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, oldpos);6309}63106311moving_selection = false;6312undo_redo->add_do_method(this, "_redraw_tracks");6313undo_redo->add_undo_method(this, "_redraw_tracks");63146315// Update key frame.6316AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();6317if (ape) {6318undo_redo->add_do_method(ape, "_animation_update_key_frame");6319undo_redo->add_undo_method(ape, "_animation_update_key_frame");6320}63216322undo_redo->commit_action();6323}63246325void AnimationTrackEditor::_move_selection_cancel() {6326moving_selection = false;6327_redraw_tracks();6328}63296330bool AnimationTrackEditor::is_moving_selection() const {6331return moving_selection;6332}63336334float AnimationTrackEditor::get_moving_selection_offset() const {6335return moving_selection_offset;6336}63376338void AnimationTrackEditor::_box_selection_draw() {6339const Rect2 selection_rect = Rect2(Point2(), box_selection->get_size());6340box_selection->draw_rect(selection_rect, get_theme_color(SNAME("box_selection_fill_color"), EditorStringName(Editor)));6341box_selection->draw_rect(selection_rect, get_theme_color(SNAME("box_selection_stroke_color"), EditorStringName(Editor)), false, Math::round(EDSCALE));6342}63436344void AnimationTrackEditor::_scroll_input(const Ref<InputEvent> &p_event) {6345if (!box_selecting) {6346if (panner->gui_input(p_event, scroll->get_global_rect())) {6347scroll->accept_event();6348return;6349}6350}63516352Ref<InputEventMouseButton> mb = p_event;63536354if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT) {6355if (mb->is_pressed()) {6356box_selecting = true;6357box_selecting_from = scroll->get_global_transform().xform(mb->get_position());6358box_select_rect = Rect2();6359} else if (box_selecting) {6360if (box_selection->is_visible_in_tree()) {6361// Only if moved.6362for (int i = 0; i < track_edits.size(); i++) {6363Rect2 local_rect = box_select_rect;6364local_rect.position -= track_edits[i]->get_global_position();6365track_edits[i]->append_to_selection(local_rect, mb->is_command_or_control_pressed());6366}63676368if (_get_track_selected() == -1 && track_edits.size() > 0) { // Minimal hack to make shortcuts work.6369track_edits[track_edits.size() - 1]->grab_focus();6370}6371} else if (!mb->is_command_or_control_pressed() && !mb->is_shift_pressed()) {6372_clear_selection(true); // Clear it.6373}63746375box_selection->hide();6376box_selecting = false;6377}6378}63796380Ref<InputEventMouseMotion> mm = p_event;63816382if (mm.is_valid() && box_selecting) {6383if (!mm->get_button_mask().has_flag(MouseButtonMask::LEFT)) {6384// No longer.6385box_selection->hide();6386box_selecting = false;6387return;6388}63896390if (!box_selection->is_visible_in_tree()) {6391if (!mm->is_command_or_control_pressed() && !mm->is_shift_pressed()) {6392_clear_selection(true);6393}6394box_selection->show();6395}63966397Vector2 from = box_selecting_from;6398Vector2 to = scroll->get_global_transform().xform(mm->get_position());63996400box_selecting_to = to;64016402if (from.x > to.x) {6403SWAP(from.x, to.x);6404}64056406if (from.y > to.y) {6407SWAP(from.y, to.y);6408}64096410Rect2 rect(from, to - from);6411box_selection->set_rect(Rect2(from - scroll->get_global_position(), rect.get_size()));6412box_select_rect = rect;6413}6414}64156416void AnimationTrackEditor::_toggle_bezier_edit() {6417if (bezier_edit->is_visible()) {6418_cancel_bezier_edit();6419} else {6420int track_count = animation->get_track_count();6421for (int i = 0; i < track_count; ++i) {6422if (animation->track_get_type(i) == Animation::TrackType::TYPE_BEZIER) {6423_bezier_edit(i);6424return;6425}6426}6427}6428}64296430void AnimationTrackEditor::_scroll_changed(const Vector2 &p_val) {6431if (box_selecting) {6432const Vector2 scroll_difference = p_val - prev_scroll_position;64336434Vector2 from = box_selecting_from - scroll_difference;6435Vector2 to = box_selecting_to;64366437box_selecting_from = from;64386439if (from.x > to.x) {6440SWAP(from.x, to.x);6441}64426443if (from.y > to.y) {6444SWAP(from.y, to.y);6445}64466447Rect2 rect(from, to - from);6448box_selection->set_rect(Rect2(from - scroll->get_global_position(), rect.get_size()));6449box_select_rect = rect;6450}64516452prev_scroll_position = p_val;6453}64546455void AnimationTrackEditor::_v_scroll_changed(float p_val) {6456_scroll_changed(Vector2(prev_scroll_position.x, p_val));6457}64586459void AnimationTrackEditor::_h_scroll_changed(float p_val) {6460_scroll_changed(Vector2(p_val, prev_scroll_position.y));6461}64626463void AnimationTrackEditor::_pan_callback(Vector2 p_scroll_vec, Ref<InputEvent> p_event) {6464Ref<InputEventWithModifiers> iewm = p_event;6465if (iewm.is_valid() && iewm->is_alt_pressed()) {6466if (p_scroll_vec.x < 0 || p_scroll_vec.y < 0) {6467goto_prev_step(true);6468} else {6469goto_next_step(true);6470}6471} else {6472timeline->set_value(timeline->get_value() - p_scroll_vec.x / timeline->get_zoom_scale());6473scroll->set_v_scroll(scroll->get_v_scroll() - p_scroll_vec.y);6474}6475}64766477void AnimationTrackEditor::_zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref<InputEvent> p_event) {6478timeline->_zoom_callback(p_zoom_factor, p_origin, p_event);6479}64806481void AnimationTrackEditor::_cancel_bezier_edit() {6482bezier_edit->hide();6483box_selection_container->show();6484bezier_edit_icon->set_pressed(false);6485auto_fit->show();6486auto_fit_bezier->hide();6487}64886489void AnimationTrackEditor::_bezier_edit(int p_for_track) {6490_clear_selection(); // Bezier probably wants to use a separate selection mode.6491bezier_edit->set_root(root);6492bezier_edit->set_animation_and_track(animation, p_for_track, read_only);6493box_selection_container->hide();6494bezier_edit->show();6495auto_fit->hide();6496auto_fit_bezier->show();6497// Search everything within the track and curve - edit it.6498}64996500void AnimationTrackEditor::_bezier_track_set_key_handle_mode(Animation *p_anim, int p_track, int p_index, Animation::HandleMode p_mode, Animation::HandleSetMode p_set_mode) {6501ERR_FAIL_NULL(p_anim);6502p_anim->bezier_track_set_key_handle_mode(p_track, p_index, p_mode, p_set_mode);6503}65046505void AnimationTrackEditor::_bezier_track_set_key_handle_mode_at_time(Animation *p_anim, int p_track, float p_time, Animation::HandleMode p_mode, Animation::HandleSetMode p_set_mode) {6506ERR_FAIL_NULL(p_anim);6507int index = p_anim->track_find_key(p_track, p_time, Animation::FIND_MODE_APPROX);6508ERR_FAIL_COND(index < 0);6509_bezier_track_set_key_handle_mode(p_anim, p_track, index, p_mode, p_set_mode);6510}65116512void AnimationTrackEditor::_anim_duplicate_keys(float p_ofs, bool p_ofs_valid, int p_track) {6513if (selection.size() && animation.is_valid()) {6514int top_track = 0x7FFFFFFF;6515float top_time = 1e10;6516for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6517const SelectedKey &sk = E->key();65186519float t = animation->track_get_key_time(sk.track, sk.key);6520if (t < top_time) {6521top_time = t;6522}6523if (sk.track < top_track) {6524top_track = sk.track;6525}6526}6527ERR_FAIL_COND(top_track == 0x7FFFFFFF || top_time == 1e10);65286529int start_track = p_track;6530if (p_track == -1) { // Duplicating from shortcut or Edit menu.6531bool is_valid_track_selected = _get_track_selected() >= 0 && _get_track_selected() < animation->get_track_count();6532start_track = is_valid_track_selected ? _get_track_selected() : top_track;6533}65346535bool all_compatible = true;65366537for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6538const SelectedKey &sk = E->key();6539int dst_track = sk.track + (start_track - top_track);65406541if (dst_track < 0 || dst_track >= animation->get_track_count()) {6542all_compatible = false;6543break;6544}65456546Variant::Type value_type = animation->track_get_key_value(sk.track, sk.key).get_type();6547Animation::TrackType track_type = animation->track_get_type(sk.track);6548if (!_is_track_compatible(dst_track, value_type, track_type)) {6549all_compatible = false;6550break;6551}6552}65536554ERR_FAIL_COND_MSG(!all_compatible, "Duplicate failed: Not all animation keys were compatible with their target tracks");65556556EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();6557undo_redo->create_action(TTR("Animation Duplicate Keys"));65586559List<Pair<int, float>> new_selection_values;65606561for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6562const SelectedKey &sk = E->key();65636564float t = animation->track_get_key_time(sk.track, sk.key);6565float insert_pos = p_ofs_valid ? p_ofs : timeline->get_play_position();65666567if (p_ofs_valid) {6568if (snap_keys->is_pressed() && step->get_value() != 0) {6569insert_pos = snap_time(insert_pos);6570}6571}65726573float dst_time = t + (insert_pos - top_time);6574int dst_track = sk.track + (start_track - top_track);65756576if (dst_track < 0 || dst_track >= animation->get_track_count()) {6577continue;6578}65796580int existing_idx = animation->track_find_key(dst_track, dst_time, Animation::FIND_MODE_APPROX);65816582Variant value = animation->track_get_key_value(sk.track, sk.key);6583bool key_is_bezier = animation->track_get_type(sk.track) == Animation::TYPE_BEZIER;6584bool track_is_bezier = animation->track_get_type(dst_track) == Animation::TYPE_BEZIER;6585if (key_is_bezier && !track_is_bezier) {6586value = AnimationBezierTrackEdit::get_bezier_key_value(value);6587} else if (!key_is_bezier && track_is_bezier) {6588value = animation->make_default_bezier_key(value);6589}65906591undo_redo->add_do_method(animation.ptr(), "track_insert_key", dst_track, dst_time, value, animation->track_get_key_transition(E->key().track, E->key().key));6592undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", dst_track, dst_time);65936594Pair<int, float> p;6595p.first = dst_track;6596p.second = dst_time;6597new_selection_values.push_back(p);65986599if (existing_idx != -1) {6600undo_redo->add_undo_method(animation.ptr(), "track_insert_key", dst_track, dst_time, animation->track_get_key_value(dst_track, existing_idx), animation->track_get_key_transition(dst_track, existing_idx));6601}6602}66036604undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);6605undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);66066607// Reselect duplicated.6608RBMap<SelectedKey, KeyInfo> new_selection;6609for (const Pair<int, float> &E : new_selection_values) {6610undo_redo->add_do_method(this, "_select_at_anim", animation, E.first, E.second);6611}6612for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6613undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, E->get().pos);6614}66156616undo_redo->add_do_method(this, "_redraw_tracks");6617undo_redo->add_undo_method(this, "_redraw_tracks");6618undo_redo->commit_action();6619}6620}66216622void AnimationTrackEditor::_anim_copy_keys(bool p_cut) {6623if (is_selection_active() && animation.is_valid()) {6624int top_track = 0x7FFFFFFF;6625float top_time = 1e10;66266627for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6628const SelectedKey &sk = E->key();66296630float t = animation->track_get_key_time(sk.track, sk.key);6631if (t < top_time) {6632top_time = t;6633}6634if (sk.track < top_track) {6635top_track = sk.track;6636}6637}66386639ERR_FAIL_COND(top_track == 0x7FFFFFFF || top_time == 1e10);66406641_set_key_clipboard(top_track, top_time, selection);66426643if (p_cut) {6644EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();6645undo_redo->create_action(TTR("Animation Cut Keys"), UndoRedo::MERGE_DISABLE, animation.ptr());6646undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);6647undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);6648for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6649int track_idx = E->key().track;6650int key_idx = E->key().key;6651float time = E->value().pos;6652undo_redo->add_do_method(animation.ptr(), "track_remove_key_at_time", track_idx, time);6653undo_redo->add_undo_method(animation.ptr(), "track_insert_key", track_idx, time, animation->track_get_key_value(track_idx, key_idx), animation->track_get_key_transition(track_idx, key_idx));6654}6655for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6656undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, E->value().pos);6657}6658undo_redo->commit_action();6659}6660}6661}66626663void AnimationTrackEditor::_set_key_clipboard(int p_top_track, float p_top_time, RBMap<SelectedKey, KeyInfo> &p_keys) {6664key_clipboard.keys.clear();6665key_clipboard.top_track = p_top_track;6666for (RBMap<SelectedKey, KeyInfo>::Element *E = p_keys.back(); E; E = E->prev()) {6667KeyClipboard::Key k;6668k.value = animation->track_get_key_value(E->key().track, E->key().key);6669k.transition = animation->track_get_key_transition(E->key().track, E->key().key);6670k.time = E->value().pos - p_top_time;6671k.track = E->key().track - p_top_track;6672k.track_type = animation->track_get_type(E->key().track);66736674key_clipboard.keys.push_back(k);6675}6676}66776678void AnimationTrackEditor::_anim_paste_keys(float p_ofs, bool p_ofs_valid, int p_track) {6679if (is_key_clipboard_active() && animation.is_valid()) {6680int start_track = p_track;6681if (p_track == -1) { // Pasting from shortcut or Edit menu.6682bool is_valid_track_selected = _get_track_selected() >= 0 && _get_track_selected() < animation->get_track_count();6683start_track = is_valid_track_selected ? _get_track_selected() : key_clipboard.top_track;6684}66856686bool all_compatible = true;66876688for (int i = 0; i < key_clipboard.keys.size(); i++) {6689const KeyClipboard::Key key = key_clipboard.keys[i];66906691int dst_track = key.track + start_track;66926693if (dst_track < 0 || dst_track >= animation->get_track_count()) {6694all_compatible = false;6695break;6696}66976698if (!_is_track_compatible(dst_track, key.value.get_type(), key.track_type)) {6699all_compatible = false;6700break;6701}6702}67036704ERR_FAIL_COND_MSG(!all_compatible, "Paste failed: Not all animation keys were compatible with their target tracks");67056706EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();6707undo_redo->create_action(TTR("Animation Paste Keys"));6708List<Pair<int, float>> new_selection_values;67096710for (int i = 0; i < key_clipboard.keys.size(); i++) {6711const KeyClipboard::Key key = key_clipboard.keys[i];67126713float insert_pos = p_ofs_valid ? p_ofs : timeline->get_play_position();67146715if (p_ofs_valid) {6716if (snap_keys->is_pressed() && step->get_value() != 0) {6717insert_pos = snap_time(insert_pos);6718}6719}67206721float dst_time = key.time + insert_pos;6722int dst_track = key.track + start_track;67236724int existing_idx = animation->track_find_key(dst_track, dst_time, Animation::FIND_MODE_APPROX);67256726Variant value = key.value;6727bool key_is_bezier = key.track_type == Animation::TYPE_BEZIER;6728bool track_is_bezier = animation->track_get_type(dst_track) == Animation::TYPE_BEZIER;6729if (key_is_bezier && !track_is_bezier) {6730value = AnimationBezierTrackEdit::get_bezier_key_value(value);6731} else if (!key_is_bezier && track_is_bezier) {6732value = animation->make_default_bezier_key(value);6733}67346735undo_redo->add_do_method(animation.ptr(), "track_insert_key", dst_track, dst_time, value, key.transition);6736undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", dst_track, dst_time);67376738Pair<int, float> p;6739p.first = dst_track;6740p.second = dst_time;6741new_selection_values.push_back(p);67426743if (existing_idx != -1) {6744undo_redo->add_undo_method(animation.ptr(), "track_insert_key", dst_track, dst_time, animation->track_get_key_value(dst_track, existing_idx), animation->track_get_key_transition(dst_track, existing_idx));6745}6746}67476748undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);6749undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);67506751// Reselect pasted.6752for (const Pair<int, float> &E : new_selection_values) {6753undo_redo->add_do_method(this, "_select_at_anim", animation, E.first, E.second);6754}6755for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6756undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, E->get().pos);6757}67586759undo_redo->add_do_method(this, "_redraw_tracks");6760undo_redo->add_undo_method(this, "_redraw_tracks");6761undo_redo->commit_action();6762}6763}67646765bool AnimationTrackEditor::_is_track_compatible(int p_target_track_idx, Variant::Type p_source_value_type, Animation::TrackType p_source_track_type) {6766if (animation.is_valid()) {6767Animation::TrackType target_track_type = animation->track_get_type(p_target_track_idx);6768bool track_types_equal = target_track_type == p_source_track_type;6769bool is_source_vector3_type = p_source_track_type == Animation::TYPE_POSITION_3D || p_source_track_type == Animation::TYPE_SCALE_3D || p_source_track_type == Animation::TYPE_ROTATION_3D;6770bool is_source_bezier = p_source_track_type == Animation::TYPE_BEZIER;6771switch (target_track_type) {6772case Animation::TYPE_POSITION_3D:6773case Animation::TYPE_SCALE_3D:6774return p_source_value_type == Variant::VECTOR3;6775case Animation::TYPE_ROTATION_3D:6776return p_source_value_type == Variant::QUATERNION;6777case Animation::TYPE_BEZIER:6778return track_types_equal || p_source_value_type == Variant::FLOAT;6779case Animation::TYPE_VALUE:6780if (track_types_equal || is_source_vector3_type || is_source_bezier) {6781bool path_valid = false;6782Variant::Type property_type = Variant::NIL;67836784AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();6785if (ape) {6786AnimationPlayer *ap = ape->get_player();6787if (ap) {6788NodePath npath = animation->track_get_path(p_target_track_idx);6789Node *a_ap_root_node = ap->get_node(ap->get_root_node());6790Node *nd = nullptr;6791// We must test that we have a valid a_ap_root_node before trying to access its content to init the nd Node.6792if (a_ap_root_node) {6793nd = a_ap_root_node->get_node(NodePath(npath.get_concatenated_names()));6794}6795if (nd) {6796StringName prop = npath.get_concatenated_subnames();6797PropertyInfo prop_info;6798path_valid = ClassDB::get_property_info(nd->get_class(), prop, &prop_info);6799property_type = prop_info.type;6800}6801}6802}68036804if (path_valid) {6805if (is_source_bezier) {6806p_source_value_type = Variant::FLOAT;6807}6808return property_type == p_source_value_type;6809} else {6810if (animation->track_get_key_count(p_target_track_idx) > 0) {6811Variant::Type first_key_type = animation->track_get_key_value(p_target_track_idx, 0).get_type();6812return first_key_type == p_source_value_type;6813}6814return true; // Type is Undefined.6815}6816}6817return false;6818default: // Works for TYPE_ANIMATION; TYPE_AUDIO; TYPE_CALL_METHOD; BLEND_SHAPE.6819return track_types_equal;6820}6821}6822return false;6823}68246825void AnimationTrackEditor::_edit_menu_about_to_popup() {6826AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();6827edit->get_popup()->set_item_disabled(edit->get_popup()->get_item_index(EDIT_ADD_RESET_KEY), !can_add_reset_key() || animation == player->get_animation(SceneStringName(RESET)));6828edit->get_popup()->set_item_disabled(edit->get_popup()->get_item_index(EDIT_APPLY_RESET), !player->can_apply_reset());68296830bool has_length = false;6831for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {6832if (animation->track_get_type(E.key.track) == Animation::TYPE_AUDIO && animation->audio_track_get_key_stream(E.key.track, E.key.key).is_valid()) {6833has_length = true;6834break;6835}6836}6837edit->get_popup()->set_item_disabled(edit->get_popup()->get_item_index(EDIT_SET_START_OFFSET), !has_length);6838edit->get_popup()->set_item_disabled(edit->get_popup()->get_item_index(EDIT_SET_END_OFFSET), !has_length);6839}68406841void AnimationTrackEditor::goto_prev_step(bool p_from_mouse_event) {6842if (animation.is_null()) {6843return;6844}6845float anim_step = animation->get_step();6846if (anim_step == 0.0) {6847anim_step = 1.0;6848}6849if (p_from_mouse_event && Input::get_singleton()->is_key_pressed(Key::SHIFT)) {6850// Use more precise snapping when holding Shift.6851// This is used when scrobbling the timeline using Alt + Mouse wheel.6852anim_step *= 0.25;6853}68546855float pos = timeline->get_play_position();6856pos = Math::snapped(pos - anim_step, anim_step);6857if (pos < 0.0) {6858pos = 0.0;6859}6860set_anim_pos(pos);6861_timeline_changed(pos, false);6862}68636864void AnimationTrackEditor::goto_next_step(bool p_from_mouse_event, bool p_timeline_only) {6865if (animation.is_null()) {6866return;6867}6868float anim_step = animation->get_step();6869if (anim_step == 0.0) {6870anim_step = 1.0;6871}6872if (p_from_mouse_event && Input::get_singleton()->is_key_pressed(Key::SHIFT)) {6873// Use more precise snapping when holding Shift.6874// This is used when scrobbling the timeline using Alt + Mouse wheel.6875// Do not use precise snapping when using the menu action or keyboard shortcut,6876// as the default keyboard shortcut requires pressing Shift.6877anim_step *= 0.25;6878}68796880float pos = timeline->get_play_position();68816882pos = Math::snapped(pos + anim_step, anim_step);6883if (pos > animation->get_length()) {6884pos = animation->get_length();6885}6886set_anim_pos(pos);68876888_timeline_changed(pos, p_timeline_only);6889}68906891void AnimationTrackEditor::_edit_menu_pressed(int p_option) {6892switch (p_option) {6893case EDIT_COPY_TRACKS: {6894track_copy_select->clear();6895TreeItem *troot = track_copy_select->create_item();68966897for (int i = 0; i < animation->get_track_count(); i++) {6898NodePath path = animation->track_get_path(i);6899Node *node = nullptr;69006901if (root) {6902node = root->get_node_or_null(path);6903}69046905String text;6906Ref<Texture2D> icon = get_editor_theme_icon(SNAME("Node"));6907if (node) {6908if (has_theme_icon(node->get_class(), EditorStringName(EditorIcons))) {6909icon = get_editor_theme_icon(node->get_class());6910}69116912text = node->get_name();6913Vector<StringName> sn = path.get_subnames();6914for (int j = 0; j < sn.size(); j++) {6915text += ".";6916text += sn[j];6917}69186919path = NodePath(node->get_path().get_names(), path.get_subnames(), true); // Store full path instead for copying.6920} else {6921text = String(path);6922int sep = text.find_char(':');6923if (sep != -1) {6924text = text.substr(sep + 1);6925}6926}69276928String track_type;6929switch (animation->track_get_type(i)) {6930case Animation::TYPE_POSITION_3D:6931track_type = TTR("Position");6932break;6933case Animation::TYPE_ROTATION_3D:6934track_type = TTR("Rotation");6935break;6936case Animation::TYPE_SCALE_3D:6937track_type = TTR("Scale");6938break;6939case Animation::TYPE_BLEND_SHAPE:6940track_type = TTR("BlendShape");6941break;6942case Animation::TYPE_METHOD:6943track_type = TTR("Methods");6944break;6945case Animation::TYPE_BEZIER:6946track_type = TTR("Bezier");6947break;6948case Animation::TYPE_AUDIO:6949track_type = TTR("Audio");6950break;6951default: {6952};6953}6954if (!track_type.is_empty()) {6955text += vformat(" (%s)", track_type);6956}69576958TreeItem *it = track_copy_select->create_item(troot);6959it->set_editable(0, true);6960it->set_selectable(0, true);6961it->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);6962it->set_icon(0, icon);6963it->set_text(0, text);6964Dictionary md;6965md["track_idx"] = i;6966md["path"] = path;6967it->set_metadata(0, md);6968}69696970track_copy_dialog->popup_centered(Size2(350, 500) * EDSCALE);6971} break;6972case EDIT_COPY_TRACKS_CONFIRM: {6973track_clipboard.clear();6974TreeItem *tree_root = track_copy_select->get_root();6975if (tree_root) {6976TreeItem *it = tree_root->get_first_child();6977while (it) {6978Dictionary md = it->get_metadata(0);6979int idx = md["track_idx"];6980if (it->is_checked(0) && idx >= 0 && idx < animation->get_track_count()) {6981TrackClipboard tc;6982tc.base_path = animation->track_get_path(idx);6983tc.full_path = md["path"];6984tc.track_type = animation->track_get_type(idx);6985tc.interp_type = animation->track_get_interpolation_type(idx);6986if (tc.track_type == Animation::TYPE_VALUE) {6987tc.update_mode = animation->value_track_get_update_mode(idx);6988}6989if (tc.track_type == Animation::TYPE_AUDIO) {6990tc.use_blend = animation->audio_track_is_use_blend(idx);6991}6992tc.loop_wrap = animation->track_get_interpolation_loop_wrap(idx);6993tc.enabled = animation->track_is_enabled(idx);6994for (int i = 0; i < animation->track_get_key_count(idx); i++) {6995TrackClipboard::Key k;6996k.time = animation->track_get_key_time(idx, i);6997k.value = animation->track_get_key_value(idx, i);6998k.transition = animation->track_get_key_transition(idx, i);6999tc.keys.push_back(k);7000}7001track_clipboard.push_back(tc);7002}7003it = it->get_next();7004}7005}7006} break;7007case EDIT_PASTE_TRACKS: {7008if (track_clipboard.is_empty()) {7009EditorNode::get_singleton()->show_warning(TTR("Clipboard is empty!"));7010break;7011}70127013int base_track = animation->get_track_count();7014EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7015undo_redo->create_action(TTR("Paste Tracks"));7016for (int i = 0; i < track_clipboard.size(); i++) {7017undo_redo->add_do_method(animation.ptr(), "add_track", track_clipboard[i].track_type);7018Node *exists = nullptr;7019NodePath path = track_clipboard[i].base_path;70207021if (root) {7022NodePath np = track_clipboard[i].full_path;7023exists = root->get_node_or_null(np);7024if (exists) {7025path = NodePath(root->get_path_to(exists).get_names(), track_clipboard[i].full_path.get_subnames(), false);7026}7027}70287029undo_redo->add_do_method(animation.ptr(), "track_set_path", base_track, path);7030undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_type", base_track, track_clipboard[i].interp_type);7031undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_loop_wrap", base_track, track_clipboard[i].loop_wrap);7032undo_redo->add_do_method(animation.ptr(), "track_set_enabled", base_track, track_clipboard[i].enabled);7033if (track_clipboard[i].track_type == Animation::TYPE_VALUE) {7034undo_redo->add_do_method(animation.ptr(), "value_track_set_update_mode", base_track, track_clipboard[i].update_mode);7035}7036if (track_clipboard[i].track_type == Animation::TYPE_AUDIO) {7037undo_redo->add_do_method(animation.ptr(), "audio_track_set_use_blend", base_track, track_clipboard[i].use_blend);7038}70397040for (int j = 0; j < track_clipboard[i].keys.size(); j++) {7041undo_redo->add_do_method(animation.ptr(), "track_insert_key", base_track, track_clipboard[i].keys[j].time, track_clipboard[i].keys[j].value, track_clipboard[i].keys[j].transition);7042}70437044undo_redo->add_undo_method(animation.ptr(), "remove_track", animation->get_track_count());70457046base_track++;7047}70487049undo_redo->commit_action();7050} break;7051case EDIT_SCALE_SELECTION: {7052scale_dialog->popup_centered(Size2(200, 100) * EDSCALE);7053scale->get_line_edit()->grab_focus();7054scale_from_cursor = false;7055} break;7056case EDIT_SCALE_FROM_CURSOR: {7057scale_dialog->popup_centered(Size2(200, 100) * EDSCALE);7058scale->get_line_edit()->grab_focus();7059scale_from_cursor = true;7060} break;7061case EDIT_SCALE_CONFIRM: {7062if (selection.is_empty()) {7063return;7064}70657066float from_t = 1e20;7067float to_t = -1e20;7068float len = -1e20;7069float pivot = 0;70707071for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {7072float t = animation->track_get_key_time(E.key.track, E.key.key);7073if (t < from_t) {7074from_t = t;7075}7076if (t > to_t) {7077to_t = t;7078}7079}70807081len = to_t - from_t;7082if (scale_from_cursor) {7083pivot = timeline->get_play_position();7084} else {7085pivot = from_t;7086}70877088float s = scale->get_value();7089ERR_FAIL_COND_MSG(s == 0, "Can't scale to 0.");70907091EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7092undo_redo->create_action(TTR("Animation Scale Keys"));70937094List<_AnimMoveRestore> to_restore;70957096// 1 - Remove the keys.7097for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {7098undo_redo->add_do_method(animation.ptr(), "track_remove_key", E->key().track, E->key().key);7099}7100// 2 - Remove overlapped keys.7101for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {7102float newtime = (E->get().pos - from_t) * s + from_t;7103int idx = animation->track_find_key(E->key().track, newtime, Animation::FIND_MODE_APPROX);7104if (idx == -1) {7105continue;7106}7107SelectedKey sk;7108sk.key = idx;7109sk.track = E->key().track;7110if (selection.has(sk)) {7111continue; // Already in selection, don't save.7112}71137114undo_redo->add_do_method(animation.ptr(), "track_remove_key_at_time", E->key().track, newtime);7115_AnimMoveRestore amr;71167117amr.key = animation->track_get_key_value(E->key().track, idx);7118amr.track = E->key().track;7119amr.time = newtime;7120amr.transition = animation->track_get_key_transition(E->key().track, idx);71217122to_restore.push_back(amr);7123}71247125#define NEW_POS(m_ofs) (((s > 0) ? m_ofs : from_t + (len - (m_ofs - from_t))) - pivot) * Math::abs(s) + pivot7126// 3 - Move the keys (re insert them).7127for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {7128float newpos = NEW_POS(E->get().pos);7129undo_redo->add_do_method(animation.ptr(), "track_insert_key", E->key().track, newpos, animation->track_get_key_value(E->key().track, E->key().key), animation->track_get_key_transition(E->key().track, E->key().key));7130}71317132// 4 - (Undo) Remove inserted keys.7133for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {7134float newpos = NEW_POS(E->get().pos);7135undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", E->key().track, newpos);7136}71377138// 5 - (Undo) Reinsert keys.7139for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {7140undo_redo->add_undo_method(animation.ptr(), "track_insert_key", E->key().track, E->get().pos, animation->track_get_key_value(E->key().track, E->key().key), animation->track_get_key_transition(E->key().track, E->key().key));7141}71427143// 6 - (Undo) Reinsert overlapped keys.7144for (_AnimMoveRestore &amr : to_restore) {7145undo_redo->add_undo_method(animation.ptr(), "track_insert_key", amr.track, amr.time, amr.key, amr.transition);7146}71477148undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);7149undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);71507151// 7 - Reselect.7152for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {7153float oldpos = E->get().pos;7154float newpos = NEW_POS(oldpos);7155if (newpos >= 0) {7156undo_redo->add_do_method(this, "_select_at_anim", animation, E->key().track, newpos);7157}7158undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, oldpos);7159}7160#undef NEW_POS71617162undo_redo->add_do_method(this, "_redraw_tracks");7163undo_redo->add_undo_method(this, "_redraw_tracks");7164undo_redo->commit_action();7165} break;71667167case EDIT_SET_START_OFFSET: {7168EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7169undo_redo->create_action(TTR("Animation Set Start Offset"), UndoRedo::MERGE_ENDS);7170for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {7171if (animation->track_get_type(E.key.track) != Animation::TYPE_AUDIO) {7172continue;7173}7174Ref<AudioStream> stream = animation->audio_track_get_key_stream(E.key.track, E.key.key);7175if (stream.is_null()) {7176continue;7177}7178double len = stream->get_length() - animation->audio_track_get_key_end_offset(E.key.track, E.key.key);7179real_t prev_offset = animation->audio_track_get_key_start_offset(E.key.track, E.key.key);7180double prev_time = animation->track_get_key_time(E.key.track, E.key.key);7181float cur_time = timeline->get_play_position();7182float diff = prev_offset + cur_time - prev_time;7183float destination = cur_time - MIN(0, diff);7184if (diff >= len || animation->track_find_key(E.key.track, destination, Animation::FIND_MODE_EXACT) >= 0) {7185continue;7186}7187undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_start_offset", E.key.track, E.key.key, diff);7188undo_redo->add_do_method(animation.ptr(), "track_set_key_time", E.key.track, E.key.key, destination);7189undo_redo->add_undo_method(animation.ptr(), "track_set_key_time", E.key.track, E.key.key, prev_time);7190undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_start_offset", E.key.track, E.key.key, prev_offset);7191}7192undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);7193undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);7194undo_redo->commit_action();7195} break;7196case EDIT_SET_END_OFFSET: {7197EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7198undo_redo->create_action(TTR("Animation Set End Offset"), UndoRedo::MERGE_ENDS);7199for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {7200if (animation->track_get_type(E.key.track) != Animation::TYPE_AUDIO) {7201continue;7202}7203Ref<AudioStream> stream = animation->audio_track_get_key_stream(E.key.track, E.key.key);7204if (stream.is_null()) {7205continue;7206}7207double len = stream->get_length() - animation->audio_track_get_key_start_offset(E.key.track, E.key.key);7208real_t prev_offset = animation->audio_track_get_key_end_offset(E.key.track, E.key.key);7209double prev_time = animation->track_get_key_time(E.key.track, E.key.key);7210float cur_time = timeline->get_play_position();7211float diff = prev_time + len - cur_time;7212if (diff >= len) {7213continue;7214}7215undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_end_offset", E.key.track, E.key.key, diff);7216undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_end_offset", E.key.track, E.key.key, prev_offset);7217}7218undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);7219undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);7220undo_redo->commit_action();7221} break;72227223case EDIT_EASE_SELECTION: {7224ease_dialog->popup_centered(Size2(200, 100) * EDSCALE);7225} break;7226case EDIT_EASE_CONFIRM: {7227EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7228undo_redo->create_action(TTR("Make Easing Keys"));72297230Tween::TransitionType transition_type = static_cast<Tween::TransitionType>(transition_selection->get_selected_id());7231Tween::EaseType ease_type = static_cast<Tween::EaseType>(ease_selection->get_selected_id());7232float fps = ease_fps->get_value();7233double dur_step = 1.0 / fps;72347235// Organize track and key.7236HashMap<int, Vector<int>> keymap;7237Vector<int> tracks;7238for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {7239if (!tracks.has(E.key.track)) {7240tracks.append(E.key.track);7241}7242}7243for (int i = 0; i < tracks.size(); i++) {7244switch (animation->track_get_type(tracks[i])) {7245case Animation::TYPE_VALUE:7246case Animation::TYPE_POSITION_3D:7247case Animation::TYPE_ROTATION_3D:7248case Animation::TYPE_SCALE_3D:7249case Animation::TYPE_BLEND_SHAPE: {7250Vector<int> keys;7251for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {7252if (E.key.track == tracks[i]) {7253keys.append(E.key.key);7254}7255}7256keys.sort();7257keymap.insert(tracks[i], keys);7258} break;7259default: {7260} break;7261}7262}72637264// Make easing.7265HashMap<int, Vector<int>>::Iterator E = keymap.begin();7266while (E) {7267int track = E->key;7268Vector<int> keys = E->value;7269int len = keys.size() - 1;72707271// Special case for angle interpolation.7272bool is_using_angle = animation->track_get_interpolation_type(track) == Animation::INTERPOLATION_LINEAR_ANGLE || animation->track_get_interpolation_type(track) == Animation::INTERPOLATION_CUBIC_ANGLE;72737274// Make insert queue.7275Vector<Pair<real_t, Variant>> insert_queue_new;7276for (int i = 0; i < len; i++) {7277// Check neighboring keys.7278if (keys[i] + 1 == keys[i + 1]) {7279double from_t = animation->track_get_key_time(track, keys[i]);7280double to_t = animation->track_get_key_time(track, keys[i + 1]);7281Variant from_v = animation->track_get_key_value(track, keys[i]);7282Variant to_v = animation->track_get_key_value(track, keys[i + 1]);7283if (is_using_angle) {7284real_t a = from_v;7285real_t b = to_v;7286real_t to_diff = std::fmod(b - a, Math::TAU);7287to_v = a + std::fmod(2.0 * to_diff, Math::TAU) - to_diff;7288}7289Variant delta_v = Animation::subtract_variant(to_v, from_v);7290double duration = to_t - from_t;7291double fixed_duration = duration - UNIT_EPSILON; // Prevent to overwrap keys...7292for (double delta_t = dur_step; delta_t < fixed_duration; delta_t += dur_step) {7293Pair<real_t, Variant> keydata;7294keydata.first = from_t + delta_t;7295keydata.second = Tween::interpolate_variant(from_v, delta_v, delta_t, duration, transition_type, ease_type);7296insert_queue_new.append(keydata);7297}7298}7299}73007301// Do insertion.7302for (int i = 0; i < insert_queue_new.size(); i++) {7303undo_redo->add_do_method(animation.ptr(), "track_insert_key", track, insert_queue_new[i].first, insert_queue_new[i].second);7304undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", track, insert_queue_new[i].first);7305}73067307++E;7308}73097310undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);7311undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);7312undo_redo->add_do_method(this, "_redraw_tracks");7313undo_redo->add_undo_method(this, "_redraw_tracks");7314undo_redo->commit_action();7315_update_key_edit();73167317} break;73187319case EDIT_DUPLICATE_SELECTED_KEYS: {7320if (bezier_edit->is_visible()) {7321bezier_edit->duplicate_selected_keys(-1.0, false);7322break;7323}7324_anim_duplicate_keys(-1.0, false, -1.0);7325} break;7326case EDIT_CUT_KEYS: {7327if (bezier_edit->is_visible()) {7328bezier_edit->copy_selected_keys(true);7329break;7330}7331_anim_copy_keys(true);7332} break;7333case EDIT_COPY_KEYS: {7334if (bezier_edit->is_visible()) {7335bezier_edit->copy_selected_keys(false);7336break;7337}7338_anim_copy_keys(false);7339} break;7340case EDIT_PASTE_KEYS: {7341_anim_paste_keys(-1.0, false, -1.0);7342} break;7343case EDIT_MOVE_FIRST_SELECTED_KEY_TO_CURSOR: {7344if (moving_selection || selection.is_empty()) {7345break;7346}7347real_t from_t = 1e20;7348for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {7349real_t t = animation->track_get_key_time(E.key.track, E.key.key);7350if (t < from_t) {7351from_t = t;7352}7353}7354_move_selection_begin();7355_move_selection(timeline->get_play_position() - from_t);7356_move_selection_commit();7357} break;7358case EDIT_MOVE_LAST_SELECTED_KEY_TO_CURSOR: {7359if (moving_selection || selection.is_empty()) {7360break;7361}7362real_t to_t = -1e20;7363for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {7364real_t t = animation->track_get_key_time(E.key.track, E.key.key);7365if (t > to_t) {7366to_t = t;7367}7368}7369_move_selection_begin();7370_move_selection(timeline->get_play_position() - to_t);7371_move_selection_commit();7372} break;7373case EDIT_ADD_RESET_KEY: {7374EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7375undo_redo->create_action(TTR("Animation Add RESET Keys"));73767377Ref<Animation> reset = _create_and_get_reset_animation();7378int reset_tracks = reset->get_track_count();7379HashSet<int> tracks_added;73807381for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {7382const SelectedKey &sk = E.key;73837384const Animation::TrackType track_type = animation->track_get_type(E.key.track);7385if (track_type == Animation::TYPE_ANIMATION || track_type == Animation::TYPE_AUDIO || track_type == Animation::TYPE_METHOD) {7386continue;7387}73887389// Only add one key per track.7390if (tracks_added.has(sk.track)) {7391continue;7392}7393tracks_added.insert(sk.track);73947395int dst_track = -1;73967397const NodePath &path = animation->track_get_path(sk.track);7398for (int i = 0; i < reset->get_track_count(); i++) {7399if (reset->track_get_path(i) == path) {7400dst_track = i;7401break;7402}7403}74047405int existing_idx = -1;7406if (dst_track == -1) {7407// If adding multiple tracks, make sure that correct track is referenced.7408dst_track = reset_tracks;7409reset_tracks++;74107411undo_redo->add_do_method(reset.ptr(), "add_track", animation->track_get_type(sk.track));7412undo_redo->add_do_method(reset.ptr(), "track_set_path", dst_track, path);7413undo_redo->add_undo_method(reset.ptr(), "remove_track", dst_track);7414} else {7415existing_idx = reset->track_find_key(dst_track, 0, Animation::FIND_MODE_APPROX);7416}74177418if (animation->track_get_type(sk.track) == Animation::TYPE_VALUE) {7419undo_redo->add_do_method(reset.ptr(), "value_track_set_update_mode", dst_track, animation->value_track_get_update_mode(sk.track));7420}7421if (animation->track_get_type(sk.track) == Animation::TYPE_AUDIO) {7422undo_redo->add_do_method(reset.ptr(), "audio_track_set_use_blend", dst_track, animation->audio_track_is_use_blend(sk.track));7423}7424undo_redo->add_do_method(reset.ptr(), "track_set_interpolation_type", dst_track, animation->track_get_interpolation_type(sk.track));7425undo_redo->add_do_method(reset.ptr(), "track_set_interpolation_loop_wrap", dst_track, animation->track_get_interpolation_loop_wrap(sk.track));74267427undo_redo->add_do_method(reset.ptr(), "track_insert_key", dst_track, 0, animation->track_get_key_value(sk.track, sk.key), animation->track_get_key_transition(sk.track, sk.key));7428undo_redo->add_undo_method(reset.ptr(), "track_remove_key_at_time", dst_track, 0);74297430if (existing_idx != -1) {7431undo_redo->add_undo_method(reset.ptr(), "track_insert_key", dst_track, 0, reset->track_get_key_value(dst_track, existing_idx), reset->track_get_key_transition(dst_track, existing_idx));7432}7433}74347435undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);7436undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);7437undo_redo->add_do_method(this, "_redraw_tracks");7438undo_redo->add_undo_method(this, "_redraw_tracks");7439undo_redo->commit_action();74407441} break;7442case EDIT_DELETE_SELECTION: {7443if (bezier_edit->is_visible()) {7444bezier_edit->delete_selection();7445break;7446}74477448if (selection.size()) {7449EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7450undo_redo->create_action(TTR("Animation Delete Keys"));74517452for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {7453undo_redo->add_do_method(animation.ptr(), "track_remove_key", E->key().track, E->key().key);7454undo_redo->add_undo_method(animation.ptr(), "track_insert_key", E->key().track, E->get().pos, animation->track_get_key_value(E->key().track, E->key().key), animation->track_get_key_transition(E->key().track, E->key().key));7455}7456undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);7457undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);7458undo_redo->add_do_method(this, "_redraw_tracks");7459undo_redo->add_undo_method(this, "_redraw_tracks");7460undo_redo->commit_action();7461_update_key_edit();7462}7463} break;7464case EDIT_GOTO_NEXT_STEP_TIMELINE_ONLY:7465case EDIT_GOTO_NEXT_STEP: {7466goto_next_step(false, p_option == EDIT_GOTO_NEXT_STEP_TIMELINE_ONLY);7467} break;7468case EDIT_GOTO_PREV_STEP: {7469goto_prev_step(false);7470} break;74717472case EDIT_GOTO_NEXT_KEYFRAME: {7473AnimationPlayerEditor::get_singleton()->go_to_nearest_keyframe(false);7474} break;7475case EDIT_GOTO_PREV_KEYFRAME: {7476AnimationPlayerEditor::get_singleton()->go_to_nearest_keyframe(true);7477} break;74787479case EDIT_APPLY_RESET: {7480AnimationPlayerEditor::get_singleton()->get_player()->apply_reset(true);7481} break;74827483case EDIT_BAKE_ANIMATION: {7484bake_dialog->popup_centered(Size2(200, 100) * EDSCALE);7485} break;7486case EDIT_BAKE_ANIMATION_CONFIRM: {7487EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7488undo_redo->create_action(TTR("Bake Animation as Linear Keys"));74897490int track_len = animation->get_track_count();7491bool b_trs = bake_trs->is_pressed();7492bool b_bs = bake_blendshape->is_pressed();7493bool b_v = bake_value->is_pressed();74947495double anim_len = animation->get_length() + CMP_EPSILON; // For end key.7496float fps = bake_fps->get_value();7497double dur_step = 1.0 / fps;74987499for (int i = 0; i < track_len; i++) {7500bool do_bake = false;7501Animation::TrackType type = animation->track_get_type(i);7502do_bake |= b_trs && (type == Animation::TYPE_POSITION_3D || type == Animation::TYPE_ROTATION_3D || type == Animation::TYPE_SCALE_3D);7503do_bake |= b_bs && type == Animation::TYPE_BLEND_SHAPE;7504do_bake |= b_v && type == Animation::TYPE_VALUE;7505if (do_bake && !animation->track_is_compressed(i)) {7506Animation::InterpolationType it = animation->track_get_interpolation_type(i);7507if (it == Animation::INTERPOLATION_NEAREST) {7508continue; // Nearest interpolation cannot be baked.7509}75107511// Special case for angle interpolation.7512bool is_using_angle = it == Animation::INTERPOLATION_LINEAR_ANGLE || it == Animation::INTERPOLATION_CUBIC_ANGLE;75137514// Make insert queue.7515Vector<Pair<real_t, Variant>> insert_queue_new;75167517switch (type) {7518case Animation::TYPE_POSITION_3D: {7519for (double delta_t = 0.0; delta_t <= anim_len; delta_t += dur_step) {7520Pair<real_t, Variant> keydata;7521keydata.first = delta_t;7522Vector3 v;7523animation->try_position_track_interpolate(i, delta_t, &v);7524keydata.second = v;7525insert_queue_new.append(keydata);7526}7527} break;7528case Animation::TYPE_ROTATION_3D: {7529for (double delta_t = 0.0; delta_t <= anim_len; delta_t += dur_step) {7530Pair<real_t, Variant> keydata;7531keydata.first = delta_t;7532Quaternion v;7533animation->try_rotation_track_interpolate(i, delta_t, &v);7534keydata.second = v;7535insert_queue_new.append(keydata);7536}7537} break;7538case Animation::TYPE_SCALE_3D: {7539for (double delta_t = 0.0; delta_t <= anim_len; delta_t += dur_step) {7540Pair<real_t, Variant> keydata;7541keydata.first = delta_t;7542Vector3 v;7543animation->try_scale_track_interpolate(i, delta_t, &v);7544keydata.second = v;7545insert_queue_new.append(keydata);7546}7547} break;7548case Animation::TYPE_BLEND_SHAPE: {7549for (double delta_t = 0.0; delta_t <= anim_len; delta_t += dur_step) {7550Pair<real_t, Variant> keydata;7551keydata.first = delta_t;7552float v;7553animation->try_blend_shape_track_interpolate(i, delta_t, &v);7554keydata.second = v;7555insert_queue_new.append(keydata);7556}7557} break;7558case Animation::TYPE_VALUE: {7559for (double delta_t = 0.0; delta_t < anim_len; delta_t += dur_step) {7560Pair<real_t, Variant> keydata;7561keydata.first = delta_t;7562keydata.second = animation->value_track_interpolate(i, delta_t);7563insert_queue_new.append(keydata);7564}7565} break;7566default: {7567} break;7568}75697570// Cleanup keys.7571int key_len = animation->track_get_key_count(i);7572for (int j = key_len - 1; j >= 0; j--) {7573undo_redo->add_do_method(animation.ptr(), "track_remove_key", i, j);7574}75757576// Insert keys.7577undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_type", i, is_using_angle ? Animation::INTERPOLATION_LINEAR_ANGLE : Animation::INTERPOLATION_LINEAR);7578for (int j = insert_queue_new.size() - 1; j >= 0; j--) {7579undo_redo->add_do_method(animation.ptr(), "track_insert_key", i, insert_queue_new[j].first, insert_queue_new[j].second);7580undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", i, insert_queue_new[j].first);7581}75827583// Undo methods.7584undo_redo->add_undo_method(animation.ptr(), "track_set_interpolation_type", i, animation->track_get_interpolation_type(i));7585for (int j = key_len - 1; j >= 0; j--) {7586undo_redo->add_undo_method(animation.ptr(), "track_insert_key", i, animation->track_get_key_time(i, j), animation->track_get_key_value(i, j), animation->track_get_key_transition(i, j));7587}7588}7589}75907591undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);7592undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);7593undo_redo->add_do_method(this, "_redraw_tracks");7594undo_redo->add_undo_method(this, "_redraw_tracks");7595undo_redo->commit_action();7596_update_key_edit();75977598} break;75997600case EDIT_OPTIMIZE_ANIMATION: {7601optimize_dialog->popup_centered(Size2(250, 180) * EDSCALE);76027603} break;7604case EDIT_OPTIMIZE_ANIMATION_CONFIRM: {7605animation->optimize(optimize_velocity_error->get_value(), optimize_angular_error->get_value(), optimize_precision_error->get_value());7606_redraw_tracks();7607_update_key_edit();7608EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7609undo_redo->clear_history(undo_redo->get_history_id_for_object(animation.ptr()));7610undo_redo->clear_history(undo_redo->get_history_id_for_object(this));76117612} break;7613case EDIT_CLEAN_UP_ANIMATION: {7614cleanup_dialog->popup_centered(Size2(300, 0) * EDSCALE);76157616} break;7617case EDIT_CLEAN_UP_ANIMATION_CONFIRM: {7618if (cleanup_all->is_pressed()) {7619List<StringName> names;7620AnimationPlayerEditor::get_singleton()->get_player()->get_animation_list(&names);7621for (const StringName &E : names) {7622_cleanup_animation(AnimationPlayerEditor::get_singleton()->get_player()->get_animation(E));7623}7624} else {7625_cleanup_animation(animation);7626}76277628} break;7629}7630}76317632void AnimationTrackEditor::_cleanup_animation(Ref<Animation> p_animation) {7633_clear_selection();7634for (int i = 0; i < p_animation->get_track_count(); i++) {7635if (!root->has_node_and_resource(p_animation->track_get_path(i))) {7636continue;7637}7638Ref<Resource> res;7639Vector<StringName> leftover_path;7640Node *node = root->get_node_and_resource(p_animation->track_get_path(i), res, leftover_path);76417642bool prop_exists = false;7643Variant::Type valid_type = Variant::NIL;7644Object *obj = nullptr;76457646if (res.is_valid()) {7647obj = res.ptr();7648} else if (node) {7649obj = node;7650}76517652if (obj && p_animation->track_get_type(i) == Animation::TYPE_VALUE) {7653valid_type = obj->get_static_property_type_indexed(leftover_path, &prop_exists);7654}76557656if (!obj && cleanup_tracks->is_pressed()) {7657p_animation->remove_track(i);7658i--;7659continue;7660}76617662if (cleanup_keys_with_trimming_head->is_pressed()) {7663// Check is necessary because if there is already a key in position 0, it should not be replaced.7664if (p_animation->track_get_type(i) == Animation::TYPE_AUDIO && p_animation->track_find_key(i, 0, Animation::FIND_MODE_EXACT) < 0) {7665for (int j = 0; j < p_animation->track_get_key_count(i); j++) {7666double t = p_animation->track_get_key_time(i, j);7667if (t < 0) {7668if (j == p_animation->track_get_key_count(i) - 1 || (j + 1 < p_animation->track_get_key_count(i) && p_animation->track_get_key_time(i, j + 1) > 0)) {7669Ref<AudioStream> stream = p_animation->audio_track_get_key_stream(i, j);7670double len = stream->get_length() - p_animation->audio_track_get_key_end_offset(i, j);7671double prev_offset = p_animation->audio_track_get_key_start_offset(i, j);7672double prev_time = p_animation->track_get_key_time(i, j);7673double diff = prev_offset - prev_time;7674if (diff >= len) {7675p_animation->track_remove_key(i, j);7676j--;7677continue;7678}7679p_animation->audio_track_set_key_start_offset(i, j, diff);7680p_animation->track_set_key_time(i, j, 0);7681} else {7682p_animation->track_remove_key(i, j);7683j--;7684}7685}7686}7687} else {7688for (int j = 0; j < p_animation->track_get_key_count(i); j++) {7689double t = p_animation->track_get_key_time(i, j);7690if (t < 0) {7691p_animation->track_remove_key(i, j);7692j--;7693}7694}7695}7696}76977698if (cleanup_keys_with_trimming_end->is_pressed()) {7699if (p_animation->track_get_type(i) == Animation::TYPE_AUDIO) {7700for (int j = 0; j < p_animation->track_get_key_count(i); j++) {7701double t = p_animation->track_get_key_time(i, j);7702if (t <= p_animation->get_length() && (j == p_animation->track_get_key_count(i) - 1 || (j + 1 < p_animation->track_get_key_count(i) && p_animation->track_get_key_time(i, j + 1) > p_animation->get_length()))) {7703Ref<AudioStream> stream = animation->audio_track_get_key_stream(i, j);7704double len = stream->get_length() - animation->audio_track_get_key_start_offset(i, j);7705if (t + len < p_animation->get_length()) {7706continue;7707}7708double prev_time = animation->track_get_key_time(i, j);7709double diff = prev_time + len - p_animation->get_length();7710if (diff >= len) {7711p_animation->track_remove_key(i, j);7712j--;7713continue;7714}7715p_animation->audio_track_set_key_end_offset(i, j, diff);7716} else if (t > p_animation->get_length()) {7717p_animation->track_remove_key(i, j);7718j--;7719}7720}7721} else {7722for (int j = 0; j < p_animation->track_get_key_count(i); j++) {7723double t = p_animation->track_get_key_time(i, j);7724if (t > p_animation->get_length()) {7725p_animation->track_remove_key(i, j);7726j--;7727}7728}7729}7730}77317732if (!prop_exists || p_animation->track_get_type(i) != Animation::TYPE_VALUE || !cleanup_keys->is_pressed()) {7733continue;7734}77357736for (int j = 0; j < p_animation->track_get_key_count(i); j++) {7737Variant v = p_animation->track_get_key_value(i, j);77387739if (!Variant::can_convert(v.get_type(), valid_type)) {7740p_animation->track_remove_key(i, j);7741j--;7742}7743}77447745if (p_animation->track_get_key_count(i) == 0 && cleanup_tracks->is_pressed()) {7746p_animation->remove_track(i);7747i--;7748}7749}77507751EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7752undo_redo->clear_history(undo_redo->get_history_id_for_object(animation.ptr()));7753undo_redo->clear_history(undo_redo->get_history_id_for_object(this));7754_update_tracks();7755}77567757void AnimationTrackEditor::_toggle_function_names() {7758_redraw_tracks();7759}77607761void AnimationTrackEditor::_view_group_toggle() {7762_update_tracks();7763view_group->set_button_icon(get_editor_theme_icon(view_group->is_pressed() ? SNAME("AnimationTrackList") : SNAME("AnimationTrackGroup")));7764bezier_edit->set_filtered(selected_filter->is_pressed());7765}77667767bool AnimationTrackEditor::is_grouping_tracks() {7768if (!view_group) {7769return false;7770}77717772return !view_group->is_pressed();7773}77747775bool AnimationTrackEditor::is_sorting_alphabetically() {7776return alphabetic_sorting->is_pressed();7777}77787779bool AnimationTrackEditor::is_function_name_pressed() {7780return function_name_toggler->is_pressed();7781}77827783void AnimationTrackEditor::_auto_fit() {7784timeline->auto_fit();7785}77867787void AnimationTrackEditor::_auto_fit_bezier() {7788timeline->auto_fit();77897790if (bezier_edit->is_visible()) {7791bezier_edit->auto_fit_vertically();7792}7793}77947795void AnimationTrackEditor::_root_node_changed(Node *p_node, bool p_removed) {7796add_animation_player->set_disabled(p_removed);7797}77987799void AnimationTrackEditor::_scene_changed() {7800add_animation_player->set_disabled(EditorNode::get_singleton()->get_edited_scene() == nullptr);7801}78027803void AnimationTrackEditor::_selection_changed() {7804if (selected_filter->is_pressed()) {7805_update_tracks(); // Needs updating.7806} else {7807_redraw_tracks();7808_redraw_groups();7809}7810}78117812void AnimationTrackEditor::_update_snap_unit() {7813nearest_fps = 0;78147815if (step->get_value() <= 0) {7816snap_unit = 0;7817_update_nearest_fps_label();7818return; // Avoid zero div.7819}78207821if (timeline->is_using_fps()) {7822snap_unit = 1.0 / step->get_value();7823} else {7824if (fps_compat->is_pressed()) {7825snap_unit = CLAMP(step->get_value(), 0.0, 1.0);7826if (!Math::is_zero_approx(snap_unit)) {7827real_t fps = Math::round(1.0 / snap_unit);7828nearest_fps = int(fps);7829snap_unit = 1.0 / fps;7830}7831} else {7832snap_unit = step->get_value();7833}7834}7835_update_nearest_fps_label();7836}78377838float AnimationTrackEditor::snap_time(float p_value, bool p_relative) {7839if (is_snap_keys_enabled()) {7840double current_snap = snap_unit;7841if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) {7842// Use more precise snapping when holding Shift.7843current_snap *= 0.25;7844}78457846if (p_relative) {7847double rel = Math::fmod(timeline->get_value(), current_snap);7848p_value = Math::snapped(p_value + rel, current_snap) - rel;7849} else {7850p_value = Math::snapped(p_value, current_snap);7851}7852}78537854return p_value;7855}78567857float AnimationTrackEditor::get_snap_unit() {7858return snap_unit;7859}78607861void AnimationTrackEditor::_update_timeline_margins() {7862int margin_left = timeline_mc->get_theme_constant(SNAME("margin_left"), SNAME("AnimationTrackMargins"));7863int margin_right = timeline_mc->get_theme_constant(SNAME("margin_right"), SNAME("AnimationTrackMargins"));78647865// Prevent the timeline cursor from misaligning with the tracks on the right-to-left layout.7866if (scroll->get_v_scroll_bar()->is_visible() && is_layout_rtl()) {7867margin_left += scroll->get_v_scroll_bar()->get_minimum_size().width;7868}78697870timeline_mc->add_theme_constant_override(SNAME("margin_left"), margin_left);7871timeline_mc->add_theme_constant_override(SNAME("margin_right"), margin_right);7872}78737874void AnimationTrackEditor::_add_animation_player() {7875EditorData &editor_data = EditorNode::get_editor_data();7876Node *scene = editor_data.get_edited_scene_root();78777878ERR_FAIL_NULL_EDMSG(scene, "Cannot add AnimationPlayer without root node in scene");78797880AnimationPlayer *animation_player = memnew(AnimationPlayer);7881editor_data.instantiate_object_properties(animation_player);78827883String new_name = scene->validate_child_name(animation_player);7884if (GLOBAL_GET("editor/naming/node_name_casing").operator int() != NAME_CASING_PASCAL_CASE) {7885new_name = adjust_name_casing(new_name);7886}7887animation_player->set_name(new_name);78887889EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7890undo_redo->create_action_for_history(TTR("Create Node"), editor_data.get_current_edited_scene_history_id());78917892undo_redo->add_do_method(scene, "add_child", animation_player, true);7893undo_redo->add_do_method(animation_player, "set_owner", scene);7894undo_redo->add_do_reference(animation_player);7895undo_redo->add_undo_method(scene, "remove_child", animation_player);78967897undo_redo->commit_action();78987899EditorSelection *editor_selection = EditorNode::get_singleton()->get_editor_selection();7900editor_selection->clear();7901editor_selection->add_node(animation_player);7902}79037904void AnimationTrackEditor::_show_imported_anim_warning() {7905// It looks terrible on a single line but the TTR extractor doesn't support line breaks yet.7906EditorNode::get_singleton()->show_warning(7907TTR("This animation belongs to an imported scene, so changes to imported tracks will not be saved.\n\nTo modify this animation, navigate to the scene's Advanced Import settings and select the animation.\nSome options, including looping, are available here. To add custom tracks, enable \"Save To File\" and\n\"Keep Custom Tracks\"."));7908}79097910void AnimationTrackEditor::_show_dummy_player_warning() {7911EditorNode::get_singleton()->show_warning(7912TTR("Some AnimationPlayerEditor's options are disabled since this is the dummy AnimationPlayer for preview.\n\nThe dummy player is forced active, non-deterministic and doesn't have the root motion track. Furthermore, the original node is inactive temporary."));7913}79147915void AnimationTrackEditor::_show_inactive_player_warning() {7916EditorNode::get_singleton()->show_warning(7917TTR("AnimationPlayer is inactive. The playback will not be processed."));7918}79197920void AnimationTrackEditor::_select_all_tracks_for_copy() {7921TreeItem *track = track_copy_select->get_root()->get_first_child();7922if (!track) {7923return;7924}79257926bool all_selected = true;7927while (track) {7928if (!track->is_checked(0)) {7929all_selected = false;7930}79317932track = track->get_next();7933}79347935track = track_copy_select->get_root()->get_first_child();7936while (track) {7937track->set_checked(0, !all_selected);7938track = track->get_next();7939}7940}79417942void AnimationTrackEditor::_bind_methods() {7943ClassDB::bind_method("_track_grab_focus", &AnimationTrackEditor::_track_grab_focus);7944ClassDB::bind_method("_redraw_tracks", &AnimationTrackEditor::_redraw_tracks);7945ClassDB::bind_method("_clear_selection_for_anim", &AnimationTrackEditor::_clear_selection_for_anim);7946ClassDB::bind_method("_select_at_anim", &AnimationTrackEditor::_select_at_anim);7947ClassDB::bind_method("_clear_selection", &AnimationTrackEditor::_clear_selection);79487949ClassDB::bind_method(D_METHOD("_bezier_track_set_key_handle_mode", "animation", "track_idx", "key_idx", "key_handle_mode", "key_handle_set_mode"), &AnimationTrackEditor::_bezier_track_set_key_handle_mode, DEFVAL(Animation::HANDLE_SET_MODE_NONE));7950ClassDB::bind_method(D_METHOD("_bezier_track_set_key_handle_mode_at_time", "animation", "track_idx", "time", "key_handle_mode", "key_handle_set_mode"), &AnimationTrackEditor::_bezier_track_set_key_handle_mode_at_time, DEFVAL(Animation::HANDLE_SET_MODE_NONE));79517952ADD_SIGNAL(MethodInfo("timeline_changed", PropertyInfo(Variant::FLOAT, "position"), PropertyInfo(Variant::BOOL, "timeline_only"), PropertyInfo(Variant::BOOL, "update_position_only")));7953ADD_SIGNAL(MethodInfo("keying_changed"));7954ADD_SIGNAL(MethodInfo("animation_len_changed", PropertyInfo(Variant::FLOAT, "len")));7955ADD_SIGNAL(MethodInfo("animation_step_changed", PropertyInfo(Variant::FLOAT, "step")));7956}79577958void AnimationTrackEditor::_pick_track_filter_text_changed(const String &p_newtext) {7959TreeItem *root_item = pick_track->get_scene_tree()->get_scene_tree()->get_root();79607961Vector<Node *> select_candidates;7962Node *to_select = nullptr;79637964String filter = pick_track->get_filter_line_edit()->get_text();79657966_pick_track_select_recursive(root_item, filter, select_candidates);79677968if (!select_candidates.is_empty()) {7969for (int i = 0; i < select_candidates.size(); ++i) {7970Node *candidate = select_candidates[i];79717972if (((String)candidate->get_name()).to_lower().begins_with(filter.to_lower())) {7973to_select = candidate;7974break;7975}7976}79777978if (!to_select) {7979to_select = select_candidates[0];7980}7981}79827983pick_track->get_scene_tree()->set_selected(to_select);7984}79857986void AnimationTrackEditor::_pick_track_select_recursive(TreeItem *p_item, const String &p_filter, Vector<Node *> &p_select_candidates) {7987if (!p_item) {7988return;7989}79907991NodePath np = p_item->get_metadata(0);7992Node *node = get_node_or_null(np);79937994if (node && !p_filter.is_empty() && ((String)node->get_name()).containsn(p_filter)) {7995p_select_candidates.push_back(node);7996}79977998TreeItem *c = p_item->get_first_child();79998000while (c) {8001_pick_track_select_recursive(c, p_filter, p_select_candidates);8002c = c->get_next();8003}8004}80058006void AnimationTrackEditor::popup_read_only_dialog() {8007read_only_dialog->popup_centered(Size2(200, 100) * EDSCALE);8008}80098010AnimationTrackEditor::AnimationTrackEditor() {8011MarginContainer *mc = memnew(MarginContainer);8012mc->set_theme_type_variation("NoBorderAnimation");8013mc->set_v_size_flags(SIZE_EXPAND_FILL);8014add_child(mc);80158016main_panel = memnew(PanelContainer);8017main_panel->set_focus_mode(FOCUS_ALL); // Allow panel to have focus so that shortcuts work as expected.8018mc->add_child(main_panel);8019HBoxContainer *timeline_scroll = memnew(HBoxContainer);8020main_panel->add_child(timeline_scroll);8021timeline_scroll->set_v_size_flags(SIZE_EXPAND_FILL);80228023timeline_vbox = memnew(VBoxContainer);8024timeline_scroll->add_child(timeline_vbox);8025timeline_vbox->set_v_size_flags(SIZE_EXPAND_FILL);8026timeline_vbox->set_h_size_flags(SIZE_EXPAND_FILL);80278028info_message_vbox = memnew(VBoxContainer);8029main_panel->add_child(info_message_vbox);8030info_message_vbox->set_alignment(AlignmentMode::ALIGNMENT_CENTER);8031info_message_vbox->set_v_size_flags(SIZE_EXPAND_FILL);8032info_message_vbox->set_h_size_flags(SIZE_EXPAND_FILL);80338034info_message = memnew(Label);8035info_message_vbox->add_child(info_message);8036info_message->set_focus_mode(FOCUS_ACCESSIBILITY);8037info_message->set_text(TTR("Select an AnimationPlayer node to create and edit animations."));8038info_message->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);8039info_message->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);8040info_message->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);8041info_message->set_custom_minimum_size(Size2(100 * EDSCALE, 0));8042info_message->set_anchors_and_offsets_preset(PRESET_FULL_RECT, PRESET_MODE_KEEP_SIZE, 8 * EDSCALE);80438044add_animation_player = memnew(Button);8045info_message_vbox->add_child(add_animation_player);8046add_animation_player->set_text(TTR("Add AnimationPlayer"));8047add_animation_player->set_tooltip_text(TTR("Add a new AnimationPlayer node to the scene."));8048add_animation_player->set_h_size_flags(SIZE_SHRINK_CENTER);8049add_animation_player->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_add_animation_player));80508051timeline_mc = memnew(MarginContainer);8052timeline_mc->set_h_size_flags(SIZE_EXPAND_FILL);8053timeline_vbox->add_child(timeline_mc);80548055timeline = memnew(AnimationTimelineEdit);8056timeline_mc->add_child(timeline);8057timeline->set_editor(this);8058timeline->connect("timeline_changed", callable_mp(this, &AnimationTrackEditor::_timeline_changed));8059timeline->connect("name_limit_changed", callable_mp(this, &AnimationTrackEditor::_name_limit_changed));8060timeline->connect("track_added", callable_mp(this, &AnimationTrackEditor::_add_track));8061timeline->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_timeline_value_changed));8062timeline->connect("length_changed", callable_mp(this, &AnimationTrackEditor::_update_length));8063timeline->connect("filter_changed", callable_mp(this, &AnimationTrackEditor::_update_tracks));80648065panner.instantiate();8066panner->set_scroll_zoom_factor(AnimationTimelineEdit::SCROLL_ZOOM_FACTOR_IN);8067panner->set_callbacks(callable_mp(this, &AnimationTrackEditor::_pan_callback), callable_mp(this, &AnimationTrackEditor::_zoom_callback));80688069box_selection_container = memnew(Control);8070box_selection_container->set_v_size_flags(SIZE_EXPAND_FILL);8071box_selection_container->set_clip_contents(true);8072timeline_vbox->add_child(box_selection_container);80738074bezier_edit = memnew(AnimationBezierTrackEdit);8075timeline_vbox->add_child(bezier_edit);8076bezier_edit->set_editor(this);8077bezier_edit->set_timeline(timeline);8078bezier_edit->hide();8079bezier_edit->set_v_size_flags(SIZE_EXPAND_FILL);8080bezier_edit->connect("timeline_changed", callable_mp(this, &AnimationTrackEditor::_timeline_changed));80818082marker_edit = memnew(AnimationMarkerEdit);8083timeline->get_child(0)->add_child(marker_edit);8084// Prevents the play position from being drawn at the wrong place in specific cases.8085timeline->get_child(0)->connect(SceneStringName(resized), callable_mp(marker_edit, &AnimationMarkerEdit::update_play_position));8086marker_edit->set_editor(this);8087marker_edit->set_timeline(timeline);8088marker_edit->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT);8089marker_edit->set_z_index(1); // Ensure marker appears over the animation track editor.8090marker_edit->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_redraw_groups));8091marker_edit->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_redraw_tracks));8092marker_edit->connect(SceneStringName(draw), callable_mp((CanvasItem *)bezier_edit, &CanvasItem::queue_redraw));80938094scroll = memnew(ScrollContainer);8095scroll->set_scroll_hint_mode(ScrollContainer::SCROLL_HINT_MODE_ALL);8096box_selection_container->add_child(scroll);8097scroll->set_anchors_and_offsets_preset(PRESET_FULL_RECT);80988099scroll->set_focus_mode(FOCUS_CLICK);8100scroll->connect(SceneStringName(gui_input), callable_mp(this, &AnimationTrackEditor::_scroll_input));8101scroll->connect(SceneStringName(focus_exited), callable_mp(panner.ptr(), &ViewPanner::release_pan_key));81028103// Must be updated from here, so it guarantees that the scrollbar theme has already changed.8104scroll->connect(SceneStringName(theme_changed), callable_mp(this, &AnimationTrackEditor::_update_timeline_margins), CONNECT_DEFERRED);8105scroll->get_v_scroll_bar()->connect(SceneStringName(visibility_changed), callable_mp(this, &AnimationTrackEditor::_update_timeline_margins));8106scroll->get_v_scroll_bar()->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_v_scroll_changed));8107scroll->get_h_scroll_bar()->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_h_scroll_changed));81088109timeline_vbox->set_custom_minimum_size(Size2(0, 150) * EDSCALE);81108111hscroll = memnew(HScrollBar);8112hscroll->share(timeline);8113hscroll->hide();8114hscroll->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_update_scroll));8115timeline_vbox->add_child(hscroll);8116timeline->set_hscroll(hscroll);81178118mc = memnew(MarginContainer);8119mc->set_h_size_flags(SIZE_EXPAND_FILL);8120mc->set_theme_type_variation("AnimationTrackMargins");8121scroll->add_child(mc);81228123track_vbox = memnew(VBoxContainer);8124mc->add_child(track_vbox);8125scroll->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);81268127HFlowContainer *bottom_hf = memnew(HFlowContainer);8128add_child(bottom_hf);81298130imported_anim_warning = memnew(Button);8131imported_anim_warning->hide();8132imported_anim_warning->set_text(TTR("Imported Animation"));8133imported_anim_warning->set_tooltip_text(TTR("Warning: Editing imported animation"));8134imported_anim_warning->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_show_imported_anim_warning));8135bottom_hf->add_child(imported_anim_warning);81368137dummy_player_warning = memnew(Button);8138dummy_player_warning->hide();8139dummy_player_warning->set_text(TTR("Dummy Player"));8140dummy_player_warning->set_tooltip_text(TTR("Warning: Editing dummy AnimationPlayer"));8141dummy_player_warning->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_show_dummy_player_warning));8142bottom_hf->add_child(dummy_player_warning);81438144inactive_player_warning = memnew(Button);8145inactive_player_warning->hide();8146inactive_player_warning->set_text(TTR("Inactive Player"));8147inactive_player_warning->set_tooltip_text(TTR("Warning: AnimationPlayer is inactive"));8148inactive_player_warning->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_show_inactive_player_warning));8149bottom_hf->add_child(inactive_player_warning);81508151Control *spacer = memnew(Control);8152spacer->set_mouse_filter(MOUSE_FILTER_PASS);8153spacer->set_h_size_flags(SIZE_EXPAND_FILL);8154bottom_hf->add_child(spacer);81558156bezier_key_mode = memnew(OptionButton);8157bezier_key_mode->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);8158bezier_key_mode->add_item(String(), Animation::HANDLE_MODE_FREE);8159bezier_key_mode->add_item(String(), Animation::HANDLE_MODE_LINEAR);8160bezier_key_mode->add_item(String(), Animation::HANDLE_MODE_BALANCED);8161bezier_key_mode->add_item(String(), Animation::HANDLE_MODE_MIRRORED);8162bezier_key_mode->select(Animation::HANDLE_MODE_BALANCED);8163bezier_key_mode->set_accessibility_name(TTRC("Bezier Default Mode"));81648165bottom_hf->add_child(bezier_key_mode);8166bottom_hf->add_child(memnew(VSeparator));81678168bezier_edit_icon = memnew(Button);8169bezier_edit_icon->set_flat(true);8170bezier_edit_icon->set_disabled(true);8171bezier_edit_icon->set_toggle_mode(true);8172bezier_edit_icon->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_toggle_bezier_edit));8173bezier_edit_icon->set_tooltip_text(TTR("Toggle between the bezier curve editor and track editor."));81748175bottom_hf->add_child(bezier_edit_icon);81768177function_name_toggler = memnew(Button);8178function_name_toggler->set_flat(true);8179function_name_toggler->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_toggle_function_names));8180function_name_toggler->set_shortcut(ED_SHORTCUT("animation_editor/toggle_function_names", TTRC("Toggle method names")));8181function_name_toggler->set_toggle_mode(true);8182function_name_toggler->set_shortcut_in_tooltip(false);8183function_name_toggler->set_tooltip_text(TTRC("Toggle function names in the track editor."));81848185bottom_hf->add_child(function_name_toggler);81868187selected_filter = memnew(Button);8188selected_filter->set_flat(true);8189selected_filter->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_view_group_toggle)); // Same function works the same.8190selected_filter->set_toggle_mode(true);8191selected_filter->set_tooltip_text(TTR("Only show tracks from nodes selected in tree."));81928193bottom_hf->add_child(selected_filter);81948195alphabetic_sorting = memnew(Button);8196alphabetic_sorting->set_flat(true);8197alphabetic_sorting->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_update_tracks));8198alphabetic_sorting->set_toggle_mode(true);8199alphabetic_sorting->set_tooltip_text(TTR("Sort tracks/groups alphabetically.\nIf disabled, tracks are shown in the order they are added and can be reordered using drag-and-drop."));82008201bottom_hf->add_child(alphabetic_sorting);82028203view_group = memnew(Button);8204view_group->set_flat(true);8205view_group->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_view_group_toggle));8206view_group->set_toggle_mode(true);8207view_group->set_tooltip_text(TTR("Group tracks by node or display them as plain list."));82088209bottom_hf->add_child(view_group);82108211insert_at_current_time = memnew(Button);8212insert_at_current_time->set_flat(true);8213bottom_hf->add_child(insert_at_current_time);8214insert_at_current_time->set_disabled(true);8215insert_at_current_time->set_toggle_mode(true);8216insert_at_current_time->set_pressed(EDITOR_GET("editors/animation/insert_at_current_time"));8217insert_at_current_time->set_tooltip_text(TTRC("Insert at current time."));82188219bottom_hf->add_child(memnew(VSeparator));82208221snap_timeline = memnew(Button);8222snap_timeline->set_flat(true);8223snap_timeline->set_disabled(true);8224snap_timeline->set_toggle_mode(true);8225snap_timeline->set_tooltip_text(TTR("Apply snapping to timeline cursor."));8226snap_timeline->set_pressed(EditorSettings::get_singleton()->get_project_metadata("animation_track_editor", "snap_timeline", false));8227bottom_hf->add_child(snap_timeline);8228snap_timeline->connect(SceneStringName(toggled), callable_mp(this, &AnimationTrackEditor::_store_snap_states).unbind(1));82298230snap_keys = memnew(Button);8231snap_keys->set_flat(true);8232snap_keys->set_disabled(true);8233snap_keys->set_toggle_mode(true);8234snap_keys->set_tooltip_text(TTR("Apply snapping to selected key(s)."));8235snap_keys->set_pressed(EditorSettings::get_singleton()->get_project_metadata("animation_track_editor", "snap_keys", true));8236bottom_hf->add_child(snap_keys);8237snap_keys->connect(SceneStringName(toggled), callable_mp(this, &AnimationTrackEditor::_store_snap_states).unbind(1));82388239fps_compat = memnew(Button);8240fps_compat->set_flat(true);8241bottom_hf->add_child(fps_compat);8242fps_compat->set_disabled(true);8243fps_compat->set_toggle_mode(true);8244fps_compat->set_pressed(true);8245fps_compat->set_tooltip_text(TTR("Apply snapping to the nearest integer FPS."));8246fps_compat->connect(SceneStringName(toggled), callable_mp(this, &AnimationTrackEditor::_update_fps_compat_mode));82478248nearest_fps_label = memnew(Label);8249nearest_fps_label->set_focus_mode(FOCUS_ACCESSIBILITY);8250nearest_fps_label->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);8251bottom_hf->add_child(nearest_fps_label);82528253step = memnew(EditorSpinSlider);8254step->set_min(0);8255step->set_max(1000000);8256step->set_step(SECOND_DECIMAL);8257step->set_control_state(EditorSpinSlider::CONTROL_STATE_HIDE);8258step->set_custom_minimum_size(Size2(100, 0) * EDSCALE);8259step->set_tooltip_text(TTR("Animation step value."));8260step->set_accessibility_name(TTRC("Animation step value."));8261bottom_hf->add_child(step);8262step->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_update_step));8263step->set_read_only(true);82648265snap_mode = memnew(OptionButton);8266snap_mode->add_item(TTR("Seconds"));8267snap_mode->add_item(TTR("FPS"));8268snap_mode->set_accessibility_name(TTRC("Snap Mode"));8269snap_mode->set_disabled(true);8270bottom_hf->add_child(snap_mode);8271snap_mode->connect(SceneStringName(item_selected), callable_mp(this, &AnimationTrackEditor::_snap_mode_changed));82728273bottom_hf->add_child(memnew(VSeparator));82748275HBoxContainer *zoom_hb = memnew(HBoxContainer);8276zoom_icon = memnew(TextureRect);8277zoom_icon->set_v_size_flags(SIZE_SHRINK_CENTER);8278zoom_hb->add_child(zoom_icon);8279zoom = memnew(HSlider);8280zoom->set_step(0.01);8281zoom->set_min(0.0);8282zoom->set_max(2.0);8283zoom->set_value(1.0);8284zoom->set_custom_minimum_size(Size2(200, 0) * EDSCALE);8285zoom->set_v_size_flags(SIZE_SHRINK_CENTER);8286zoom->set_accessibility_name(TTRC("Zoom"));8287zoom_hb->add_child(zoom);8288bottom_hf->add_child(zoom_hb);8289timeline->set_zoom(zoom);82908291ED_SHORTCUT("animation_editor/auto_fit", TTRC("Fit to panel"), KeyModifierMask::ALT | Key::F);82928293auto_fit = memnew(Button);8294auto_fit->set_flat(true);8295auto_fit->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_auto_fit));8296auto_fit->set_shortcut(ED_GET_SHORTCUT("animation_editor/auto_fit"));8297auto_fit->set_accessibility_name(TTRC("Auto Fit"));8298bottom_hf->add_child(auto_fit);82998300auto_fit_bezier = memnew(Button);8301auto_fit_bezier->set_flat(true);8302auto_fit_bezier->set_visible(false);8303auto_fit_bezier->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_auto_fit_bezier));8304auto_fit_bezier->set_shortcut(ED_GET_SHORTCUT("animation_editor/auto_fit"));8305auto_fit_bezier->set_accessibility_name(TTRC("Auto Fit Bezier"));8306bottom_hf->add_child(auto_fit_bezier);83078308edit = memnew(MenuButton);8309edit->set_shortcut_context(this);8310edit->set_text(TTR("Edit"));8311edit->set_flat(false);8312edit->set_disabled(true);8313edit->set_tooltip_text(TTR("Animation properties."));8314edit->set_accessibility_name(TTRC("Animation properties."));8315edit->get_popup()->add_item(TTR("Copy Tracks..."), EDIT_COPY_TRACKS);8316edit->get_popup()->add_item(TTR("Paste Tracks"), EDIT_PASTE_TRACKS);8317edit->get_popup()->add_separator();8318edit->get_popup()->add_item(TTR("Scale Selection..."), EDIT_SCALE_SELECTION);8319edit->get_popup()->add_item(TTR("Scale From Cursor..."), EDIT_SCALE_FROM_CURSOR);8320edit->get_popup()->add_separator();8321edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/set_start_offset", TTRC("Set Start Offset (Audio)"), KeyModifierMask::CMD_OR_CTRL | Key::BRACKETLEFT), EDIT_SET_START_OFFSET);8322edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/set_end_offset", TTRC("Set End Offset (Audio)"), KeyModifierMask::CMD_OR_CTRL | Key::BRACKETRIGHT), EDIT_SET_END_OFFSET);8323edit->get_popup()->add_separator();8324edit->get_popup()->add_item(TTR("Make Easing Selection..."), EDIT_EASE_SELECTION);8325edit->get_popup()->add_separator();8326edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/duplicate_selected_keys", TTRC("Duplicate Selected Keys"), KeyModifierMask::CMD_OR_CTRL | Key::D), EDIT_DUPLICATE_SELECTED_KEYS);8327edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/cut_selected_keys", TTRC("Cut Selected Keys"), KeyModifierMask::CMD_OR_CTRL | Key::X), EDIT_CUT_KEYS);8328edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/copy_selected_keys", TTRC("Copy Selected Keys"), KeyModifierMask::CMD_OR_CTRL | Key::C), EDIT_COPY_KEYS);8329edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/paste_keys", TTRC("Paste Keys"), KeyModifierMask::CMD_OR_CTRL | Key::V), EDIT_PASTE_KEYS);83308331edit->get_popup()->add_separator();8332edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/move_first_selected_key_to_cursor", TTRC("Move First Selected Key to Cursor"), Key::BRACKETLEFT), EDIT_MOVE_FIRST_SELECTED_KEY_TO_CURSOR);8333edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/move_last_selected_key_to_cursor", TTRC("Move Last Selected Key to Cursor"), Key::BRACKETRIGHT), EDIT_MOVE_LAST_SELECTED_KEY_TO_CURSOR);8334edit->get_popup()->add_separator();8335edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/delete_selection", TTRC("Delete Selection"), Key::KEY_DELETE), EDIT_DELETE_SELECTION);83368337edit->get_popup()->add_separator();8338edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/goto_next_step", TTRC("Go to Next Step"), KeyModifierMask::CMD_OR_CTRL | Key::RIGHT), EDIT_GOTO_NEXT_STEP);8339edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/goto_prev_step", TTRC("Go to Previous Step"), KeyModifierMask::CMD_OR_CTRL | Key::LEFT), EDIT_GOTO_PREV_STEP);8340edit->get_popup()->add_separator();8341edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/go_to_next_keyframe", TTRC("Go to Next Keyframe"), KeyModifierMask::SHIFT | KeyModifierMask::ALT | Key::D), EDIT_GOTO_NEXT_KEYFRAME);8342edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/go_to_previous_keyframe", TTRC("Go to Previous Keyframe"), KeyModifierMask::SHIFT | KeyModifierMask::ALT | Key::A), EDIT_GOTO_PREV_KEYFRAME);8343edit->get_popup()->add_separator();83448345edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/add_reset_value", TTRC("Add tracks to RESET")), EDIT_ADD_RESET_KEY);8346edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/apply_reset", TTRC("Apply RESET")), EDIT_APPLY_RESET);8347edit->get_popup()->add_separator();8348edit->get_popup()->add_item(TTR("Bake Animation..."), EDIT_BAKE_ANIMATION);8349edit->get_popup()->add_item(TTR("Optimize Animation (no undo)..."), EDIT_OPTIMIZE_ANIMATION);8350edit->get_popup()->add_item(TTR("Clean-Up Animation (no undo)..."), EDIT_CLEAN_UP_ANIMATION);83518352edit->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed));8353edit->get_popup()->connect("about_to_popup", callable_mp(this, &AnimationTrackEditor::_edit_menu_about_to_popup));83548355pick_track = memnew(SceneTreeDialog);8356add_child(pick_track);8357pick_track->set_title(TTR("Pick a node to animate:"));8358pick_track->connect("selected", callable_mp(this, &AnimationTrackEditor::_new_track_node_selected));8359pick_track->get_filter_line_edit()->connect(SceneStringName(text_changed), callable_mp(this, &AnimationTrackEditor::_pick_track_filter_text_changed));83608361prop_selector = memnew(PropertySelector);8362add_child(prop_selector);8363prop_selector->connect("selected", callable_mp(this, &AnimationTrackEditor::_new_track_property_selected));8364prop_selector->set_accessibility_name(TTRC("Track Property"));83658366method_selector = memnew(PropertySelector);8367add_child(method_selector);8368method_selector->connect("selected", callable_mp(this, &AnimationTrackEditor::_add_method_key));8369method_selector->set_accessibility_name(TTRC("Method Key"));83708371insert_confirm = memnew(ConfirmationDialog);8372add_child(insert_confirm);8373insert_confirm->connect(SceneStringName(confirmed), callable_mp(this, &AnimationTrackEditor::_confirm_insert_list));8374VBoxContainer *icvb = memnew(VBoxContainer);8375insert_confirm->add_child(icvb);8376insert_confirm_text = memnew(Label);8377insert_confirm_text->set_focus_mode(FOCUS_ACCESSIBILITY);8378icvb->add_child(insert_confirm_text);8379HBoxContainer *ichb = memnew(HBoxContainer);8380icvb->add_child(ichb);8381insert_confirm_bezier = memnew(CheckBox);8382insert_confirm_bezier->set_text(TTR("Use Bezier Curves"));8383insert_confirm_bezier->set_pressed(EDITOR_GET("editors/animation/default_create_bezier_tracks"));8384ichb->add_child(insert_confirm_bezier);8385insert_confirm_reset = memnew(CheckBox);8386insert_confirm_reset->set_text(TTR("Create RESET Track(s)", ""));8387insert_confirm_reset->set_pressed(EDITOR_GET("editors/animation/default_create_reset_tracks"));8388ichb->add_child(insert_confirm_reset);83898390box_selection = memnew(Control);8391box_selection_container->add_child(box_selection);8392box_selection->set_mouse_filter(MOUSE_FILTER_IGNORE);8393box_selection->hide();8394box_selection->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_box_selection_draw));83958396// Default Plugins.83978398Ref<AnimationTrackEditDefaultPlugin> def_plugin;8399def_plugin.instantiate();8400add_track_edit_plugin(def_plugin);84018402// Dialogs.84038404optimize_dialog = memnew(ConfirmationDialog);8405add_child(optimize_dialog);8406optimize_dialog->set_title(TTR("Animation Optimizer"));8407VBoxContainer *optimize_vb = memnew(VBoxContainer);8408optimize_dialog->add_child(optimize_vb);84098410optimize_velocity_error = memnew(SpinBox);8411optimize_velocity_error->set_max(1.0);8412optimize_velocity_error->set_min(0.001);8413optimize_velocity_error->set_step(0.001);8414optimize_velocity_error->set_value(0.01);8415optimize_velocity_error->set_accessibility_name(TTRC("Max Velocity Error:"));8416optimize_vb->add_margin_child(TTR("Max Velocity Error:"), optimize_velocity_error);8417optimize_angular_error = memnew(SpinBox);8418optimize_angular_error->set_max(1.0);8419optimize_angular_error->set_min(0.001);8420optimize_angular_error->set_step(0.001);8421optimize_angular_error->set_value(0.01);8422optimize_angular_error->set_accessibility_name(TTRC("Max Angular Error:"));8423optimize_vb->add_margin_child(TTR("Max Angular Error:"), optimize_angular_error);8424optimize_precision_error = memnew(SpinBox);8425optimize_precision_error->set_max(6);8426optimize_precision_error->set_min(1);8427optimize_precision_error->set_step(1);8428optimize_precision_error->set_value(3);8429optimize_precision_error->set_accessibility_name(TTRC("Max Precision Error:"));8430optimize_vb->add_margin_child(TTR("Max Precision Error:"), optimize_precision_error);84318432optimize_dialog->set_ok_button_text(TTR("Optimize"));8433optimize_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_OPTIMIZE_ANIMATION_CONFIRM));84348435//8436cleanup_dialog = memnew(ConfirmationDialog);8437add_child(cleanup_dialog);8438VBoxContainer *cleanup_vb = memnew(VBoxContainer);8439cleanup_dialog->add_child(cleanup_vb);84408441cleanup_keys_with_trimming_head = memnew(CheckBox);8442cleanup_keys_with_trimming_head->set_text(TTR("Trim keys placed in negative time"));8443cleanup_keys_with_trimming_head->set_pressed(true);8444cleanup_vb->add_child(cleanup_keys_with_trimming_head);84458446cleanup_keys_with_trimming_end = memnew(CheckBox);8447cleanup_keys_with_trimming_end->set_text(TTR("Trim keys placed exceed the animation length"));8448cleanup_keys_with_trimming_end->set_pressed(true);8449cleanup_vb->add_child(cleanup_keys_with_trimming_end);84508451cleanup_keys = memnew(CheckBox);8452cleanup_keys->set_text(TTR("Remove invalid keys"));8453cleanup_keys->set_pressed(true);8454cleanup_vb->add_child(cleanup_keys);84558456cleanup_tracks = memnew(CheckBox);8457cleanup_tracks->set_text(TTR("Remove unresolved and empty tracks"));8458cleanup_tracks->set_pressed(true);8459cleanup_vb->add_child(cleanup_tracks);84608461cleanup_all = memnew(CheckBox);8462cleanup_all->set_text(TTR("Clean-up all animations"));8463cleanup_vb->add_child(cleanup_all);84648465cleanup_dialog->set_title(TTR("Clean-Up Animation(s) (NO UNDO!)"));8466cleanup_dialog->set_ok_button_text(TTR("Clean-Up"));84678468cleanup_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_CLEAN_UP_ANIMATION_CONFIRM));84698470//8471scale_dialog = memnew(ConfirmationDialog);8472VBoxContainer *vbc = memnew(VBoxContainer);8473scale_dialog->add_child(vbc);84748475scale = memnew(SpinBox);8476scale->set_min(-99999);8477scale->set_max(99999);8478scale->set_step(0.001);8479scale->set_select_all_on_focus(true);8480scale->set_accessibility_name(TTRC("Scale Ratio"));8481vbc->add_margin_child(TTR("Scale Ratio:"), scale);8482scale_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_SCALE_CONFIRM), CONNECT_DEFERRED);8483add_child(scale_dialog);84848485scale_dialog->register_text_enter(scale->get_line_edit());84868487//8488ease_dialog = memnew(ConfirmationDialog);8489ease_dialog->set_title(TTR("Select Transition and Easing"));8490ease_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_EASE_CONFIRM));8491add_child(ease_dialog);8492GridContainer *ease_grid = memnew(GridContainer);8493ease_grid->set_columns(2);8494ease_dialog->add_child(ease_grid);8495transition_selection = memnew(OptionButton);8496transition_selection->set_accessibility_name(TTRC("Transition Type:"));8497transition_selection->add_item(TTR("Linear", "Transition Type"), Tween::TRANS_LINEAR);8498transition_selection->add_item(TTR("Sine", "Transition Type"), Tween::TRANS_SINE);8499transition_selection->add_item(TTR("Quint", "Transition Type"), Tween::TRANS_QUINT);8500transition_selection->add_item(TTR("Quart", "Transition Type"), Tween::TRANS_QUART);8501transition_selection->add_item(TTR("Quad", "Transition Type"), Tween::TRANS_QUAD);8502transition_selection->add_item(TTR("Expo", "Transition Type"), Tween::TRANS_EXPO);8503transition_selection->add_item(TTR("Elastic", "Transition Type"), Tween::TRANS_ELASTIC);8504transition_selection->add_item(TTR("Cubic", "Transition Type"), Tween::TRANS_CUBIC);8505transition_selection->add_item(TTR("Circ", "Transition Type"), Tween::TRANS_CIRC);8506transition_selection->add_item(TTR("Bounce", "Transition Type"), Tween::TRANS_BOUNCE);8507transition_selection->add_item(TTR("Back", "Transition Type"), Tween::TRANS_BACK);8508transition_selection->add_item(TTR("Spring", "Transition Type"), Tween::TRANS_SPRING);8509transition_selection->select(Tween::TRANS_LINEAR); // Default8510transition_selection->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); // Translation context is needed.8511ease_selection = memnew(OptionButton);8512ease_selection->set_accessibility_name(TTRC("Ease Type:"));8513ease_selection->add_item(TTR("Ease In", "Ease Type"), Tween::EASE_IN);8514ease_selection->add_item(TTR("Ease Out", "Ease Type"), Tween::EASE_OUT);8515ease_selection->add_item(TTR("Ease In-Out", "Ease Type"), Tween::EASE_IN_OUT);8516ease_selection->add_item(TTR("Ease Out-In", "Ease Type"), Tween::EASE_OUT_IN);8517ease_selection->select(Tween::EASE_IN_OUT); // Default8518ease_selection->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); // Translation context is needed.8519ease_fps = memnew(SpinBox);8520ease_fps->set_min(FPS_DECIMAL);8521ease_fps->set_max(999);8522ease_fps->set_step(FPS_DECIMAL);8523ease_fps->set_value(30); // Default8524ease_fps->set_accessibility_name(TTRC("FPS:"));8525ease_grid->add_child(memnew(Label(TTR("Transition Type:"))));8526ease_grid->add_child(transition_selection);8527ease_grid->add_child(memnew(Label(TTR("Ease Type:"))));8528ease_grid->add_child(ease_selection);8529ease_grid->add_child(memnew(Label(TTR("FPS:"))));8530ease_grid->add_child(ease_fps);85318532//8533bake_dialog = memnew(ConfirmationDialog);8534bake_dialog->set_title(TTR("Animation Baker"));8535bake_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_BAKE_ANIMATION_CONFIRM));8536add_child(bake_dialog);8537GridContainer *bake_grid = memnew(GridContainer);8538bake_grid->set_columns(2);8539bake_dialog->add_child(bake_grid);8540bake_trs = memnew(CheckBox);8541bake_trs->set_accessibility_name(TTRC("3D Pos/Rot/Scl Track:"));8542bake_trs->set_pressed(true);8543bake_blendshape = memnew(CheckBox);8544bake_blendshape->set_accessibility_name(TTRC("Blendshape Track:"));8545bake_blendshape->set_pressed(true);8546bake_value = memnew(CheckBox);8547bake_value->set_accessibility_name(TTRC("Value Track:"));8548bake_value->set_pressed(true);8549bake_fps = memnew(SpinBox);8550bake_fps->set_accessibility_name(TTRC("FPS:"));8551bake_fps->set_min(FPS_DECIMAL);8552bake_fps->set_max(999);8553bake_fps->set_step(FPS_DECIMAL);8554bake_fps->set_value(30); // Default8555bake_grid->add_child(memnew(Label(TTR("3D Pos/Rot/Scl Track:"))));8556bake_grid->add_child(bake_trs);8557bake_grid->add_child(memnew(Label(TTR("Blendshape Track:"))));8558bake_grid->add_child(bake_blendshape);8559bake_grid->add_child(memnew(Label(TTR("Value Track:"))));8560bake_grid->add_child(bake_value);8561bake_grid->add_child(memnew(Label(TTR("FPS:"))));8562bake_grid->add_child(bake_fps);85638564track_copy_dialog = memnew(ConfirmationDialog);8565add_child(track_copy_dialog);8566track_copy_dialog->set_title(TTR("Select Tracks to Copy"));8567track_copy_dialog->set_ok_button_text(TTR("Copy"));85688569VBoxContainer *track_copy_vbox = memnew(VBoxContainer);8570track_copy_dialog->add_child(track_copy_vbox);85718572Button *select_all_button = memnew(Button);8573select_all_button->set_text(TTR("Select All/None"));8574select_all_button->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_select_all_tracks_for_copy));8575track_copy_vbox->add_child(select_all_button);85768577track_copy_select = memnew(Tree);8578track_copy_select->set_accessibility_name(TTRC("Copy Selection"));8579track_copy_select->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);8580track_copy_select->set_h_size_flags(SIZE_EXPAND_FILL);8581track_copy_select->set_v_size_flags(SIZE_EXPAND_FILL);8582track_copy_select->set_hide_root(true);8583track_copy_vbox->add_child(track_copy_select);8584track_copy_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_COPY_TRACKS_CONFIRM));85858586read_only_dialog = memnew(AcceptDialog);8587read_only_dialog->set_title(TTRC("Key Insertion Error"));8588read_only_dialog->set_text(TTRC("Imported Animation cannot be edited!"));8589add_child(read_only_dialog);8590}85918592AnimationTrackEditor::~AnimationTrackEditor() {8593if (key_edit) {8594memdelete(key_edit);8595}8596if (multi_key_edit) {8597memdelete(multi_key_edit);8598}8599}86008601// AnimationTrackKeyEditEditorPlugin.86028603void AnimationTrackKeyEditEditor::_time_edit_spun() {8604_time_edit_entered();8605_time_edit_exited();8606}86078608void AnimationTrackKeyEditEditor::_time_edit_entered() {8609int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX);8610if (key == -1) {8611return;8612}8613key_data_cache.time = animation->track_get_key_time(track, key);8614key_data_cache.transition = animation->track_get_key_transition(track, key);8615key_data_cache.value = animation->track_get_key_value(track, key);8616}86178618void AnimationTrackKeyEditEditor::_time_edit_exited() {8619real_t new_time = spinner->get_value();86208621if (use_fps) {8622real_t fps = animation->get_step();8623if (fps > 0) {8624fps = 1.0 / fps;8625}8626new_time /= fps;8627}86288629if (Math::is_equal_approx(new_time, key_data_cache.time)) {8630return; // No change.8631}86328633int existing = animation->track_find_key(track, new_time, Animation::FIND_MODE_APPROX);8634EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();8635undo_redo->create_action(TTR("Animation Change Keyframe Time"));86368637if (existing != -1) {8638undo_redo->add_do_method(animation.ptr(), "track_remove_key_at_time", track, animation->track_get_key_time(track, existing));8639}8640undo_redo->add_do_method(animation.ptr(), "track_remove_key_at_time", track, key_data_cache.time);8641undo_redo->add_do_method(animation.ptr(), "track_insert_key", track, new_time, key_data_cache.value, key_data_cache.transition);8642undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", track, new_time);8643undo_redo->add_undo_method(animation.ptr(), "track_insert_key", track, key_data_cache.time, key_data_cache.value, key_data_cache.transition);8644if (existing != -1) {8645undo_redo->add_undo_method(animation.ptr(), "track_insert_key", track, animation->track_get_key_time(track, existing), animation->track_get_key_value(track, existing), animation->track_get_key_transition(track, existing));8646}86478648// Reselect key.8649AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();8650if (ape) {8651AnimationTrackEditor *ate = ape->get_track_editor();8652if (ate) {8653undo_redo->add_do_method(ate, "_clear_selection_for_anim", animation);8654undo_redo->add_undo_method(ate, "_clear_selection_for_anim", animation);8655undo_redo->add_do_method(ate, "_select_at_anim", animation, track, new_time);8656undo_redo->add_undo_method(ate, "_select_at_anim", animation, track, key_data_cache.time);8657}8658undo_redo->add_do_method(ape, "_animation_update_key_frame");8659undo_redo->add_undo_method(ape, "_animation_update_key_frame");8660}86618662undo_redo->commit_action();8663}86648665AnimationTrackKeyEditEditor::AnimationTrackKeyEditEditor(Ref<Animation> p_animation, int p_track, real_t p_key_ofs, bool p_use_fps) {8666if (p_animation.is_null()) {8667return;8668}86698670animation = p_animation;8671track = p_track;8672key_ofs = p_key_ofs;8673use_fps = p_use_fps;86748675set_label("Time");86768677spinner = memnew(EditorSpinSlider);8678spinner->set_focus_mode(Control::FOCUS_CLICK);8679spinner->set_min(0);8680spinner->set_allow_greater(true);8681spinner->set_allow_lesser(true);8682add_child(spinner);86838684if (use_fps) {8685spinner->set_step(FPS_DECIMAL);8686real_t fps = animation->get_step();8687if (fps > 0) {8688fps = 1.0 / fps;8689}8690spinner->set_value(key_ofs * fps);8691spinner->connect("updown_pressed", callable_mp(this, &AnimationTrackKeyEditEditor::_time_edit_spun), CONNECT_DEFERRED);8692} else {8693spinner->set_step(SECOND_DECIMAL);8694spinner->set_value(key_ofs);8695spinner->set_max(animation->get_length());8696}86978698spinner->connect("grabbed", callable_mp(this, &AnimationTrackKeyEditEditor::_time_edit_entered), CONNECT_DEFERRED);8699spinner->connect("ungrabbed", callable_mp(this, &AnimationTrackKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED);8700spinner->connect("value_focus_entered", callable_mp(this, &AnimationTrackKeyEditEditor::_time_edit_entered), CONNECT_DEFERRED);8701spinner->connect("value_focus_exited", callable_mp(this, &AnimationTrackKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED);8702}87038704void AnimationMarkerEdit::_zoom_changed() {8705queue_redraw();8706play_position->queue_redraw();8707}87088709void AnimationMarkerEdit::_menu_selected(int p_index) {8710switch (p_index) {8711case MENU_KEY_INSERT: {8712_insert_marker(insert_at_pos);8713} break;8714case MENU_KEY_RENAME: {8715if (selection.size() > 0) {8716_rename_marker(*selection.last());8717}8718} break;8719case MENU_KEY_DELETE: {8720_delete_selected_markers();8721} break;8722case MENU_KEY_TOGGLE_MARKER_NAMES: {8723should_show_all_marker_names = !should_show_all_marker_names;8724queue_redraw();8725} break;8726}8727}87288729void AnimationMarkerEdit::_play_position_draw() {8730if (animation.is_null() || play_position_pos < 0) {8731return;8732}87338734float scale = timeline->get_zoom_scale();8735int px = (play_position_pos - timeline->get_value()) * scale + timeline->get_name_limit();87368737if (px >= timeline->get_name_limit() && px < (get_size().width - timeline->get_buttons_width())) {8738Color color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));8739play_position->draw_line(Point2(px, 0), Point2(px, get_size().height), color, Math::round(2 * EDSCALE));8740}8741}87428743bool AnimationMarkerEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable) {8744int limit = timeline->get_name_limit();8745int limit_end = get_size().width - timeline->get_buttons_width();8746// Left Border including space occupied by keyframes on t=0.8747int limit_start_hitbox = limit - type_icon->get_width();87488749if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) {8750int key_idx = -1;8751float key_distance = 1e20;8752PackedStringArray names = animation->get_marker_names();8753for (int i = 0; i < names.size(); i++) {8754Rect2 rect = const_cast<AnimationMarkerEdit *>(this)->get_key_rect(timeline->get_zoom_scale());8755float offset = animation->get_marker_time(names[i]) - timeline->get_value();8756offset = offset * timeline->get_zoom_scale() + limit;8757rect.position.x += offset;8758if (rect.has_point(p_pos)) {8759if (const_cast<AnimationMarkerEdit *>(this)->is_key_selectable_by_distance()) {8760float distance = Math::abs(offset - p_pos.x);8761if (key_idx == -1 || distance < key_distance) {8762key_idx = i;8763key_distance = distance;8764}8765} else {8766// First one does it.8767break;8768}8769}8770}87718772if (key_idx != -1) {8773if (p_aggregate) {8774StringName name = names[key_idx];8775if (selection.has(name)) {8776if (p_deselectable) {8777call_deferred("_deselect_key", name);8778moving_selection_pivot = 0.0f;8779moving_selection_mouse_begin_x = 0.0f;8780}8781} else {8782call_deferred("_select_key", name, false);8783moving_selection_attempt = true;8784moving_selection_effective = false;8785select_single_attempt = StringName();8786moving_selection_pivot = animation->get_marker_time(name);8787moving_selection_mouse_begin_x = p_pos.x;8788}87898790} else {8791StringName name = names[key_idx];8792if (!selection.has(name)) {8793call_deferred("_select_key", name, true);8794select_single_attempt = StringName();8795} else {8796select_single_attempt = name;8797}87988799moving_selection_attempt = true;8800moving_selection_effective = false;8801moving_selection_pivot = animation->get_marker_time(name);8802moving_selection_mouse_begin_x = p_pos.x;8803}88048805if (read_only) {8806moving_selection_attempt = false;8807moving_selection_pivot = 0.0f;8808moving_selection_mouse_begin_x = 0.0f;8809}8810return true;8811}8812}88138814return false;8815}88168817bool AnimationMarkerEdit::_is_ui_pos_in_current_section(const Point2 &p_pos) {8818int limit = timeline->get_name_limit();8819int limit_end = get_size().width - timeline->get_buttons_width();88208821if (p_pos.x >= limit && p_pos.x <= limit_end) {8822PackedStringArray section = get_selected_section();8823if (!section.is_empty()) {8824StringName start_marker = section[0];8825StringName end_marker = section[1];8826float start_offset = (animation->get_marker_time(start_marker) - timeline->get_value()) * timeline->get_zoom_scale() + limit;8827float end_offset = (animation->get_marker_time(end_marker) - timeline->get_value()) * timeline->get_zoom_scale() + limit;8828return p_pos.x >= start_offset && p_pos.x <= end_offset;8829}8830}88318832return false;8833}88348835HBoxContainer *AnimationMarkerEdit::_create_hbox_labeled_control(const String &p_text, Control *p_control) const {8836HBoxContainer *hbox = memnew(HBoxContainer);8837Label *label = memnew(Label);8838label->set_text(p_text);8839hbox->add_child(label);8840hbox->add_child(p_control);8841hbox->set_h_size_flags(SIZE_EXPAND_FILL);8842label->set_h_size_flags(SIZE_EXPAND_FILL);8843label->set_stretch_ratio(1.0);8844p_control->set_h_size_flags(SIZE_EXPAND_FILL);8845p_control->set_stretch_ratio(1.0);8846return hbox;8847}88488849void AnimationMarkerEdit::_update_key_edit() {8850_clear_key_edit();8851if (animation.is_null()) {8852return;8853}88548855if (selection.size() == 1) {8856key_edit = memnew(AnimationMarkerKeyEdit);8857key_edit->animation = animation;8858key_edit->animation_read_only = read_only;8859key_edit->marker_name = *selection.begin();8860key_edit->use_fps = timeline->is_using_fps();8861key_edit->marker_edit = this;88628863EditorNode::get_singleton()->push_item(key_edit);88648865InspectorDock::get_singleton()->set_info(TTR("Marker name is read-only in the inspector."), TTR("A marker's name can only be changed by right-clicking it in the animation editor and selecting \"Rename Marker\", in order to make sure that marker names are all unique."), true);8866} else if (selection.size() > 1) {8867multi_key_edit = memnew(AnimationMultiMarkerKeyEdit);8868multi_key_edit->animation = animation;8869multi_key_edit->animation_read_only = read_only;8870multi_key_edit->marker_edit = this;8871for (const StringName &name : selection) {8872multi_key_edit->marker_names.push_back(name);8873}88748875EditorNode::get_singleton()->push_item(multi_key_edit);8876}8877}88788879void AnimationMarkerEdit::_clear_key_edit() {8880if (key_edit) {8881// If key edit is the object being inspected, remove it first.8882if (InspectorDock::get_inspector_singleton()->get_edited_object() == key_edit) {8883EditorNode::get_singleton()->push_item(nullptr);8884}88858886// Then actually delete it.8887memdelete(key_edit);8888key_edit = nullptr;8889}88908891if (multi_key_edit) {8892if (InspectorDock::get_inspector_singleton()->get_edited_object() == multi_key_edit) {8893EditorNode::get_singleton()->push_item(nullptr);8894}88958896memdelete(multi_key_edit);8897multi_key_edit = nullptr;8898}8899}89008901void AnimationMarkerEdit::_bind_methods() {8902ClassDB::bind_method("_clear_selection_for_anim", &AnimationMarkerEdit::_clear_selection_for_anim);8903ClassDB::bind_method("_select_key", &AnimationMarkerEdit::_select_key);8904ClassDB::bind_method("_deselect_key", &AnimationMarkerEdit::_deselect_key);8905}89068907void AnimationMarkerEdit::_notification(int p_what) {8908switch (p_what) {8909case NOTIFICATION_THEME_CHANGED: {8910if (animation.is_null()) {8911return;8912}89138914type_icon = get_editor_theme_icon(SNAME("Marker"));8915selected_icon = get_editor_theme_icon(SNAME("MarkerSelected"));8916} break;89178918case NOTIFICATION_DRAW: {8919if (animation.is_null()) {8920return;8921}89228923int limit = timeline->get_name_limit();8924int limit_end = get_size().width - timeline->get_buttons_width();8925float scale = timeline->get_zoom_scale();89268927Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));8928Color color = get_theme_color(SceneStringName(font_color), SNAME("Label"));89298930// SECTION PREVIEW //89318932{8933PackedStringArray section = get_selected_section();8934if (section.size() == 2) {8935StringName start_marker = section[0];8936StringName end_marker = section[1];8937double start_time = animation->get_marker_time(start_marker);8938double end_time = animation->get_marker_time(end_marker);89398940// When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section.8941AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();8942if (moving_selection && !(player && player->is_playing())) {8943start_time += moving_selection_offset;8944end_time += moving_selection_offset;8945}89468947if (start_time < animation->get_length() && end_time >= 0) {8948float start_ofs = MAX(0, start_time) - timeline->get_value();8949float end_ofs = MIN(animation->get_length(), end_time) - timeline->get_value();8950start_ofs = start_ofs * scale + limit;8951end_ofs = end_ofs * scale + limit;8952start_ofs = MAX(start_ofs, limit);8953end_ofs = MIN(end_ofs, limit_end);8954Rect2 rect;8955rect.set_position(Vector2(start_ofs, 0));8956rect.set_size(Vector2(end_ofs - start_ofs, get_size().height));89578958draw_rect(rect, Color(1, 0.1, 0.1, 0.2));8959}8960}8961}89628963// KEYFRAMES //89648965draw_bg(limit, get_size().width - timeline->get_buttons_width());89668967{8968PackedStringArray names = animation->get_marker_names();8969for (int i = 0; i < names.size(); i++) {8970StringName name = names[i];8971bool is_selected = selection.has(name);8972float offset = animation->get_marker_time(name) - timeline->get_value();8973if (is_selected && moving_selection) {8974offset += moving_selection_offset;8975}89768977offset = offset * scale + limit;89788979draw_key(name, scale, int(offset), is_selected, limit, limit_end);89808981const int font_size = 12 * EDSCALE;8982Size2 string_size = font->get_string_size(name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size);8983if (int(offset) <= limit_end && int(offset) >= limit && should_show_all_marker_names) {8984float bottom = get_size().height + string_size.y - font->get_descent(font_size);8985float extrusion = MAX(0, offset + string_size.x - limit_end); // How much the string would extrude outside limit_end if unadjusted.8986Color marker_color = animation->get_marker_color(name);8987float margin = 4 * EDSCALE;8988Point2 pos = Point2(offset - extrusion + margin, bottom + margin);8989draw_string(font, pos, name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size, marker_color);8990draw_string_outline(font, pos, name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size, 1, color);8991}8992}8993}89948995draw_fg(limit, limit_end);8996} break;89978998case NOTIFICATION_MOUSE_ENTER:8999hovered = true;9000queue_redraw();9001break;9002case NOTIFICATION_MOUSE_EXIT:9003hovered = false;9004// When the mouse cursor exits the track, we're no longer hovering any keyframe.9005hovering_marker = StringName();9006queue_redraw();9007break;9008}9009}90109011void AnimationMarkerEdit::gui_input(const Ref<InputEvent> &p_event) {9012ERR_FAIL_COND(p_event.is_null());90139014if (animation.is_null()) {9015return;9016}90179018if (p_event->is_pressed()) {9019if (ED_IS_SHORTCUT("animation_marker_edit/rename_marker", p_event)) {9020if (!read_only) {9021_menu_selected(MENU_KEY_RENAME);9022}9023}90249025if (ED_IS_SHORTCUT("animation_marker_edit/delete_selection", p_event)) {9026if (!read_only) {9027_menu_selected(MENU_KEY_DELETE);9028}9029}90309031if (ED_IS_SHORTCUT("animation_marker_edit/toggle_marker_names", p_event)) {9032if (!read_only) {9033_menu_selected(MENU_KEY_TOGGLE_MARKER_NAMES);9034}9035}9036}90379038Ref<InputEventMouseButton> mb = p_event;90399040if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {9041Point2 pos = mb->get_position();9042if (_try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), true)) {9043accept_event();9044} else if (!_is_ui_pos_in_current_section(pos)) {9045_clear_selection_for_anim(animation);9046}9047}90489049// Check if moving markers or resizing the timeline first, so they can be canceled instead.9050if (!moving_selection && !timeline->resizing_timeline && mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {9051Point2 pos = mb->get_position();9052if (pos.x >= timeline->get_name_limit() && pos.x <= get_size().width - timeline->get_buttons_width()) {9053// Can do something with menu too! show insert key.9054float offset = (pos.x - timeline->get_name_limit()) / timeline->get_zoom_scale();9055if (!read_only) {9056bool selected = _try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), false);90579058menu->clear();9059menu->add_icon_item(get_editor_theme_icon(SNAME("Key")), TTR("Insert Marker..."), MENU_KEY_INSERT);90609061if (selected || selection.size() > 0) {9062menu->add_icon_item(get_editor_theme_icon(SNAME("Edit")), TTR("Rename Marker"), MENU_KEY_RENAME);9063menu->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Delete Marker(s)"), MENU_KEY_DELETE);9064}90659066menu->add_icon_item(get_editor_theme_icon(should_show_all_marker_names ? SNAME("GuiChecked") : SNAME("GuiUnchecked")), TTR("Show All Marker Names"), MENU_KEY_TOGGLE_MARKER_NAMES);9067menu->reset_size();90689069menu->set_position(get_screen_position() + get_local_mouse_position());9070menu->popup();90719072insert_at_pos = offset + timeline->get_value();9073accept_event();90749075timeline->_stop_dragging();9076}9077}9078}90799080if (mb.is_valid() && moving_selection_attempt) {9081if (!mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {9082moving_selection_attempt = false;9083if (moving_selection && moving_selection_effective) {9084if (Math::abs(moving_selection_offset) > CMP_EPSILON) {9085_move_selection_commit();9086accept_event(); // So play position doesn't snap to the end of move selection.9087}9088} else if (select_single_attempt) {9089call_deferred("_select_key", select_single_attempt, true);90909091// First select click should not affect play position.9092if (!selection.has(select_single_attempt)) {9093accept_event();9094} else {9095// Second click and onwards should snap to marker time.9096double ofs = animation->get_marker_time(select_single_attempt);9097timeline->set_play_position(ofs);9098timeline->emit_signal(SNAME("timeline_changed"), ofs, mb->is_alt_pressed());9099accept_event();9100}9101} else {9102// First select click should not affect play position.9103if (!selection.has(select_single_attempt)) {9104accept_event();9105}9106}91079108moving_selection = false;9109select_single_attempt = StringName();9110}91119112if (moving_selection && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {9113moving_selection_attempt = false;9114moving_selection = false;9115_move_selection_cancel();9116}9117}91189119Ref<InputEventMouseMotion> mm = p_event;91209121if (mm.is_valid()) {9122const StringName previous_hovering_marker = hovering_marker;91239124// Hovering compressed keyframes for editing is not possible.9125const float scale = timeline->get_zoom_scale();9126const int limit = timeline->get_name_limit();9127const int limit_end = get_size().width - timeline->get_buttons_width();9128// Left Border including space occupied by keyframes on t=0.9129const int limit_start_hitbox = limit - type_icon->get_width();9130const Point2 pos = mm->get_position();91319132if (pos.x >= limit_start_hitbox && pos.x <= limit_end) {9133// Use the same logic as key selection to ensure that hovering accurately represents9134// which key will be selected when clicking.9135int key_idx = -1;9136float key_distance = 1e20;91379138hovering_marker = StringName();91399140PackedStringArray names = animation->get_marker_names();91419142// Hovering should happen in the opposite order of drawing for more accurate overlap hovering.9143for (int i = names.size() - 1; i >= 0; i--) {9144StringName name = names[i];9145Rect2 rect = get_key_rect(scale);9146float offset = animation->get_marker_time(name) - timeline->get_value();9147offset = offset * scale + limit;9148rect.position.x += offset;91499150if (rect.has_point(pos)) {9151if (is_key_selectable_by_distance()) {9152const float distance = Math::abs(offset - pos.x);9153if (key_idx == -1 || distance < key_distance) {9154key_idx = i;9155key_distance = distance;9156hovering_marker = name;9157}9158} else {9159// First one does it.9160hovering_marker = name;9161break;9162}9163}9164}91659166if (hovering_marker != previous_hovering_marker) {9167// Required to draw keyframe hover feedback on the correct keyframe.9168queue_redraw();9169}9170}9171}91729173if (mm.is_valid() && mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && moving_selection_attempt) {9174if (!moving_selection) {9175moving_selection = true;9176_move_selection_begin();9177}91789179float moving_begin_time = ((moving_selection_mouse_begin_x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value();9180float new_time = ((mm->get_position().x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value();9181float delta = new_time - moving_begin_time;9182float snapped_time = editor->snap_time(moving_selection_pivot + delta);91839184float offset = 0.0;9185if (Math::abs(editor->get_moving_selection_offset()) > CMP_EPSILON || (snapped_time > moving_selection_pivot && delta > CMP_EPSILON) || (snapped_time < moving_selection_pivot && delta < -CMP_EPSILON)) {9186offset = snapped_time - moving_selection_pivot;9187moving_selection_effective = true;9188}91899190_move_selection(offset);9191}9192}91939194String AnimationMarkerEdit::get_tooltip(const Point2 &p_pos) const {9195if (animation.is_null()) {9196return Control::get_tooltip(p_pos);9197}91989199int limit = timeline->get_name_limit();9200int limit_end = get_size().width - timeline->get_buttons_width();9201// Left Border including space occupied by keyframes on t=0.9202int limit_start_hitbox = limit - type_icon->get_width();92039204if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) {9205int key_idx = -1;9206float key_distance = 1e20;92079208PackedStringArray names = animation->get_marker_names();92099210// Select should happen in the opposite order of drawing for more accurate overlap select.9211for (int i = names.size() - 1; i >= 0; i--) {9212StringName name = names[i];9213Rect2 rect = const_cast<AnimationMarkerEdit *>(this)->get_key_rect(timeline->get_zoom_scale());9214float offset = animation->get_marker_time(name) - timeline->get_value();9215offset = offset * timeline->get_zoom_scale() + limit;9216rect.position.x += offset;92179218if (rect.has_point(p_pos)) {9219if (const_cast<AnimationMarkerEdit *>(this)->is_key_selectable_by_distance()) {9220float distance = Math::abs(offset - p_pos.x);9221if (key_idx == -1 || distance < key_distance) {9222key_idx = i;9223key_distance = distance;9224}9225} else {9226// First one does it.9227break;9228}9229}9230}92319232if (key_idx != -1) {9233String name = names[key_idx];9234String text = TTR("Time (s):") + " " + TranslationServer::get_singleton()->format_number(rtos(Math::snapped(animation->get_marker_time(name), 0.0001)), _get_locale()) + "\n";9235text += TTR("Marker:") + " " + name + "\n";9236return text;9237}9238}92399240return Control::get_tooltip(p_pos);9241}92429243int AnimationMarkerEdit::get_key_height() const {9244if (animation.is_null()) {9245return 0;9246}92479248return type_icon->get_height();9249}92509251Rect2 AnimationMarkerEdit::get_key_rect(float p_pixels_sec) const {9252if (animation.is_null()) {9253return Rect2();9254}92559256Rect2 rect = Rect2(-type_icon->get_width() / 2, get_size().height - type_icon->get_size().height, type_icon->get_width(), type_icon->get_size().height);92579258// Make it a big easier to click.9259rect.position.x -= rect.size.x * 0.5;9260rect.size.x *= 2;9261return rect;9262}92639264PackedStringArray AnimationMarkerEdit::get_selected_section() const {9265if (selection.size() >= 2) {9266PackedStringArray arr;9267arr.push_back(""); // Marker with smallest time.9268arr.push_back(""); // Marker with largest time.9269double min_time = Math::INF;9270double max_time = -Math::INF;9271for (const StringName &marker_name : selection) {9272double time = animation->get_marker_time(marker_name);9273if (time < min_time) {9274arr.set(0, marker_name);9275min_time = time;9276}9277if (time > max_time) {9278arr.set(1, marker_name);9279max_time = time;9280}9281}9282return arr;9283}92849285return PackedStringArray();9286}92879288bool AnimationMarkerEdit::is_marker_selected(const StringName &p_marker) const {9289return selection.has(p_marker);9290}92919292bool AnimationMarkerEdit::is_key_selectable_by_distance() const {9293return true;9294}92959296void AnimationMarkerEdit::draw_key(const StringName &p_name, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right) {9297if (animation.is_null()) {9298return;9299}93009301if (p_x < p_clip_left || p_x > p_clip_right) {9302return;9303}93049305Ref<Texture2D> icon_to_draw = p_selected ? selected_icon : type_icon;93069307Vector2 ofs(p_x - icon_to_draw->get_width() / 2, int(get_size().height - icon_to_draw->get_height()));93089309// Don't apply custom marker color when the key is selected.9310Color marker_color = p_selected ? Color(1, 1, 1) : animation->get_marker_color(p_name);93119312// Use a different color for the currently hovered key.9313// The color multiplier is chosen to work with both dark and light editor themes,9314// and on both unselected and selected key icons.9315draw_texture(9316icon_to_draw,9317ofs,9318p_name == hovering_marker ? get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")) : marker_color);9319}93209321void AnimationMarkerEdit::draw_bg(int p_clip_left, int p_clip_right) {9322}93239324void AnimationMarkerEdit::draw_fg(int p_clip_left, int p_clip_right) {9325}93269327Ref<Animation> AnimationMarkerEdit::get_animation() const {9328return animation;9329}93309331void AnimationMarkerEdit::set_animation(const Ref<Animation> &p_animation, bool p_read_only) {9332if (animation.is_valid()) {9333_clear_selection_for_anim(animation);9334}9335animation = p_animation;9336read_only = p_read_only;9337type_icon = get_editor_theme_icon(SNAME("Marker"));9338selected_icon = get_editor_theme_icon(SNAME("MarkerSelected"));93399340queue_redraw();9341}93429343Size2 AnimationMarkerEdit::get_minimum_size() const {9344Ref<Texture2D> texture = get_editor_theme_icon(SNAME("Object"));9345Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));9346int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));9347int separation = get_theme_constant(SNAME("v_separation"), SNAME("ItemList"));93489349int max_h = MAX(texture->get_height(), font->get_height(font_size));9350max_h = MAX(max_h, get_key_height());93519352return Vector2(1, max_h + separation);9353}93549355void AnimationMarkerEdit::set_timeline(AnimationTimelineEdit *p_timeline) {9356timeline = p_timeline;9357timeline->connect("zoom_changed", callable_mp(this, &AnimationMarkerEdit::_zoom_changed));9358timeline->connect("name_limit_changed", callable_mp(this, &AnimationMarkerEdit::_zoom_changed));9359}93609361void AnimationMarkerEdit::set_editor(AnimationTrackEditor *p_editor) {9362editor = p_editor;9363}93649365void AnimationMarkerEdit::set_play_position(float p_pos) {9366play_position_pos = p_pos;9367play_position->queue_redraw();9368}93699370void AnimationMarkerEdit::update_play_position() {9371play_position->queue_redraw();9372}93739374void AnimationMarkerEdit::set_use_fps(bool p_use_fps) {9375if (key_edit) {9376key_edit->use_fps = p_use_fps;9377key_edit->notify_property_list_changed();9378}9379}93809381void AnimationMarkerEdit::_move_selection_begin() {9382moving_selection = true;9383moving_selection_offset = 0;9384}93859386void AnimationMarkerEdit::_move_selection(float p_offset) {9387moving_selection_offset = p_offset;9388queue_redraw();9389}93909391void AnimationMarkerEdit::_move_selection_commit() {9392EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();9393undo_redo->create_action(TTR("Animation Move Markers"));93949395for (HashSet<StringName>::Iterator E = selection.last(); E; --E) {9396StringName name = *E;9397double time = animation->get_marker_time(name);9398float newpos = time + moving_selection_offset;9399undo_redo->add_do_method(animation.ptr(), "remove_marker", name);9400undo_redo->add_do_method(animation.ptr(), "add_marker", name, newpos);9401undo_redo->add_do_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name));9402undo_redo->add_undo_method(animation.ptr(), "remove_marker", name);9403undo_redo->add_undo_method(animation.ptr(), "add_marker", name, time);9404undo_redo->add_undo_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name));94059406// add_marker will overwrite the overlapped key on the redo pass, so we add it back on the undo pass.9407if (StringName overlap = animation->get_marker_at_time(newpos)) {9408if (select_single_attempt == overlap) {9409select_single_attempt = "";9410}9411undo_redo->add_undo_method(animation.ptr(), "add_marker", overlap, newpos);9412undo_redo->add_undo_method(animation.ptr(), "set_marker_color", overlap, animation->get_marker_color(overlap));9413}9414}94159416moving_selection = false;9417AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();9418if (player) {9419PackedStringArray selected_section = get_selected_section();9420if (selected_section.size() >= 2) {9421undo_redo->add_do_method(player, "set_section_with_markers", selected_section[0], selected_section[1]);9422undo_redo->add_undo_method(player, "set_section_with_markers", selected_section[0], selected_section[1]);9423}9424}9425undo_redo->add_do_method(timeline, "queue_redraw");9426undo_redo->add_undo_method(timeline, "queue_redraw");9427undo_redo->add_do_method(this, "queue_redraw");9428undo_redo->add_undo_method(this, "queue_redraw");9429undo_redo->commit_action();9430_update_key_edit();9431}94329433void AnimationMarkerEdit::_delete_selected_markers() {9434if (selection.size()) {9435EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();9436undo_redo->create_action(TTR("Animation Delete Markers"));9437for (const StringName &name : selection) {9438double time = animation->get_marker_time(name);9439undo_redo->add_do_method(animation.ptr(), "remove_marker", name);9440undo_redo->add_undo_method(animation.ptr(), "add_marker", name, time);9441undo_redo->add_undo_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name));9442}9443_clear_selection_for_anim(animation);94449445undo_redo->add_do_method(this, "queue_redraw");9446undo_redo->add_undo_method(this, "queue_redraw");9447undo_redo->commit_action();9448_update_key_edit();9449}9450}94519452void AnimationMarkerEdit::_move_selection_cancel() {9453moving_selection = false;9454queue_redraw();9455}94569457void AnimationMarkerEdit::_clear_selection(bool p_update) {9458AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();9459if (player) {9460player->reset_section();9461}94629463selection.clear();94649465if (p_update) {9466queue_redraw();9467}94689469_clear_key_edit();9470}94719472void AnimationMarkerEdit::_clear_selection_for_anim(const Ref<Animation> &p_anim) {9473if (animation != p_anim) {9474return;9475}94769477_clear_selection(true);9478}94799480void AnimationMarkerEdit::_select_key(const StringName &p_name, bool is_single) {9481if (is_single) {9482_clear_selection(false);9483}94849485selection.insert(p_name);94869487AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();9488if (player) {9489if (selection.size() >= 2) {9490PackedStringArray selected_section = get_selected_section();9491double start_time = animation->get_marker_time(selected_section[0]);9492double end_time = animation->get_marker_time(selected_section[1]);9493player->set_section(start_time, end_time);9494} else {9495player->reset_section();9496}9497}94989499queue_redraw();9500_update_key_edit();95019502editor->_clear_selection(editor->is_selection_active());9503}95049505void AnimationMarkerEdit::_deselect_key(const StringName &p_name) {9506selection.erase(p_name);95079508AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();9509if (player) {9510if (selection.size() >= 2) {9511PackedStringArray selected_section = get_selected_section();9512double start_time = animation->get_marker_time(selected_section[0]);9513double end_time = animation->get_marker_time(selected_section[1]);9514player->set_section(start_time, end_time);9515} else {9516player->reset_section();9517}9518}95199520queue_redraw();9521_update_key_edit();9522}95239524void AnimationMarkerEdit::_insert_marker(float p_ofs) {9525if (editor->is_snap_timeline_enabled()) {9526p_ofs = editor->snap_time(p_ofs);9527}95289529editor->resolve_insertion_offset(p_ofs);95309531marker_insert_confirm->popup_centered(Size2(200, 100) * EDSCALE);9532marker_insert_color->set_pick_color(Color(1, 1, 1));95339534String base = "new_marker";9535int count = 1;9536while (true) {9537String attempt = base;9538if (count > 1) {9539attempt += vformat("_%d", count);9540}9541if (animation->has_marker(attempt)) {9542count++;9543continue;9544}9545base = attempt;9546break;9547}95489549marker_insert_new_name->set_text(base);9550_marker_insert_new_name_changed(base);9551marker_insert_ofs = p_ofs;9552}95539554void AnimationMarkerEdit::_rename_marker(const StringName &p_name) {9555marker_rename_confirm->popup_centered(Size2i(200, 0) * EDSCALE);9556marker_rename_prev_name = p_name;9557marker_rename_new_name->set_text(p_name);9558}95599560void AnimationMarkerEdit::_marker_insert_confirmed() {9561StringName name = marker_insert_new_name->get_text();95629563if (animation->has_marker(name)) {9564marker_insert_error_dialog->set_text(vformat(TTR("Marker '%s' already exists!"), name));9565marker_insert_error_dialog->popup_centered();9566return;9567}95689569EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();95709571undo_redo->create_action(TTR("Add Marker Key"));9572undo_redo->add_do_method(animation.ptr(), "add_marker", name, marker_insert_ofs);9573undo_redo->add_undo_method(animation.ptr(), "remove_marker", name);9574StringName existing_marker = animation->get_marker_at_time(marker_insert_ofs);9575if (existing_marker) {9576undo_redo->add_undo_method(animation.ptr(), "add_marker", existing_marker, marker_insert_ofs);9577undo_redo->add_undo_method(animation.ptr(), "set_marker_color", existing_marker, animation->get_marker_color(existing_marker));9578}9579undo_redo->add_do_method(animation.ptr(), "set_marker_color", name, marker_insert_color->get_pick_color());95809581undo_redo->add_do_method(this, "queue_redraw");9582undo_redo->add_undo_method(this, "queue_redraw");95839584undo_redo->commit_action();95859586marker_insert_confirm->hide();9587}95889589void AnimationMarkerEdit::_marker_insert_new_name_changed(const String &p_text) {9590marker_insert_confirm->get_ok_button()->set_disabled(p_text.is_empty());9591}95929593void AnimationMarkerEdit::_marker_rename_confirmed() {9594StringName new_name = marker_rename_new_name->get_text();9595StringName prev_name = marker_rename_prev_name;95969597if (new_name == StringName()) {9598marker_rename_error_dialog->set_text(TTR("Empty marker names are not allowed."));9599marker_rename_error_dialog->popup_centered();9600return;9601}96029603if (new_name != prev_name && animation->has_marker(new_name)) {9604marker_rename_error_dialog->set_text(vformat(TTR("Marker '%s' already exists!"), new_name));9605marker_rename_error_dialog->popup_centered();9606return;9607}96089609if (prev_name != new_name) {9610EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();9611undo_redo->create_action(TTR("Rename Marker"));9612undo_redo->add_do_method(animation.ptr(), "remove_marker", prev_name);9613undo_redo->add_do_method(animation.ptr(), "add_marker", new_name, animation->get_marker_time(prev_name));9614undo_redo->add_do_method(animation.ptr(), "set_marker_color", new_name, animation->get_marker_color(prev_name));9615undo_redo->add_undo_method(animation.ptr(), "remove_marker", new_name);9616undo_redo->add_undo_method(animation.ptr(), "add_marker", prev_name, animation->get_marker_time(prev_name));9617undo_redo->add_undo_method(animation.ptr(), "set_marker_color", prev_name, animation->get_marker_color(prev_name));9618undo_redo->add_do_method(this, "_select_key", new_name, true);9619undo_redo->add_undo_method(this, "_select_key", prev_name, true);9620undo_redo->commit_action();9621select_single_attempt = StringName();9622}9623marker_rename_confirm->hide();9624}96259626void AnimationMarkerEdit::_marker_rename_new_name_changed(const String &p_text) {9627marker_rename_confirm->get_ok_button()->set_disabled(p_text.is_empty());9628}96299630AnimationMarkerEdit::AnimationMarkerEdit() {9631play_position = memnew(Control);9632play_position->set_mouse_filter(MOUSE_FILTER_PASS);9633add_child(play_position);9634play_position->connect(SceneStringName(draw), callable_mp(this, &AnimationMarkerEdit::_play_position_draw));9635set_focus_mode(FOCUS_CLICK);9636set_mouse_filter(MOUSE_FILTER_PASS); // Scroll has to work too for selection.96379638menu = memnew(PopupMenu);9639add_child(menu);9640menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationMarkerEdit::_menu_selected));9641menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/rename_marker", TTRC("Rename Marker"), Key::R), MENU_KEY_RENAME);9642menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/delete_selection", TTRC("Delete Marker(s)"), Key::KEY_DELETE), MENU_KEY_DELETE);9643menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/toggle_marker_names", TTRC("Show All Marker Names"), Key::M), MENU_KEY_TOGGLE_MARKER_NAMES);96449645marker_insert_confirm = memnew(ConfirmationDialog);9646marker_insert_confirm->set_title(TTR("Insert Marker"));9647marker_insert_confirm->set_hide_on_ok(false);9648marker_insert_confirm->connect(SceneStringName(confirmed), callable_mp(this, &AnimationMarkerEdit::_marker_insert_confirmed));9649add_child(marker_insert_confirm);9650VBoxContainer *marker_insert_vbox = memnew(VBoxContainer);9651marker_insert_vbox->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT);9652marker_insert_confirm->add_child(marker_insert_vbox);9653marker_insert_new_name = memnew(LineEdit);9654marker_insert_new_name->connect(SceneStringName(text_changed), callable_mp(this, &AnimationMarkerEdit::_marker_insert_new_name_changed));9655marker_insert_confirm->register_text_enter(marker_insert_new_name);9656marker_insert_vbox->add_child(_create_hbox_labeled_control(TTR("Marker Name"), marker_insert_new_name));9657marker_insert_color = memnew(ColorPickerButton);9658marker_insert_color->set_edit_alpha(false);9659marker_insert_color->get_popup()->connect("about_to_popup", callable_mp(EditorNode::get_singleton(), &EditorNode::setup_color_picker).bind(marker_insert_color->get_picker()));9660marker_insert_vbox->add_child(_create_hbox_labeled_control(TTR("Marker Color"), marker_insert_color));9661marker_insert_error_dialog = memnew(AcceptDialog);9662marker_insert_error_dialog->set_ok_button_text(TTR("Close"));9663marker_insert_error_dialog->set_title(TTR("Error!"));9664marker_insert_confirm->add_child(marker_insert_error_dialog);96659666marker_rename_confirm = memnew(ConfirmationDialog);9667marker_rename_confirm->set_title(TTR("Rename Marker"));9668marker_rename_confirm->set_hide_on_ok(false);9669marker_rename_confirm->connect(SceneStringName(confirmed), callable_mp(this, &AnimationMarkerEdit::_marker_rename_confirmed));9670add_child(marker_rename_confirm);9671VBoxContainer *marker_rename_vbox = memnew(VBoxContainer);9672marker_rename_vbox->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT);9673marker_rename_confirm->add_child(marker_rename_vbox);9674Label *marker_rename_new_name_label = memnew(Label);9675marker_rename_new_name_label->set_text(TTR("Change Marker Name:"));9676marker_rename_vbox->add_child(marker_rename_new_name_label);9677marker_rename_new_name = memnew(LineEdit);9678marker_rename_new_name->set_accessibility_name(TTRC("Change Marker Name:"));9679marker_rename_new_name->connect(SceneStringName(text_changed), callable_mp(this, &AnimationMarkerEdit::_marker_rename_new_name_changed));9680marker_rename_confirm->register_text_enter(marker_rename_new_name);9681marker_rename_vbox->add_child(marker_rename_new_name);96829683marker_rename_error_dialog = memnew(AcceptDialog);9684marker_rename_error_dialog->set_ok_button_text(TTR("Close"));9685marker_rename_error_dialog->set_title(TTR("Error!"));9686marker_rename_confirm->add_child(marker_rename_error_dialog);9687}96889689float AnimationMarkerKeyEdit::get_time() const {9690return animation->get_marker_time(marker_name);9691}96929693void AnimationMarkerKeyEdit::_bind_methods() {9694ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMarkerKeyEdit::_hide_script_from_inspector);9695ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMarkerKeyEdit::_hide_metadata_from_inspector);9696ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMarkerKeyEdit::_dont_undo_redo);9697ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMarkerKeyEdit::_is_read_only);9698ClassDB::bind_method(D_METHOD("_set_marker_name"), &AnimationMarkerKeyEdit::_set_marker_name);9699}97009701void AnimationMarkerKeyEdit::_set_marker_name(const StringName &p_name) {9702marker_name = p_name;9703}97049705bool AnimationMarkerKeyEdit::_set(const StringName &p_name, const Variant &p_value) {9706EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();97079708if (p_name == "color") {9709Color color = p_value;9710Color prev_color = animation->get_marker_color(marker_name);9711if (color != prev_color) {9712undo_redo->create_action(TTR("Edit Marker Color"), UndoRedo::MERGE_ENDS);9713undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color);9714undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, prev_color);9715undo_redo->add_do_method(marker_edit, "queue_redraw");9716undo_redo->add_undo_method(marker_edit, "queue_redraw");9717undo_redo->commit_action();9718}9719return true;9720}97219722return false;9723}97249725bool AnimationMarkerKeyEdit::_get(const StringName &p_name, Variant &r_ret) const {9726if (p_name == "name") {9727r_ret = marker_name;9728return true;9729}97309731if (p_name == "color") {9732r_ret = animation->get_marker_color(marker_name);9733return true;9734}97359736return false;9737}97389739void AnimationMarkerKeyEdit::_get_property_list(List<PropertyInfo> *p_list) const {9740if (animation.is_null()) {9741return;9742}97439744p_list->push_back(PropertyInfo(Variant::STRING_NAME, "name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY | PROPERTY_USAGE_EDITOR));9745p_list->push_back(PropertyInfo(Variant::COLOR, "color", PROPERTY_HINT_COLOR_NO_ALPHA));9746}97479748void AnimationMultiMarkerKeyEdit::_bind_methods() {9749ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMultiMarkerKeyEdit::_hide_script_from_inspector);9750ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMultiMarkerKeyEdit::_hide_metadata_from_inspector);9751ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMultiMarkerKeyEdit::_dont_undo_redo);9752ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMultiMarkerKeyEdit::_is_read_only);9753}97549755bool AnimationMultiMarkerKeyEdit::_set(const StringName &p_name, const Variant &p_value) {9756EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();9757if (p_name == "color") {9758Color color = p_value;97599760undo_redo->create_action(TTR("Multi Edit Marker Color"), UndoRedo::MERGE_ENDS);97619762for (const StringName &marker_name : marker_names) {9763undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color);9764undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, animation->get_marker_color(marker_name));9765}97669767undo_redo->add_do_method(marker_edit, "queue_redraw");9768undo_redo->add_undo_method(marker_edit, "queue_redraw");9769undo_redo->commit_action();97709771return true;9772}97739774return false;9775}97769777bool AnimationMultiMarkerKeyEdit::_get(const StringName &p_name, Variant &r_ret) const {9778if (p_name == "color") {9779r_ret = animation->get_marker_color(marker_names[0]);9780return true;9781}97829783return false;9784}97859786void AnimationMultiMarkerKeyEdit::_get_property_list(List<PropertyInfo> *p_list) const {9787if (animation.is_null()) {9788return;9789}97909791p_list->push_back(PropertyInfo(Variant::COLOR, "color", PROPERTY_HINT_COLOR_NO_ALPHA));9792}97939794// AnimationMarkerKeyEditEditorPlugin97959796void AnimationMarkerKeyEditEditor::_time_edit_exited() {9797real_t new_time = spinner->get_value();97989799if (use_fps) {9800real_t fps = animation->get_step();9801if (fps > 0) {9802fps = 1.0 / fps;9803}9804new_time /= fps;9805}98069807real_t prev_time = animation->get_marker_time(marker_name);98089809if (Math::is_equal_approx(new_time, prev_time)) {9810return; // No change.9811}98129813EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();9814undo_redo->create_action(TTR("Animation Change Marker Time"));98159816Color color = animation->get_marker_color(marker_name);9817undo_redo->add_do_method(animation.ptr(), "add_marker", marker_name, new_time);9818undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color);9819undo_redo->add_undo_method(animation.ptr(), "remove_marker", marker_name);9820undo_redo->add_undo_method(animation.ptr(), "add_marker", marker_name, prev_time);9821undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, color);9822StringName existing_marker = animation->get_marker_at_time(new_time);9823if (existing_marker) {9824undo_redo->add_undo_method(animation.ptr(), "add_marker", existing_marker, animation->get_marker_time(existing_marker));9825undo_redo->add_undo_method(animation.ptr(), "set_marker_color", existing_marker, animation->get_marker_color(existing_marker));9826}9827AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();9828if (ape) {9829AnimationTrackEditor *ate = ape->get_track_editor();9830if (ate) {9831AnimationMarkerEdit *ame = ate->marker_edit;9832undo_redo->add_do_method(ame, "queue_redraw");9833undo_redo->add_undo_method(ame, "queue_redraw");9834}9835}9836undo_redo->commit_action();9837}98389839AnimationMarkerKeyEditEditor::AnimationMarkerKeyEditEditor(Ref<Animation> p_animation, const StringName &p_name, bool p_use_fps) {9840if (p_animation.is_null()) {9841return;9842}98439844animation = p_animation;9845use_fps = p_use_fps;9846marker_name = p_name;98479848set_label("Time");98499850spinner = memnew(EditorSpinSlider);9851spinner->set_focus_mode(Control::FOCUS_CLICK);9852spinner->set_min(0);9853spinner->set_allow_greater(true);9854spinner->set_allow_lesser(true);9855add_child(spinner);98569857float time = animation->get_marker_time(marker_name);98589859if (use_fps) {9860spinner->set_step(FPS_DECIMAL);9861real_t fps = animation->get_step();9862if (fps > 0) {9863fps = 1.0 / fps;9864}9865spinner->set_value(time * fps);9866spinner->connect("updown_pressed", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED);9867} else {9868spinner->set_step(SECOND_DECIMAL);9869spinner->set_value(time);9870spinner->set_max(animation->get_length());9871}98729873spinner->connect("ungrabbed", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED);9874spinner->connect("value_focus_exited", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED);9875}987698779878