Path: blob/master/editor/animation/animation_track_editor.cpp
9896 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/error/error_macros.h"34#include "core/input/input.h"35#include "editor/animation/animation_bezier_editor.h"36#include "editor/animation/animation_player_editor_plugin.h"37#include "editor/docks/inspector_dock.h"38#include "editor/editor_node.h"39#include "editor/editor_string_names.h"40#include "editor/editor_undo_redo_manager.h"41#include "editor/gui/editor_spin_slider.h"42#include "editor/gui/editor_validation_panel.h"43#include "editor/inspector/multi_node_edit.h"44#include "editor/scene/scene_tree_editor.h"45#include "editor/script/script_editor_plugin.h"46#include "editor/settings/editor_settings.h"47#include "editor/themes/editor_scale.h"48#include "scene/3d/mesh_instance_3d.h"49#include "scene/animation/animation_player.h"50#include "scene/animation/tween.h"51#include "scene/gui/check_box.h"52#include "scene/gui/color_picker.h"53#include "scene/gui/flow_container.h"54#include "scene/gui/grid_container.h"55#include "scene/gui/option_button.h"56#include "scene/gui/panel_container.h"57#include "scene/gui/separator.h"58#include "scene/gui/slider.h"59#include "scene/gui/spin_box.h"60#include "scene/gui/texture_rect.h"61#include "scene/gui/view_panner.h"62#include "scene/main/window.h"63#include "servers/audio/audio_stream.h"6465constexpr double FPS_DECIMAL = 1.0;66constexpr double SECOND_DECIMAL = 0.0001;6768void AnimationTrackKeyEdit::_bind_methods() {69ClassDB::bind_method(D_METHOD("_update_obj"), &AnimationTrackKeyEdit::_update_obj);70ClassDB::bind_method(D_METHOD("_key_ofs_changed"), &AnimationTrackKeyEdit::_key_ofs_changed);71ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationTrackKeyEdit::_hide_script_from_inspector);72ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationTrackKeyEdit::_hide_metadata_from_inspector);73ClassDB::bind_method(D_METHOD("get_root_path"), &AnimationTrackKeyEdit::get_root_path);74ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationTrackKeyEdit::_dont_undo_redo);75ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationTrackKeyEdit::_is_read_only);76}7778void AnimationTrackKeyEdit::_fix_node_path(Variant &value) {79NodePath np = value;8081if (np == NodePath()) {82return;83}8485Node *root = EditorNode::get_singleton()->get_tree()->get_root();8687Node *np_node = root->get_node_or_null(np);88ERR_FAIL_NULL(np_node);8990Node *edited_node = root->get_node_or_null(base);91ERR_FAIL_NULL(edited_node);9293value = edited_node->get_path_to(np_node);94}9596void AnimationTrackKeyEdit::_update_obj(const Ref<Animation> &p_anim) {97if (setting || animation != p_anim) {98return;99}100101notify_change();102}103104void AnimationTrackKeyEdit::_key_ofs_changed(const Ref<Animation> &p_anim, float from, float to) {105if (animation != p_anim || from != key_ofs) {106return;107}108109key_ofs = to;110111if (setting) {112return;113}114115notify_change();116}117118bool AnimationTrackKeyEdit::_set(const StringName &p_name, const Variant &p_value) {119int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX);120ERR_FAIL_COND_V(key == -1, false);121122String name = p_name;123if (name == "easing") {124float val = p_value;125float prev_val = animation->track_get_key_transition(track, key);126setting = true;127128EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();129undo_redo->create_action(TTR("Animation Change Transition"), UndoRedo::MERGE_ENDS);130undo_redo->add_do_method(animation.ptr(), "track_set_key_transition", track, key, val);131undo_redo->add_undo_method(animation.ptr(), "track_set_key_transition", track, key, prev_val);132undo_redo->add_do_method(this, "_update_obj", animation);133undo_redo->add_undo_method(this, "_update_obj", animation);134AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();135if (ape) {136undo_redo->add_do_method(ape, "_animation_update_key_frame");137undo_redo->add_undo_method(ape, "_animation_update_key_frame");138}139undo_redo->commit_action();140141setting = false;142return true;143}144145EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();146switch (animation->track_get_type(track)) {147case Animation::TYPE_POSITION_3D:148case Animation::TYPE_ROTATION_3D:149case Animation::TYPE_SCALE_3D: {150if (name == "position" || name == "rotation" || name == "scale") {151Variant old = animation->track_get_key_value(track, key);152setting = true;153String action_name;154switch (animation->track_get_type(track)) {155case Animation::TYPE_POSITION_3D:156action_name = TTR("Animation Change Position3D");157break;158case Animation::TYPE_ROTATION_3D:159action_name = TTR("Animation Change Rotation3D");160break;161case Animation::TYPE_SCALE_3D:162action_name = TTR("Animation Change Scale3D");163break;164default: {165}166}167168undo_redo->create_action(action_name);169undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, p_value);170undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, old);171undo_redo->add_do_method(this, "_update_obj", animation);172undo_redo->add_undo_method(this, "_update_obj", animation);173undo_redo->commit_action();174175setting = false;176return true;177}178179} break;180case Animation::TYPE_BLEND_SHAPE:181case Animation::TYPE_VALUE: {182if (name == "value") {183Variant value = p_value;184185if (value.get_type() == Variant::NODE_PATH) {186_fix_node_path(value);187}188189setting = true;190191undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);192Variant prev = animation->track_get_key_value(track, key);193undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, value);194undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, prev);195undo_redo->add_do_method(this, "_update_obj", animation);196undo_redo->add_undo_method(this, "_update_obj", animation);197AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();198if (ape) {199undo_redo->add_do_method(ape, "_animation_update_key_frame");200undo_redo->add_undo_method(ape, "_animation_update_key_frame");201}202undo_redo->commit_action();203204setting = false;205return true;206}207} break;208case Animation::TYPE_METHOD: {209Dictionary d_old = animation->track_get_key_value(track, key);210Dictionary d_new = d_old.duplicate();211212bool change_notify_deserved = false;213bool mergeable = false;214215if (name == "name") {216d_new["method"] = p_value;217} else if (name == "arg_count") {218Vector<Variant> args = d_old["args"];219args.resize(p_value);220d_new["args"] = args;221change_notify_deserved = true;222} else if (name.begins_with("args/")) {223Vector<Variant> args = d_old["args"];224int idx = name.get_slicec('/', 1).to_int();225ERR_FAIL_INDEX_V(idx, args.size(), false);226227String what = name.get_slicec('/', 2);228if (what == "type") {229Variant::Type t = Variant::Type(int(p_value));230231if (t != args[idx].get_type()) {232Callable::CallError err;233if (Variant::can_convert_strict(args[idx].get_type(), t)) {234Variant old = args[idx];235Variant *ptrs[1] = { &old };236Variant::construct(t, args.write[idx], (const Variant **)ptrs, 1, err);237} else {238Variant::construct(t, args.write[idx], nullptr, 0, err);239}240change_notify_deserved = true;241d_new["args"] = args;242}243} else if (what == "value") {244Variant value = p_value;245if (value.get_type() == Variant::NODE_PATH) {246_fix_node_path(value);247}248249args.write[idx] = value;250d_new["args"] = args;251mergeable = true;252}253}254255if (mergeable) {256undo_redo->create_action(TTR("Animation Change Call"), UndoRedo::MERGE_ENDS);257} else {258undo_redo->create_action(TTR("Animation Change Call"));259}260261setting = true;262undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, d_new);263undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, d_old);264undo_redo->add_do_method(this, "_update_obj", animation);265undo_redo->add_undo_method(this, "_update_obj", animation);266undo_redo->commit_action();267268setting = false;269if (change_notify_deserved) {270notify_change();271}272return true;273} break;274case Animation::TYPE_BEZIER: {275if (name == "value") {276const Variant &value = p_value;277278setting = true;279undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);280float prev = animation->bezier_track_get_key_value(track, key);281undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_value", track, key, value);282undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_value", track, key, prev);283undo_redo->add_do_method(this, "_update_obj", animation);284undo_redo->add_undo_method(this, "_update_obj", animation);285AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();286if (ape) {287undo_redo->add_do_method(ape, "_animation_update_key_frame");288undo_redo->add_undo_method(ape, "_animation_update_key_frame");289}290undo_redo->commit_action();291292setting = false;293return true;294}295296if (name == "in_handle") {297const Variant &value = p_value;298299setting = true;300undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);301Vector2 prev = animation->bezier_track_get_key_in_handle(track, key);302undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, value);303undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, prev);304undo_redo->add_do_method(this, "_update_obj", animation);305undo_redo->add_undo_method(this, "_update_obj", animation);306AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();307if (ape) {308undo_redo->add_do_method(ape, "_animation_update_key_frame");309undo_redo->add_undo_method(ape, "_animation_update_key_frame");310}311undo_redo->commit_action();312313setting = false;314return true;315}316317if (name == "out_handle") {318const Variant &value = p_value;319320setting = true;321undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);322Vector2 prev = animation->bezier_track_get_key_out_handle(track, key);323undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, value);324undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, prev);325undo_redo->add_do_method(this, "_update_obj", animation);326undo_redo->add_undo_method(this, "_update_obj", animation);327AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();328if (ape) {329undo_redo->add_do_method(ape, "_animation_update_key_frame");330undo_redo->add_undo_method(ape, "_animation_update_key_frame");331}332undo_redo->commit_action();333334setting = false;335return true;336}337338if (name == "handle_mode") {339const Variant &value = p_value;340341setting = true;342undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS, animation.ptr());343int prev_mode = animation->bezier_track_get_key_handle_mode(track, key);344Vector2 prev_in_handle = animation->bezier_track_get_key_in_handle(track, key);345Vector2 prev_out_handle = animation->bezier_track_get_key_out_handle(track, key);346undo_redo->add_do_method(editor, "_bezier_track_set_key_handle_mode", animation.ptr(), track, key, value);347undo_redo->add_do_method(this, "_update_obj", animation);348undo_redo->add_undo_method(editor, "_bezier_track_set_key_handle_mode", animation.ptr(), track, key, prev_mode);349undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, prev_in_handle);350undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, prev_out_handle);351undo_redo->add_undo_method(this, "_update_obj", animation);352AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();353if (ape) {354undo_redo->add_do_method(ape, "_animation_update_key_frame");355undo_redo->add_undo_method(ape, "_animation_update_key_frame");356}357undo_redo->commit_action();358359setting = false;360return true;361}362} break;363case Animation::TYPE_AUDIO: {364if (name == "stream") {365Ref<AudioStream> stream = p_value;366367setting = true;368undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);369Ref<Resource> prev = animation->audio_track_get_key_stream(track, key);370undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_stream", track, key, stream);371undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_stream", track, key, prev);372undo_redo->add_do_method(this, "_update_obj", animation);373undo_redo->add_undo_method(this, "_update_obj", animation);374undo_redo->commit_action();375376setting = false;377notify_change(); // To update limits for `start_offset`/`end_offset` sliders (they depend on the stream length).378return true;379}380381if (name == "start_offset") {382float value = p_value;383384setting = true;385undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);386float prev = animation->audio_track_get_key_start_offset(track, key);387undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_start_offset", track, key, value);388undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_start_offset", track, key, prev);389undo_redo->add_do_method(this, "_update_obj", animation);390undo_redo->add_undo_method(this, "_update_obj", animation);391undo_redo->commit_action();392393setting = false;394return true;395}396397if (name == "end_offset") {398float value = p_value;399400setting = true;401undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);402float prev = animation->audio_track_get_key_end_offset(track, key);403undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_end_offset", track, key, value);404undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_end_offset", track, key, prev);405undo_redo->add_do_method(this, "_update_obj", animation);406undo_redo->add_undo_method(this, "_update_obj", animation);407undo_redo->commit_action();408409setting = false;410return true;411}412} break;413case Animation::TYPE_ANIMATION: {414if (name == "animation") {415StringName anim_name = p_value;416417setting = true;418undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS);419StringName prev = animation->animation_track_get_key_animation(track, key);420undo_redo->add_do_method(animation.ptr(), "animation_track_set_key_animation", track, key, anim_name);421undo_redo->add_undo_method(animation.ptr(), "animation_track_set_key_animation", track, key, prev);422undo_redo->add_do_method(this, "_update_obj", animation);423undo_redo->add_undo_method(this, "_update_obj", animation);424undo_redo->commit_action();425426setting = false;427return true;428}429} break;430}431432return false;433}434435bool AnimationTrackKeyEdit::_get(const StringName &p_name, Variant &r_ret) const {436int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX);437ERR_FAIL_COND_V(key == -1, false);438439String name = p_name;440if (name == "easing") {441r_ret = animation->track_get_key_transition(track, key);442return true;443}444445switch (animation->track_get_type(track)) {446case Animation::TYPE_POSITION_3D:447case Animation::TYPE_ROTATION_3D:448case Animation::TYPE_SCALE_3D: {449if (name == "position" || name == "rotation" || name == "scale") {450r_ret = animation->track_get_key_value(track, key);451return true;452}453} break;454case Animation::TYPE_BLEND_SHAPE:455case Animation::TYPE_VALUE: {456if (name == "value") {457r_ret = animation->track_get_key_value(track, key);458return true;459}460461} break;462case Animation::TYPE_METHOD: {463Dictionary d = animation->track_get_key_value(track, key);464465if (name == "name") {466ERR_FAIL_COND_V(!d.has("method"), false);467r_ret = d["method"];468return true;469}470471ERR_FAIL_COND_V(!d.has("args"), false);472473Vector<Variant> args = d["args"];474475if (name == "arg_count") {476r_ret = args.size();477return true;478}479480if (name.begins_with("args/")) {481int idx = name.get_slicec('/', 1).to_int();482ERR_FAIL_INDEX_V(idx, args.size(), false);483484String what = name.get_slicec('/', 2);485if (what == "type") {486r_ret = args[idx].get_type();487return true;488}489490if (what == "value") {491r_ret = args[idx];492return true;493}494}495496} break;497case Animation::TYPE_BEZIER: {498if (name == "value") {499r_ret = animation->bezier_track_get_key_value(track, key);500return true;501}502503if (name == "in_handle") {504r_ret = animation->bezier_track_get_key_in_handle(track, key);505return true;506}507508if (name == "out_handle") {509r_ret = animation->bezier_track_get_key_out_handle(track, key);510return true;511}512513if (name == "handle_mode") {514r_ret = animation->bezier_track_get_key_handle_mode(track, key);515return true;516}517518} break;519case Animation::TYPE_AUDIO: {520if (name == "stream") {521r_ret = animation->audio_track_get_key_stream(track, key);522return true;523}524525if (name == "start_offset") {526r_ret = animation->audio_track_get_key_start_offset(track, key);527return true;528}529530if (name == "end_offset") {531r_ret = animation->audio_track_get_key_end_offset(track, key);532return true;533}534535} break;536case Animation::TYPE_ANIMATION: {537if (name == "animation") {538r_ret = animation->animation_track_get_key_animation(track, key);539return true;540}541542} break;543}544545return false;546}547548void AnimationTrackKeyEdit::_get_property_list(List<PropertyInfo> *p_list) const {549if (animation.is_null()) {550return;551}552553ERR_FAIL_INDEX(track, animation->get_track_count());554int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX);555ERR_FAIL_COND(key == -1);556557switch (animation->track_get_type(track)) {558case Animation::TYPE_POSITION_3D: {559p_list->push_back(PropertyInfo(Variant::VECTOR3, PNAME("position")));560} break;561case Animation::TYPE_ROTATION_3D: {562p_list->push_back(PropertyInfo(Variant::QUATERNION, PNAME("rotation")));563} break;564case Animation::TYPE_SCALE_3D: {565p_list->push_back(PropertyInfo(Variant::VECTOR3, PNAME("scale")));566} break;567case Animation::TYPE_BLEND_SHAPE: {568p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("value")));569} break;570case Animation::TYPE_VALUE: {571Variant v = animation->track_get_key_value(track, key);572573if (hint.type != Variant::NIL) {574PropertyInfo pi = hint;575pi.name = PNAME("value");576p_list->push_back(pi);577} else {578PropertyHint val_hint = PROPERTY_HINT_NONE;579String val_hint_string;580581if (v.get_type() == Variant::OBJECT) {582// Could actually check the object property if exists..? Yes I will!583Ref<Resource> res = v;584if (res.is_valid()) {585val_hint = PROPERTY_HINT_RESOURCE_TYPE;586val_hint_string = res->get_class();587}588}589590if (v.get_type() != Variant::NIL) {591p_list->push_back(PropertyInfo(v.get_type(), PNAME("value"), val_hint, val_hint_string));592}593}594595} break;596case Animation::TYPE_METHOD: {597p_list->push_back(PropertyInfo(Variant::STRING_NAME, PNAME("name")));598p_list->push_back(PropertyInfo(Variant::INT, PNAME("arg_count"), PROPERTY_HINT_RANGE, "0,32,1,or_greater"));599600Dictionary d = animation->track_get_key_value(track, key);601ERR_FAIL_COND(!d.has("args"));602Vector<Variant> args = d["args"];603String vtypes;604for (int i = 0; i < Variant::VARIANT_MAX; i++) {605if (i > 0) {606vtypes += ",";607}608vtypes += Variant::get_type_name(Variant::Type(i));609}610611for (int i = 0; i < args.size(); i++) {612p_list->push_back(PropertyInfo(Variant::INT, vformat("%s/%d/%s", PNAME("args"), i, PNAME("type")), PROPERTY_HINT_ENUM, vtypes));613if (args[i].get_type() != Variant::NIL) {614p_list->push_back(PropertyInfo(args[i].get_type(), vformat("%s/%d/%s", PNAME("args"), i, PNAME("value"))));615}616}617618} break;619case Animation::TYPE_BEZIER: {620Animation::HandleMode hm = animation->bezier_track_get_key_handle_mode(track, key);621p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("value")));622if (hm == Animation::HANDLE_MODE_LINEAR) {623p_list->push_back(PropertyInfo(Variant::VECTOR2, PNAME("in_handle"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY));624p_list->push_back(PropertyInfo(Variant::VECTOR2, PNAME("out_handle"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY));625} else {626p_list->push_back(PropertyInfo(Variant::VECTOR2, PNAME("in_handle")));627p_list->push_back(PropertyInfo(Variant::VECTOR2, PNAME("out_handle")));628}629p_list->push_back(PropertyInfo(Variant::INT, PNAME("handle_mode"), PROPERTY_HINT_ENUM, "Free,Linear,Balanced,Mirrored"));630631} break;632case Animation::TYPE_AUDIO: {633p_list->push_back(PropertyInfo(Variant::OBJECT, PNAME("stream"), PROPERTY_HINT_RESOURCE_TYPE, "AudioStream"));634Ref<AudioStream> audio_stream = animation->audio_track_get_key_stream(track, key);635String hint_string = vformat("0,%.4f,0.0001,or_greater", audio_stream.is_valid() ? audio_stream->get_length() : 3600.0);636p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("start_offset"), PROPERTY_HINT_RANGE, hint_string));637p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("end_offset"), PROPERTY_HINT_RANGE, hint_string));638639} break;640case Animation::TYPE_ANIMATION: {641String animations;642643if (root_path) {644AnimationPlayer *ap = Object::cast_to<AnimationPlayer>(root_path->get_node_or_null(animation->track_get_path(track)));645if (ap) {646List<StringName> anims;647ap->get_animation_list(&anims);648for (const StringName &E : anims) {649if (!animations.is_empty()) {650animations += ",";651}652653animations += String(E);654}655}656}657658if (!animations.is_empty()) {659animations += ",";660}661animations += "[stop]";662663p_list->push_back(PropertyInfo(Variant::STRING_NAME, PNAME("animation"), PROPERTY_HINT_ENUM, animations));664665} break;666}667668if (animation->track_get_type(track) == Animation::TYPE_VALUE) {669p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("easing"), PROPERTY_HINT_EXP_EASING));670}671}672673void AnimationTrackKeyEdit::notify_change() {674notify_property_list_changed();675}676677Node *AnimationTrackKeyEdit::get_root_path() {678return root_path;679}680681void AnimationTrackKeyEdit::set_use_fps(bool p_enable) {682use_fps = p_enable;683notify_property_list_changed();684}685686void AnimationMultiTrackKeyEdit::_bind_methods() {687ClassDB::bind_method(D_METHOD("_update_obj"), &AnimationMultiTrackKeyEdit::_update_obj);688ClassDB::bind_method(D_METHOD("_key_ofs_changed"), &AnimationMultiTrackKeyEdit::_key_ofs_changed);689ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMultiTrackKeyEdit::_hide_script_from_inspector);690ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMultiTrackKeyEdit::_hide_metadata_from_inspector);691ClassDB::bind_method(D_METHOD("get_root_path"), &AnimationMultiTrackKeyEdit::get_root_path);692ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMultiTrackKeyEdit::_dont_undo_redo);693ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMultiTrackKeyEdit::_is_read_only);694}695696void AnimationMultiTrackKeyEdit::_fix_node_path(Variant &value, NodePath &base) {697NodePath np = value;698699if (np == NodePath()) {700return;701}702703Node *root = EditorNode::get_singleton()->get_tree()->get_root();704705Node *np_node = root->get_node_or_null(np);706ERR_FAIL_NULL(np_node);707708Node *edited_node = root->get_node_or_null(base);709ERR_FAIL_NULL(edited_node);710711value = edited_node->get_path_to(np_node);712}713714void AnimationMultiTrackKeyEdit::_update_obj(const Ref<Animation> &p_anim) {715if (setting || animation != p_anim) {716return;717}718719notify_change();720}721722void AnimationMultiTrackKeyEdit::_key_ofs_changed(const Ref<Animation> &p_anim, float from, float to) {723if (animation != p_anim) {724return;725}726727for (const KeyValue<int, List<float>> &E : key_ofs_map) {728int key = 0;729for (const float &key_ofs : E.value) {730if (from != key_ofs) {731key++;732continue;733}734735int track = E.key;736key_ofs_map[track].get(key) = to;737738if (setting) {739return;740}741742notify_change();743744return;745}746}747}748749bool AnimationMultiTrackKeyEdit::_set(const StringName &p_name, const Variant &p_value) {750bool update_obj = false;751bool change_notify_deserved = false;752for (const KeyValue<int, List<float>> &E : key_ofs_map) {753int track = E.key;754for (const float &key_ofs : E.value) {755int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX);756ERR_FAIL_COND_V(key == -1, false);757758String name = p_name;759if (name == "easing") {760float val = p_value;761float prev_val = animation->track_get_key_transition(track, key);762763EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();764if (!setting) {765setting = true;766undo_redo->create_action(TTR("Animation Multi Change Transition"), UndoRedo::MERGE_ENDS);767}768undo_redo->add_do_method(animation.ptr(), "track_set_key_transition", track, key, val);769undo_redo->add_undo_method(animation.ptr(), "track_set_key_transition", track, key, prev_val);770update_obj = true;771}772773EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();774switch (animation->track_get_type(track)) {775case Animation::TYPE_POSITION_3D:776case Animation::TYPE_ROTATION_3D:777case Animation::TYPE_SCALE_3D: {778Variant old = animation->track_get_key_value(track, key);779if (!setting) {780String action_name;781switch (animation->track_get_type(track)) {782case Animation::TYPE_POSITION_3D:783action_name = TTR("Animation Multi Change Position3D");784break;785case Animation::TYPE_ROTATION_3D:786action_name = TTR("Animation Multi Change Rotation3D");787break;788case Animation::TYPE_SCALE_3D:789action_name = TTR("Animation Multi Change Scale3D");790break;791default: {792}793}794795setting = true;796undo_redo->create_action(action_name);797}798undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, p_value);799undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, old);800update_obj = true;801} break;802case Animation::TYPE_BLEND_SHAPE:803case Animation::TYPE_VALUE: {804if (name == "value") {805Variant value = p_value;806807if (value.get_type() == Variant::NODE_PATH) {808_fix_node_path(value, base_map[track]);809}810811if (!setting) {812setting = true;813undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);814}815Variant prev = animation->track_get_key_value(track, key);816undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, value);817undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, prev);818update_obj = true;819}820} break;821case Animation::TYPE_METHOD: {822Dictionary d_old = animation->track_get_key_value(track, key);823Dictionary d_new = d_old.duplicate();824825bool mergeable = false;826827if (name == "name") {828d_new["method"] = p_value;829} else if (name == "arg_count") {830Vector<Variant> args = d_old["args"];831args.resize(p_value);832d_new["args"] = args;833change_notify_deserved = true;834} else if (name.begins_with("args/")) {835Vector<Variant> args = d_old["args"];836int idx = name.get_slicec('/', 1).to_int();837ERR_FAIL_INDEX_V(idx, args.size(), false);838839String what = name.get_slicec('/', 2);840if (what == "type") {841Variant::Type t = Variant::Type(int(p_value));842843if (t != args[idx].get_type()) {844Callable::CallError err;845if (Variant::can_convert_strict(args[idx].get_type(), t)) {846Variant old = args[idx];847Variant *ptrs[1] = { &old };848Variant::construct(t, args.write[idx], (const Variant **)ptrs, 1, err);849} else {850Variant::construct(t, args.write[idx], nullptr, 0, err);851}852change_notify_deserved = true;853d_new["args"] = args;854}855} else if (what == "value") {856Variant value = p_value;857if (value.get_type() == Variant::NODE_PATH) {858_fix_node_path(value, base_map[track]);859}860861args.write[idx] = value;862d_new["args"] = args;863mergeable = true;864}865}866867Variant prev = animation->track_get_key_value(track, key);868869if (!setting) {870if (mergeable) {871undo_redo->create_action(TTR("Animation Multi Change Call"), UndoRedo::MERGE_ENDS);872} else {873undo_redo->create_action(TTR("Animation Multi Change Call"));874}875876setting = true;877}878879undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, d_new);880undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, d_old);881update_obj = true;882} break;883case Animation::TYPE_BEZIER: {884if (name == "value") {885const Variant &value = p_value;886887if (!setting) {888setting = true;889undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);890}891float prev = animation->bezier_track_get_key_value(track, key);892undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_value", track, key, value);893undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_value", track, key, prev);894update_obj = true;895} else if (name == "in_handle") {896const Variant &value = p_value;897898if (!setting) {899setting = true;900undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);901}902Vector2 prev = animation->bezier_track_get_key_in_handle(track, key);903undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, value);904undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, prev);905update_obj = true;906} else if (name == "out_handle") {907const Variant &value = p_value;908909if (!setting) {910setting = true;911undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);912}913Vector2 prev = animation->bezier_track_get_key_out_handle(track, key);914undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, value);915undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, prev);916update_obj = true;917} else if (name == "handle_mode") {918const Variant &value = p_value;919920if (!setting) {921setting = true;922undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS, animation.ptr());923}924int prev_mode = animation->bezier_track_get_key_handle_mode(track, key);925Vector2 prev_in_handle = animation->bezier_track_get_key_in_handle(track, key);926Vector2 prev_out_handle = animation->bezier_track_get_key_out_handle(track, key);927undo_redo->add_do_method(editor, "_bezier_track_set_key_handle_mode", animation.ptr(), track, key, value);928undo_redo->add_undo_method(editor, "_bezier_track_set_key_handle_mode", animation.ptr(), track, key, prev_mode);929undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, prev_in_handle);930undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, prev_out_handle);931update_obj = true;932}933} break;934case Animation::TYPE_AUDIO: {935if (name == "stream") {936Ref<AudioStream> stream = p_value;937938if (!setting) {939setting = true;940undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);941}942Ref<Resource> prev = animation->audio_track_get_key_stream(track, key);943undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_stream", track, key, stream);944undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_stream", track, key, prev);945update_obj = true;946} else if (name == "start_offset") {947float value = p_value;948949if (!setting) {950setting = true;951undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);952}953float prev = animation->audio_track_get_key_start_offset(track, key);954undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_start_offset", track, key, value);955undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_start_offset", track, key, prev);956update_obj = true;957} else if (name == "end_offset") {958float value = p_value;959960if (!setting) {961setting = true;962undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);963}964float prev = animation->audio_track_get_key_end_offset(track, key);965undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_end_offset", track, key, value);966undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_end_offset", track, key, prev);967update_obj = true;968}969} break;970case Animation::TYPE_ANIMATION: {971if (name == "animation") {972StringName anim_name = p_value;973974if (!setting) {975setting = true;976undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);977}978StringName prev = animation->animation_track_get_key_animation(track, key);979undo_redo->add_do_method(animation.ptr(), "animation_track_set_key_animation", track, key, anim_name);980undo_redo->add_undo_method(animation.ptr(), "animation_track_set_key_animation", track, key, prev);981update_obj = true;982}983} break;984}985}986}987988EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();989if (setting) {990if (update_obj) {991undo_redo->add_do_method(this, "_update_obj", animation);992undo_redo->add_undo_method(this, "_update_obj", animation);993}994995undo_redo->commit_action();996setting = false;997998if (change_notify_deserved) {999notify_change();1000}10011002return true;1003}10041005return false;1006}10071008bool AnimationMultiTrackKeyEdit::_get(const StringName &p_name, Variant &r_ret) const {1009for (const KeyValue<int, List<float>> &E : key_ofs_map) {1010int track = E.key;1011for (const float &key_ofs : E.value) {1012int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX);1013ERR_CONTINUE(key == -1);10141015String name = p_name;1016if (name == "easing") {1017r_ret = animation->track_get_key_transition(track, key);1018return true;1019}10201021switch (animation->track_get_type(track)) {1022case Animation::TYPE_POSITION_3D:1023case Animation::TYPE_ROTATION_3D:1024case Animation::TYPE_SCALE_3D: {1025if (name == "position" || name == "rotation" || name == "scale") {1026r_ret = animation->track_get_key_value(track, key);1027return true;1028}10291030} break;1031case Animation::TYPE_BLEND_SHAPE:1032case Animation::TYPE_VALUE: {1033if (name == "value") {1034r_ret = animation->track_get_key_value(track, key);1035return true;1036}10371038} break;1039case Animation::TYPE_METHOD: {1040Dictionary d = animation->track_get_key_value(track, key);10411042if (name == "name") {1043ERR_FAIL_COND_V(!d.has("method"), false);1044r_ret = d["method"];1045return true;1046}10471048ERR_FAIL_COND_V(!d.has("args"), false);10491050Vector<Variant> args = d["args"];10511052if (name == "arg_count") {1053r_ret = args.size();1054return true;1055}10561057if (name.begins_with("args/")) {1058int idx = name.get_slicec('/', 1).to_int();1059ERR_FAIL_INDEX_V(idx, args.size(), false);10601061String what = name.get_slicec('/', 2);1062if (what == "type") {1063r_ret = args[idx].get_type();1064return true;1065}10661067if (what == "value") {1068r_ret = args[idx];1069return true;1070}1071}10721073} break;1074case Animation::TYPE_BEZIER: {1075if (name == "value") {1076r_ret = animation->bezier_track_get_key_value(track, key);1077return true;1078}10791080if (name == "in_handle") {1081r_ret = animation->bezier_track_get_key_in_handle(track, key);1082return true;1083}10841085if (name == "out_handle") {1086r_ret = animation->bezier_track_get_key_out_handle(track, key);1087return true;1088}10891090if (name == "handle_mode") {1091r_ret = animation->bezier_track_get_key_handle_mode(track, key);1092return true;1093}10941095} break;1096case Animation::TYPE_AUDIO: {1097if (name == "stream") {1098r_ret = animation->audio_track_get_key_stream(track, key);1099return true;1100}11011102if (name == "start_offset") {1103r_ret = animation->audio_track_get_key_start_offset(track, key);1104return true;1105}11061107if (name == "end_offset") {1108r_ret = animation->audio_track_get_key_end_offset(track, key);1109return true;1110}11111112} break;1113case Animation::TYPE_ANIMATION: {1114if (name == "animation") {1115r_ret = animation->animation_track_get_key_animation(track, key);1116return true;1117}11181119} break;1120}1121}1122}11231124return false;1125}11261127void AnimationMultiTrackKeyEdit::_get_property_list(List<PropertyInfo> *p_list) const {1128if (animation.is_null()) {1129return;1130}11311132int first_track = -1;1133float first_key = -1.0;11341135bool same_track_type = true;1136bool same_key_type = true;1137for (const KeyValue<int, List<float>> &E : key_ofs_map) {1138int track = E.key;1139ERR_FAIL_INDEX(track, animation->get_track_count());11401141if (first_track < 0) {1142first_track = track;1143}11441145if (same_track_type) {1146if (animation->track_get_type(first_track) != animation->track_get_type(track)) {1147same_track_type = false;1148same_key_type = false;1149}11501151for (const float &F : E.value) {1152int key = animation->track_find_key(track, F, Animation::FIND_MODE_APPROX);1153ERR_FAIL_COND(key == -1);1154if (first_key < 0) {1155first_key = key;1156}11571158if (animation->track_get_key_value(first_track, first_key).get_type() != animation->track_get_key_value(track, key).get_type()) {1159same_key_type = false;1160}1161}1162}1163}11641165if (same_track_type) {1166switch (animation->track_get_type(first_track)) {1167case Animation::TYPE_POSITION_3D: {1168p_list->push_back(PropertyInfo(Variant::VECTOR3, "position"));1169} break;1170case Animation::TYPE_ROTATION_3D: {1171p_list->push_back(PropertyInfo(Variant::QUATERNION, "rotation"));1172} break;1173case Animation::TYPE_SCALE_3D: {1174p_list->push_back(PropertyInfo(Variant::VECTOR3, "scale"));1175} break;1176case Animation::TYPE_BLEND_SHAPE: {1177p_list->push_back(PropertyInfo(Variant::FLOAT, "value"));1178} break;1179case Animation::TYPE_VALUE: {1180if (same_key_type) {1181Variant v = animation->track_get_key_value(first_track, first_key);11821183if (hint.type != Variant::NIL) {1184PropertyInfo pi = hint;1185pi.name = "value";1186p_list->push_back(pi);1187} else {1188PropertyHint val_hint = PROPERTY_HINT_NONE;1189String val_hint_string;11901191if (v.get_type() == Variant::OBJECT) {1192// Could actually check the object property if exists..? Yes I will!1193Ref<Resource> res = v;1194if (res.is_valid()) {1195val_hint = PROPERTY_HINT_RESOURCE_TYPE;1196val_hint_string = res->get_class();1197}1198}11991200if (v.get_type() != Variant::NIL) {1201p_list->push_back(PropertyInfo(v.get_type(), "value", val_hint, val_hint_string));1202}1203}1204}12051206p_list->push_back(PropertyInfo(Variant::FLOAT, "easing", PROPERTY_HINT_EXP_EASING));1207} break;1208case Animation::TYPE_METHOD: {1209p_list->push_back(PropertyInfo(Variant::STRING_NAME, "name"));12101211p_list->push_back(PropertyInfo(Variant::INT, "arg_count", PROPERTY_HINT_RANGE, "0,32,1,or_greater"));12121213Dictionary d = animation->track_get_key_value(first_track, first_key);1214ERR_FAIL_COND(!d.has("args"));1215Vector<Variant> args = d["args"];1216String vtypes;1217for (int i = 0; i < Variant::VARIANT_MAX; i++) {1218if (i > 0) {1219vtypes += ",";1220}1221vtypes += Variant::get_type_name(Variant::Type(i));1222}12231224for (int i = 0; i < args.size(); i++) {1225p_list->push_back(PropertyInfo(Variant::INT, "args/" + itos(i) + "/type", PROPERTY_HINT_ENUM, vtypes));1226if (args[i].get_type() != Variant::NIL) {1227p_list->push_back(PropertyInfo(args[i].get_type(), "args/" + itos(i) + "/value"));1228}1229}1230} break;1231case Animation::TYPE_BEZIER: {1232p_list->push_back(PropertyInfo(Variant::FLOAT, "value"));1233p_list->push_back(PropertyInfo(Variant::VECTOR2, "in_handle"));1234p_list->push_back(PropertyInfo(Variant::VECTOR2, "out_handle"));1235p_list->push_back(PropertyInfo(Variant::INT, "handle_mode", PROPERTY_HINT_ENUM, "Free,Linear,Balanced,Mirrored"));1236} break;1237case Animation::TYPE_AUDIO: {1238p_list->push_back(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream"));1239p_list->push_back(PropertyInfo(Variant::FLOAT, "start_offset", PROPERTY_HINT_RANGE, "0,3600,0.0001,or_greater"));1240p_list->push_back(PropertyInfo(Variant::FLOAT, "end_offset", PROPERTY_HINT_RANGE, "0,3600,0.0001,or_greater"));1241} break;1242case Animation::TYPE_ANIMATION: {1243if (key_ofs_map.size() > 1) {1244break;1245}12461247String animations;12481249if (root_path) {1250AnimationPlayer *ap = Object::cast_to<AnimationPlayer>(root_path->get_node_or_null(animation->track_get_path(first_track)));1251if (ap) {1252List<StringName> anims;1253ap->get_animation_list(&anims);1254for (const StringName &anim : anims) {1255if (!animations.is_empty()) {1256animations += ",";1257}12581259animations += String(anim);1260}1261}1262}12631264if (!animations.is_empty()) {1265animations += ",";1266}1267animations += "[stop]";12681269p_list->push_back(PropertyInfo(Variant::STRING_NAME, "animation", PROPERTY_HINT_ENUM, animations));1270} break;1271}1272}1273}12741275void AnimationMultiTrackKeyEdit::notify_change() {1276notify_property_list_changed();1277}12781279Node *AnimationMultiTrackKeyEdit::get_root_path() {1280return root_path;1281}12821283void AnimationMultiTrackKeyEdit::set_use_fps(bool p_enable) {1284use_fps = p_enable;1285notify_property_list_changed();1286}12871288void AnimationTimelineEdit::_zoom_changed(double) {1289double zoom_pivot = 0; // Point on timeline to stay fixed.1290double zoom_pivot_delta = 0; // Delta seconds from left-most point on timeline to zoom pivot.12911292int timeline_width_pixels = get_size().width - get_buttons_width() - get_name_limit();1293double timeline_width_seconds = timeline_width_pixels / last_zoom_scale; // Length (in seconds) of visible part of timeline before zoom.1294double updated_timeline_width_seconds = timeline_width_pixels / get_zoom_scale(); // Length after zoom.1295double updated_timeline_half_width = updated_timeline_width_seconds / 2.0;1296bool zooming = updated_timeline_width_seconds < timeline_width_seconds;12971298double timeline_left = get_value();1299double timeline_right = timeline_left + timeline_width_seconds;1300double timeline_center = timeline_left + timeline_width_seconds / 2.0;13011302if (zoom_callback_occurred) { // Zooming with scroll wheel will focus on the position of the mouse.1303double zoom_scroll_origin_norm = (zoom_scroll_origin.x - get_name_limit()) / timeline_width_pixels;1304zoom_scroll_origin_norm = MAX(zoom_scroll_origin_norm, 0);1305zoom_pivot = timeline_left + timeline_width_seconds * zoom_scroll_origin_norm;1306zoom_pivot_delta = updated_timeline_width_seconds * zoom_scroll_origin_norm;1307zoom_callback_occurred = false;1308} else { // Zooming with slider will depend on the current play position.1309// If the play position is not in range, or exactly in the center, zoom in on the center.1310if (get_play_position() < timeline_left || get_play_position() > timeline_left + timeline_width_seconds || get_play_position() == timeline_center) {1311zoom_pivot = timeline_center;1312zoom_pivot_delta = updated_timeline_half_width;1313}1314// Zoom from right if play position is right of center,1315// and shrink from right if play position is left of center.1316else if ((get_play_position() > timeline_center) == zooming) {1317// If play position crosses to other side of center, center it.1318bool center_passed = (get_play_position() < timeline_right - updated_timeline_half_width) == zooming;1319zoom_pivot = center_passed ? get_play_position() : timeline_right;1320double center_offset = CMP_EPSILON * (zooming ? 1 : -1); // Small offset to prevent crossover.1321zoom_pivot_delta = center_passed ? updated_timeline_half_width + center_offset : updated_timeline_width_seconds;1322}1323// Zoom from left if play position is left of center,1324// and shrink from left if play position is right of center.1325else if ((get_play_position() <= timeline_center) == zooming) {1326// If play position crosses to other side of center, center it.1327bool center_passed = (get_play_position() > timeline_left + updated_timeline_half_width) == zooming;1328zoom_pivot = center_passed ? get_play_position() : timeline_left;1329double center_offset = CMP_EPSILON * (zooming ? -1 : 1); // Small offset to prevent crossover.1330zoom_pivot_delta = center_passed ? updated_timeline_half_width + center_offset : 0;1331}1332}13331334double hscroll_pos = zoom_pivot - zoom_pivot_delta;1335hscroll_pos = CLAMP(hscroll_pos, hscroll->get_min(), hscroll->get_max());13361337hscroll->set_value(hscroll_pos);1338hscroll_on_zoom_buffer = hscroll_pos; // In case of page update.1339last_zoom_scale = get_zoom_scale();13401341queue_redraw();1342play_position->queue_redraw();1343emit_signal(SNAME("zoom_changed"));1344}13451346float AnimationTimelineEdit::get_zoom_scale() const {1347return _get_zoom_scale(zoom->get_value());1348}13491350float AnimationTimelineEdit::_get_zoom_scale(double p_zoom_value) const {1351float zv = zoom->get_max() - p_zoom_value;1352if (zv < 1) {1353zv = 1.0 - zv;1354return Math::pow(1.0f + zv, 8.0f) * 100;1355} else {1356return 1.0 / Math::pow(zv, 8.0f) * 100;1357}1358}13591360void AnimationTimelineEdit::_anim_length_changed(double p_new_len) {1361if (editing) {1362return;1363}13641365p_new_len = MAX(SECOND_DECIMAL, p_new_len);1366if (use_fps && animation->get_step() > 0) {1367p_new_len *= animation->get_step();1368}13691370editing = true;1371EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();1372undo_redo->create_action(TTR("Change Animation Length"), UndoRedo::MERGE_ENDS);1373undo_redo->add_do_method(animation.ptr(), "set_length", p_new_len);1374undo_redo->add_undo_method(animation.ptr(), "set_length", animation->get_length());1375undo_redo->commit_action();1376editing = false;1377queue_redraw();13781379emit_signal(SNAME("length_changed"), p_new_len);1380}13811382void AnimationTimelineEdit::_anim_loop_pressed() {1383if (!read_only) {1384EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();1385undo_redo->create_action(TTR("Change Animation Loop"));1386switch (animation->get_loop_mode()) {1387case Animation::LOOP_NONE: {1388undo_redo->add_do_method(animation.ptr(), "set_loop_mode", Animation::LOOP_LINEAR);1389} break;1390case Animation::LOOP_LINEAR: {1391undo_redo->add_do_method(animation.ptr(), "set_loop_mode", Animation::LOOP_PINGPONG);1392} break;1393case Animation::LOOP_PINGPONG: {1394undo_redo->add_do_method(animation.ptr(), "set_loop_mode", Animation::LOOP_NONE);1395} break;1396default:1397break;1398}1399undo_redo->add_do_method(this, "update_values");1400undo_redo->add_undo_method(animation.ptr(), "set_loop_mode", animation->get_loop_mode());1401undo_redo->add_undo_method(this, "update_values");1402undo_redo->commit_action();1403} else {1404String base = animation->get_path();1405int srpos = base.find("::");1406if (srpos != -1) {1407base = animation->get_path().substr(0, srpos);1408}14091410if (FileAccess::exists(base + ".import")) {1411if (ResourceLoader::get_resource_type(base) == "PackedScene") {1412EditorNode::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."));1413} else {1414EditorNode::get_singleton()->show_warning(TTR("Can't change loop mode on animation instanced from an imported resource."));1415}1416} else {1417if (ResourceLoader::get_resource_type(base) == "PackedScene") {1418EditorNode::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."));1419} else {1420EditorNode::get_singleton()->show_warning(TTR("Can't change loop mode on animation embedded in another resource."));1421}1422}14231424update_values();1425}1426}14271428int AnimationTimelineEdit::get_buttons_width() const {1429const Ref<Texture2D> interp_mode = get_editor_theme_icon(SNAME("TrackContinuous"));1430const Ref<Texture2D> interp_type = get_editor_theme_icon(SNAME("InterpRaw"));1431const Ref<Texture2D> loop_type = get_editor_theme_icon(SNAME("InterpWrapClamp"));1432const Ref<Texture2D> remove_icon = get_editor_theme_icon(SNAME("Remove"));1433const Ref<Texture2D> down_icon = get_theme_icon(SNAME("select_arrow"), SNAME("Tree"));14341435const int h_separation = get_theme_constant(SNAME("h_separation"), SNAME("AnimationTrackEdit"));14361437int total_w = interp_mode->get_width() + interp_type->get_width() + loop_type->get_width() + remove_icon->get_width();1438total_w += (down_icon->get_width() + h_separation) * 4;14391440return total_w;1441}14421443int AnimationTimelineEdit::get_name_limit() const {1444Ref<Texture2D> hsize_icon = get_editor_theme_icon(SNAME("Hsize"));14451446int filter_track_width = filter_track->is_visible() ? filter_track->get_custom_minimum_size().width : 0;1447int limit = MAX(name_limit, add_track->get_minimum_size().width + hsize_icon->get_width() + filter_track_width + 16 * EDSCALE);14481449limit = MIN(limit, get_size().width - get_buttons_width() - 1);14501451return limit;1452}14531454void AnimationTimelineEdit::_notification(int p_what) {1455switch (p_what) {1456case NOTIFICATION_THEME_CHANGED: {1457add_track->set_button_icon(get_editor_theme_icon(SNAME("Add")));1458loop->set_button_icon(get_editor_theme_icon(SNAME("Loop")));1459time_icon->set_texture(get_editor_theme_icon(SNAME("Time")));1460filter_track->set_right_icon(get_editor_theme_icon(SNAME("Search")));14611462add_track->get_popup()->clear();1463add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyValue")), TTR("Property Track..."));1464add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyXPosition")), TTR("3D Position Track..."));1465add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyXRotation")), TTR("3D Rotation Track..."));1466add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyXScale")), TTR("3D Scale Track..."));1467add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyBlendShape")), TTR("Blend Shape Track..."));1468add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyCall")), TTR("Call Method Track..."));1469add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyBezier")), TTR("Bezier Curve Track..."));1470add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyAudio")), TTR("Audio Playback Track..."));1471add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyAnimation")), TTR("Animation Playback Track..."));1472} break;14731474case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {1475if (!EditorSettings::get_singleton()->check_changed_settings_in_group("editors/panning")) {1476break;1477}1478[[fallthrough]];1479}1480case NOTIFICATION_ENTER_TREE: {1481panner->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")));1482panner->setup_warped_panning(get_viewport(), EDITOR_GET("editors/panning/warped_mouse_panning"));1483} break;14841485case NOTIFICATION_RESIZED: {1486len_hb->set_position(Vector2(get_size().width - get_buttons_width(), 0));1487len_hb->set_size(Size2(get_buttons_width(), get_size().height));1488int hsize_icon_width = get_editor_theme_icon(SNAME("Hsize"))->get_width();1489add_track_hb->set_size(Size2(name_limit - ((hsize_icon_width + 16) * EDSCALE), 0));1490} break;14911492case NOTIFICATION_ACCESSIBILITY_UPDATE: {1493RID ae = get_accessibility_element();1494ERR_FAIL_COND(ae.is_null());14951496//TODO1497DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_STATIC_TEXT);1498DisplayServer::get_singleton()->accessibility_update_set_value(ae, TTR(vformat("The %s is not accessible at this time.", "Animation timeline editor")));1499} break;15001501case NOTIFICATION_DRAW: {1502int key_range = get_size().width - get_buttons_width() - get_name_limit();15031504if (animation.is_null()) {1505return;1506}15071508const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));1509const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));15101511const Ref<StyleBox> &stylebox_time_unavailable = get_theme_stylebox(SNAME("time_unavailable"), SNAME("AnimationTimelineEdit"));1512const Ref<StyleBox> &stylebox_time_available = get_theme_stylebox(SNAME("time_available"), SNAME("AnimationTimelineEdit"));15131514const Color v_line_primary_color = get_theme_color(SNAME("v_line_primary_color"), SNAME("AnimationTimelineEdit"));1515const Color v_line_secondary_color = get_theme_color(SNAME("v_line_secondary_color"), SNAME("AnimationTimelineEdit"));1516const Color h_line_color = get_theme_color(SNAME("h_line_color"), SNAME("AnimationTimelineEdit"));1517const Color font_primary_color = get_theme_color(SNAME("font_primary_color"), SNAME("AnimationTimelineEdit"));1518const Color font_secondary_color = get_theme_color(SNAME("font_secondary_color"), SNAME("AnimationTimelineEdit"));15191520const int v_line_primary_margin = get_theme_constant(SNAME("v_line_primary_margin"), SNAME("AnimationTimelineEdit"));1521const int v_line_secondary_margin = get_theme_constant(SNAME("v_line_secondary_margin"), SNAME("AnimationTimelineEdit"));1522const int v_line_primary_width = get_theme_constant(SNAME("v_line_primary_width"), SNAME("AnimationTimelineEdit"));1523const int v_line_secondary_width = get_theme_constant(SNAME("v_line_secondary_width"), SNAME("AnimationTimelineEdit"));1524const int text_primary_margin = get_theme_constant(SNAME("text_primary_margin"), SNAME("AnimationTimelineEdit"));1525const int text_secondary_margin = get_theme_constant(SNAME("text_secondary_margin"), SNAME("AnimationTimelineEdit"));15261527int zoomw = key_range;1528float scale = get_zoom_scale();1529int h = get_size().height;15301531float l = animation->get_length();1532if (l <= 0) {1533l = SECOND_DECIMAL; // Avoid crashor.1534}15351536Ref<Texture2D> hsize_icon = get_editor_theme_icon(SNAME("Hsize"));1537hsize_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());1538draw_texture(hsize_icon, hsize_rect.position);15391540{1541float time_min = 0;1542float time_max = animation->get_length();1543for (int i = 0; i < animation->get_track_count(); i++) {1544if (animation->track_get_key_count(i) > 0) {1545float beg = animation->track_get_key_time(i, 0);15461547if (beg < time_min) {1548time_min = beg;1549}15501551float end = animation->track_get_key_time(i, animation->track_get_key_count(i) - 1);15521553if (end > time_max) {1554time_max = end;1555}1556}1557}15581559PackedStringArray markers = animation->get_marker_names();1560if (markers.size() > 0) {1561float min_marker = animation->get_marker_time(markers[0]);1562float max_marker = animation->get_marker_time(markers[markers.size() - 1]);1563if (min_marker < time_min) {1564time_min = min_marker;1565}1566if (max_marker > time_max) {1567time_max = max_marker;1568}1569}15701571float extra = (zoomw / scale) * 0.5;15721573time_max += extra;1574set_min(time_min);1575set_max(time_max);15761577if (zoomw / scale < (time_max - time_min)) {1578hscroll->show();15791580} else {1581hscroll->hide();1582}1583}15841585set_page(zoomw / scale);15861587if (hscroll->is_visible() && hscroll_on_zoom_buffer >= 0) {1588hscroll->set_value(hscroll_on_zoom_buffer);1589hscroll_on_zoom_buffer = -1.0;1590}15911592int end_px = (l - get_value()) * scale;1593int begin_px = -get_value() * scale;15941595{1596draw_style_box(stylebox_time_unavailable, Rect2(Point2(get_name_limit(), 0), Point2(zoomw - 1, h)));15971598if (begin_px < zoomw && end_px > 0) {1599if (begin_px < 0) {1600begin_px = 0;1601}1602if (end_px > zoomw) {1603end_px = zoomw;1604}16051606draw_style_box(stylebox_time_available, Rect2(Point2(get_name_limit() + begin_px, 0), Point2(end_px - begin_px, h)));1607}1608}1609#define SC_ADJ 1001610int dec = 1;1611int step = 1;1612int decimals = 2;1613bool step_found = false;16141615const float period_width = font->get_char_size('.', font_size).width;1616float max_digit_width = font->get_char_size('0', font_size).width;1617for (int i = 1; i <= 9; i++) {1618const float digit_width = font->get_char_size('0' + i, font_size).width;1619max_digit_width = MAX(digit_width, max_digit_width);1620}1621const int max_sc = int(Math::ceil(zoomw / scale));1622const int max_sc_width = String::num(max_sc).length() * Math::ceil(max_digit_width);16231624const int min_margin = MAX(text_secondary_margin, text_primary_margin);16251626while (!step_found) {1627int min = max_sc_width;1628if (decimals > 0) {1629min += Math::ceil(period_width + max_digit_width * decimals);1630}16311632min += (min_margin * 2);16331634static const int _multp[3] = { 1, 2, 5 };1635for (int i = 0; i < 3; i++) {1636step = (_multp[i] * dec);1637if (step * scale / SC_ADJ > min) {1638step_found = true;1639break;1640}1641}1642if (step_found) {1643break;1644}1645dec *= 10;1646decimals--;1647if (decimals < 0) {1648decimals = 0;1649}1650}16511652if (use_fps) {1653float step_size = animation->get_step();1654if (step_size > 0) {1655int prev_frame_ofs = -10000000;16561657for (int i = 0; i < zoomw; i++) {1658float pos = get_value() + double(i) / scale;1659float prev = get_value() + (double(i) - 1.0) / scale;16601661int frame = pos / step_size;1662int prev_frame = prev / step_size;16631664bool sub = Math::floor(prev) == Math::floor(pos);16651666if (frame != prev_frame && i >= prev_frame_ofs) {1667int line_margin = sub ? v_line_secondary_margin : v_line_primary_margin;1668int line_width = sub ? v_line_secondary_width : v_line_primary_width;1669Color line_color = sub ? v_line_secondary_color : v_line_primary_color;16701671draw_line(Point2(get_name_limit() + i, 0 + line_margin), Point2(get_name_limit() + i, h - line_margin), line_color, line_width);16721673int text_margin = sub ? text_secondary_margin : text_primary_margin;16741675draw_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);16761677prev_frame_ofs = i + font->get_string_size(itos(frame), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x + text_margin;1678}1679}1680}16811682} else {1683for (int i = 0; i < zoomw; i++) {1684float pos = get_value() + double(i) / scale;1685float prev = get_value() + (double(i) - 1.0) / scale;16861687int sc = int(Math::floor(pos * SC_ADJ));1688int prev_sc = int(Math::floor(prev * SC_ADJ));16891690if ((sc / step) != (prev_sc / step) || (prev_sc < 0 && sc >= 0)) {1691int scd = sc < 0 ? prev_sc : sc;1692bool sub = (((scd - (scd % step)) % (dec * 10)) != 0);16931694int line_margin = sub ? v_line_secondary_margin : v_line_primary_margin;1695int line_width = sub ? v_line_secondary_width : v_line_primary_width;1696Color line_color = sub ? v_line_secondary_color : v_line_primary_color;16971698draw_line(Point2(get_name_limit() + i, 0 + line_margin), Point2(get_name_limit() + i, h - line_margin), line_color, line_width);16991700int text_margin = sub ? text_secondary_margin : text_primary_margin;17011702draw_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);1703}1704}1705}17061707draw_line(Vector2(0, get_size().height), get_size(), h_line_color, Math::round(EDSCALE));1708update_values();1709} break;1710}1711}17121713void AnimationTimelineEdit::set_animation(const Ref<Animation> &p_animation, bool p_read_only) {1714animation = p_animation;1715read_only = p_read_only;17161717length->set_read_only(read_only);17181719if (animation.is_valid()) {1720len_hb->show();1721filter_track->show();1722if (read_only) {1723add_track->hide();1724} else {1725add_track->show();1726}1727play_position->show();1728} else {1729len_hb->hide();1730filter_track->hide();1731add_track->hide();1732play_position->hide();1733}1734queue_redraw();1735}17361737Size2 AnimationTimelineEdit::get_minimum_size() const {1738Size2 ms = filter_track->get_minimum_size();1739const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));1740const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));1741ms.height = MAX(ms.height, font->get_height(font_size));1742ms.width = get_buttons_width() + add_track->get_minimum_size().width + get_editor_theme_icon(SNAME("Hsize"))->get_width() + 2 + 8 * EDSCALE;1743return ms;1744}17451746void AnimationTimelineEdit::set_zoom(Range *p_zoom) {1747zoom = p_zoom;1748zoom->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTimelineEdit::_zoom_changed));1749}17501751void AnimationTimelineEdit::auto_fit() {1752if (animation.is_null()) {1753return;1754}17551756float anim_end = animation->get_length();1757float anim_start = 0;17581759// Search for keyframe outside animation boundaries to include keyframes before animation start and after animation length.1760int track_count = animation->get_track_count();1761for (int track = 0; track < track_count; ++track) {1762for (int i = 0; i < animation->track_get_key_count(track); i++) {1763float key_time = animation->track_get_key_time(track, i);1764if (key_time > anim_end) {1765anim_end = key_time;1766}1767if (key_time < anim_start) {1768anim_start = key_time;1769}1770}1771}17721773float anim_length = anim_end - anim_start;1774int timeline_width_pixels = get_size().width - get_buttons_width() - get_name_limit();17751776// I want a little buffer at the end... (5% looks nice and we should keep some space for the bezier handles)1777timeline_width_pixels *= 0.95;17781779// The technique is to reuse the _get_zoom_scale function directly to be sure that the auto_fit is always calculated1780// the same way as the zoom slider. It's a little bit more calculation then doing the inverse of get_zoom_scale but1781// it's really easier to understand and should always be accurate.1782float new_zoom = zoom->get_max();1783while (true) {1784double test_zoom_scale = _get_zoom_scale(new_zoom);17851786if (anim_length * test_zoom_scale <= timeline_width_pixels) {1787// It fits...1788break;1789}17901791new_zoom -= zoom->get_step();17921793if (new_zoom <= zoom->get_min()) {1794new_zoom = zoom->get_min();1795break;1796}1797}17981799// Horizontal scroll to get_min which should include keyframes that are before the animation start.1800hscroll->set_value(hscroll->get_min());1801// Set the zoom value... the signal value_changed will be emitted and the timeline will be refreshed correctly!1802zoom->set_value(new_zoom);1803// The new zoom value must be applied correctly so the scrollbar are updated before we move the scrollbar to1804// the beginning of the animation, hence the call deferred.1805callable_mp(this, &AnimationTimelineEdit::_scroll_to_start).call_deferred();1806}18071808void AnimationTimelineEdit::_scroll_to_start() {1809// Horizontal scroll to get_min which should include keyframes that are before the animation start.1810hscroll->set_value(hscroll->get_min());1811}18121813void AnimationTimelineEdit::set_track_edit(AnimationTrackEdit *p_track_edit) {1814track_edit = p_track_edit;1815}18161817void AnimationTimelineEdit::set_play_position(float p_pos) {1818play_position_pos = p_pos;1819play_position->queue_redraw();1820}18211822float AnimationTimelineEdit::get_play_position() const {1823return play_position_pos;1824}18251826void AnimationTimelineEdit::update_play_position() {1827play_position->queue_redraw();1828}18291830void AnimationTimelineEdit::update_values() {1831if (animation.is_null() || editing) {1832return;1833}18341835editing = true;1836if (use_fps && animation->get_step() > 0.0) {1837length->set_value(animation->get_length() / animation->get_step());1838length->set_step(FPS_DECIMAL);1839length->set_tooltip_text(TTR("Animation length (frames)"));1840time_icon->set_tooltip_text(TTR("Animation length (frames)"));1841if (track_edit) {1842track_edit->editor->_update_key_edit();1843track_edit->editor->marker_edit->_update_key_edit();1844}1845} else {1846length->set_value(animation->get_length());1847length->set_step(SECOND_DECIMAL);1848length->set_tooltip_text(TTR("Animation length (seconds)"));1849time_icon->set_tooltip_text(TTR("Animation length (seconds)"));1850}18511852switch (animation->get_loop_mode()) {1853case Animation::LOOP_NONE: {1854loop->set_button_icon(get_editor_theme_icon(SNAME("Loop")));1855loop->set_pressed(false);1856} break;1857case Animation::LOOP_LINEAR: {1858loop->set_button_icon(get_editor_theme_icon(SNAME("Loop")));1859loop->set_pressed(true);1860} break;1861case Animation::LOOP_PINGPONG: {1862loop->set_button_icon(get_editor_theme_icon(SNAME("PingPongLoop")));1863loop->set_pressed(true);1864} break;1865default:1866break;1867}18681869editing = false;1870}18711872void AnimationTimelineEdit::_play_position_draw() {1873if (animation.is_null() || play_position_pos < 0) {1874return;1875}18761877float scale = get_zoom_scale();1878int h = play_position->get_size().height;18791880int px = (-get_value() + play_position_pos) * scale + get_name_limit();18811882if (px >= get_name_limit() && px < (play_position->get_size().width - get_buttons_width())) {1883Color color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));1884play_position->draw_line(Point2(px, 0), Point2(px, h), color, Math::round(2 * EDSCALE));1885play_position->draw_texture(1886get_editor_theme_icon(SNAME("TimelineIndicator")),1887Point2(px - get_editor_theme_icon(SNAME("TimelineIndicator"))->get_width() * 0.5, 0),1888color);1889}1890}18911892void AnimationTimelineEdit::gui_input(const Ref<InputEvent> &p_event) {1893ERR_FAIL_COND(p_event.is_null());18941895if (panner->gui_input(p_event, get_global_rect())) {1896accept_event();1897return;1898}18991900const Ref<InputEventMouseButton> mb = p_event;19011902if (mb.is_valid() && mb->is_pressed() && mb->is_alt_pressed() && mb->get_button_index() == MouseButton::WHEEL_UP) {1903if (track_edit) {1904track_edit->get_editor()->goto_prev_step(true);1905}1906accept_event();1907}19081909if (mb.is_valid() && mb->is_pressed() && mb->is_alt_pressed() && mb->get_button_index() == MouseButton::WHEEL_DOWN) {1910if (track_edit) {1911track_edit->get_editor()->goto_next_step(true);1912}1913accept_event();1914}19151916if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && hsize_rect.has_point(mb->get_position())) {1917dragging_hsize = true;1918dragging_hsize_from = mb->get_position().x;1919dragging_hsize_at = name_limit;1920}19211922if (mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && dragging_hsize) {1923dragging_hsize = false;1924}1925if (mb.is_valid() && mb->get_position().x > get_name_limit() && mb->get_position().x < (get_size().width - get_buttons_width())) {1926if (!panner->is_panning() && mb->get_button_index() == MouseButton::LEFT) {1927int x = mb->get_position().x - get_name_limit();19281929float ofs = x / get_zoom_scale() + get_value();1930emit_signal(SNAME("timeline_changed"), ofs, mb->is_alt_pressed());1931dragging_timeline = true;1932}1933}19341935if (dragging_timeline && mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && !mb->is_pressed()) {1936dragging_timeline = false;1937}19381939Ref<InputEventMouseMotion> mm = p_event;19401941if (mm.is_valid()) {1942if (dragging_hsize) {1943int ofs = mm->get_position().x - dragging_hsize_from;1944name_limit = dragging_hsize_at + ofs;1945// Make sure name_limit is clamped to the range that UI allows.1946name_limit = get_name_limit();1947int hsize_icon_width = get_editor_theme_icon(SNAME("Hsize"))->get_width();1948add_track_hb->set_size(Size2(name_limit - ((hsize_icon_width + 16) * EDSCALE), 0));1949queue_redraw();1950emit_signal(SNAME("name_limit_changed"));1951play_position->queue_redraw();1952}1953if (dragging_timeline) {1954int x = mm->get_position().x - get_name_limit();1955float ofs = x / get_zoom_scale() + get_value();1956emit_signal(SNAME("timeline_changed"), ofs, mm->is_alt_pressed());1957}1958}1959}19601961Control::CursorShape AnimationTimelineEdit::get_cursor_shape(const Point2 &p_pos) const {1962if (dragging_hsize || hsize_rect.has_point(p_pos)) {1963// Indicate that the track name column's width can be adjusted1964return Control::CURSOR_HSIZE;1965} else {1966return get_default_cursor_shape();1967}1968}19691970void AnimationTimelineEdit::_pan_callback(Vector2 p_scroll_vec, Ref<InputEvent> p_event) {1971set_value(get_value() - p_scroll_vec.x / get_zoom_scale());1972}19731974void AnimationTimelineEdit::_zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref<InputEvent> p_event) {1975double current_zoom_value = get_zoom()->get_value();1976zoom_scroll_origin = p_origin;1977zoom_callback_occurred = true;1978get_zoom()->set_value(MAX(0.01, current_zoom_value - (1.0 - p_zoom_factor)));1979}19801981void AnimationTimelineEdit::set_use_fps(bool p_use_fps) {1982use_fps = p_use_fps;1983queue_redraw();1984}19851986bool AnimationTimelineEdit::is_using_fps() const {1987return use_fps;1988}19891990void AnimationTimelineEdit::set_hscroll(HScrollBar *p_hscroll) {1991hscroll = p_hscroll;1992}19931994void AnimationTimelineEdit::_track_added(int p_track) {1995emit_signal(SNAME("track_added"), p_track);1996}19971998void AnimationTimelineEdit::_bind_methods() {1999ADD_SIGNAL(MethodInfo("zoom_changed"));2000ADD_SIGNAL(MethodInfo("name_limit_changed"));2001ADD_SIGNAL(MethodInfo("timeline_changed", PropertyInfo(Variant::FLOAT, "position"), PropertyInfo(Variant::BOOL, "timeline_only")));2002ADD_SIGNAL(MethodInfo("track_added", PropertyInfo(Variant::INT, "track")));2003ADD_SIGNAL(MethodInfo("length_changed", PropertyInfo(Variant::FLOAT, "size")));2004ADD_SIGNAL(MethodInfo("filter_changed"));20052006ClassDB::bind_method(D_METHOD("update_values"), &AnimationTimelineEdit::update_values);2007}20082009AnimationTimelineEdit::AnimationTimelineEdit() {2010name_limit = 150 * EDSCALE;20112012play_position = memnew(Control);2013play_position->set_mouse_filter(MOUSE_FILTER_PASS);2014add_child(play_position);2015play_position->set_anchors_and_offsets_preset(PRESET_FULL_RECT);2016play_position->connect(SceneStringName(draw), callable_mp(this, &AnimationTimelineEdit::_play_position_draw));20172018add_track_hb = memnew(HBoxContainer);2019add_child(add_track_hb);20202021add_track = memnew(MenuButton);2022add_track->set_tooltip_text(TTR("Select a new track by type to add to this animation."));2023add_track->set_position(Vector2(0, 0));2024add_track_hb->add_child(add_track);2025filter_track = memnew(LineEdit);2026filter_track->set_h_size_flags(SIZE_EXPAND_FILL);2027filter_track->set_custom_minimum_size(Vector2(120 * EDSCALE, 0));2028filter_track->set_placeholder(TTR("Filter Tracks"));2029filter_track->set_tooltip_text(TTR("Filter tracks by entering part of their node name or property."));2030filter_track->connect(SceneStringName(text_changed), callable_mp((AnimationTrackEditor *)this, &AnimationTrackEditor::_on_filter_updated));2031filter_track->set_clear_button_enabled(true);2032filter_track->hide();2033add_track_hb->add_child(filter_track);20342035len_hb = memnew(HBoxContainer);20362037Control *expander = memnew(Control);2038expander->set_h_size_flags(SIZE_EXPAND_FILL);2039expander->set_mouse_filter(MOUSE_FILTER_IGNORE);2040len_hb->add_child(expander);20412042time_icon = memnew(TextureRect);2043time_icon->set_v_size_flags(SIZE_SHRINK_CENTER);2044time_icon->set_tooltip_text(TTR("Animation length (seconds)"));2045len_hb->add_child(time_icon);20462047length = memnew(EditorSpinSlider);2048length->set_min(SECOND_DECIMAL);2049length->set_max(36000);2050length->set_step(SECOND_DECIMAL);2051length->set_allow_greater(true);2052length->set_custom_minimum_size(Vector2(70 * EDSCALE, 0));2053length->set_hide_slider(true);2054length->set_tooltip_text(TTR("Animation length (seconds)"));2055length->set_accessibility_name(TTRC("Animation length (seconds)"));2056length->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTimelineEdit::_anim_length_changed));2057len_hb->add_child(length);20582059loop = memnew(Button);2060loop->set_flat(true);2061loop->set_tooltip_text(TTR("Animation Looping"));2062loop->connect(SceneStringName(pressed), callable_mp(this, &AnimationTimelineEdit::_anim_loop_pressed));2063loop->set_toggle_mode(true);2064len_hb->add_child(loop);2065add_child(len_hb);20662067add_track->hide();2068add_track->get_popup()->connect("index_pressed", callable_mp(this, &AnimationTimelineEdit::_track_added));2069len_hb->hide();20702071panner.instantiate();2072panner->set_scroll_zoom_factor(SCROLL_ZOOM_FACTOR_IN);2073panner->set_callbacks(callable_mp(this, &AnimationTimelineEdit::_pan_callback), callable_mp(this, &AnimationTimelineEdit::_zoom_callback));2074panner->set_pan_axis(ViewPanner::PAN_AXIS_HORIZONTAL);20752076set_layout_direction(Control::LAYOUT_DIRECTION_LTR);2077}20782079////////////////////////////////////20802081void AnimationTrackEdit::_notification(int p_what) {2082switch (p_what) {2083case NOTIFICATION_THEME_CHANGED: {2084if (animation.is_null()) {2085return;2086}2087ERR_FAIL_INDEX(track, animation->get_track_count());20882089type_icon = _get_key_type_icon();2090selected_icon = get_editor_theme_icon(SNAME("KeySelected"));2091} break;20922093case NOTIFICATION_ACCESSIBILITY_UPDATE: {2094RID ae = get_accessibility_element();2095ERR_FAIL_COND(ae.is_null());20962097//TODO2098DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_STATIC_TEXT);2099DisplayServer::get_singleton()->accessibility_update_set_value(ae, TTR(vformat("The %s is not accessible at this time.", "Animation track editor")));2100} break;21012102case NOTIFICATION_DRAW: {2103if (animation.is_null()) {2104return;2105}2106ERR_FAIL_INDEX(track, animation->get_track_count());21072108int limit = timeline->get_name_limit();21092110const Ref<StyleBox> &stylebox_odd = get_theme_stylebox(SNAME("odd"), SNAME("AnimationTrackEdit"));2111const Ref<StyleBox> &stylebox_focus = get_theme_stylebox(SNAME("focus"), SNAME("AnimationTrackEdit"));2112const Ref<StyleBox> &stylebox_hover = get_theme_stylebox(SceneStringName(hover), SNAME("AnimationTrackEdit"));21132114const Color h_line_color = get_theme_color(SNAME("h_line_color"), SNAME("AnimationTrackEdit"));2115const int h_separation = get_theme_constant(SNAME("h_separation"), SNAME("AnimationTrackEdit"));2116const int outer_margin = get_theme_constant(SNAME("outer_margin"), SNAME("AnimationTrackEdit"));21172118if (track % 2 == 1) {2119// Draw a background over odd lines to make long lists of tracks easier to read.2120draw_style_box(stylebox_odd, Rect2(Point2(1 * EDSCALE, 0), get_size() - Size2(1 * EDSCALE, 0)));2121}21222123if (hovered) {2124// Draw hover feedback.2125draw_style_box(stylebox_hover, Rect2(Point2(1 * EDSCALE, 0), get_size() - Size2(1 * EDSCALE, 0)));2126}21272128if (has_focus()) {2129// Offside so the horizontal sides aren't cutoff.2130draw_style_box(stylebox_focus, Rect2(Point2(1 * EDSCALE, 0), get_size() - Size2(1 * EDSCALE, 0)));2131}21322133const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));2134const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));2135const Color color = get_theme_color(SceneStringName(font_color), SNAME("Label"));21362137const Color dc = get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor));21382139// Names and icons.21402141{2142Ref<Texture2D> check = animation->track_is_enabled(track) ? get_theme_icon(SNAME("checked"), SNAME("CheckBox")) : get_theme_icon(SNAME("unchecked"), SNAME("CheckBox"));21432144int ofs = in_group ? outer_margin : 0;21452146check_rect = Rect2(Point2(ofs, (get_size().height - check->get_height()) / 2).round(), check->get_size());2147draw_texture(check, check_rect.position);2148ofs += check->get_width() + h_separation;21492150Ref<Texture2D> key_type_icon = _get_key_type_icon();2151draw_texture(key_type_icon, Point2(ofs, (get_size().height - key_type_icon->get_height()) / 2).round());2152ofs += key_type_icon->get_width() + h_separation;21532154NodePath anim_path = animation->track_get_path(track);2155Node *node = nullptr;2156if (root) {2157node = root->get_node_or_null(anim_path);2158}21592160String text;2161Color text_color = color;2162if (node && EditorNode::get_singleton()->get_editor_selection()->is_selected(node)) {2163text_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));2164}21652166if (in_group) {2167if (animation->track_get_type(track) == Animation::TYPE_METHOD) {2168text = TTR("Functions:");2169} else if (animation->track_get_type(track) == Animation::TYPE_AUDIO) {2170text = TTR("Audio Clips:");2171} else if (animation->track_get_type(track) == Animation::TYPE_ANIMATION) {2172text = TTR("Animation Clips:");2173} else {2174text += anim_path.get_concatenated_subnames();2175}2176text_color.a *= 0.7;2177} else if (node) {2178Ref<Texture2D> icon = EditorNode::get_singleton()->get_object_icon(node, "Node");2179const Vector2 icon_size = Vector2(1, 1) * get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor));21802181icon_rect = Rect2(Point2(ofs, (get_size().height - check->get_height()) / 2).round(), icon->get_size());2182draw_texture_rect(icon, icon_rect);2183icon_cache = icon;21842185text = String() + node->get_name() + ":" + anim_path.get_concatenated_subnames();2186ofs += h_separation;2187ofs += icon_size.x;21882189} else {2190icon_cache = key_type_icon;21912192text = String(anim_path);2193}21942195path_cache = text;21962197path_rect = Rect2(ofs, 0, limit - ofs - h_separation, get_size().height);21982199Vector2 string_pos = Point2(ofs, (get_size().height - font->get_height(font_size)) / 2 + font->get_ascent(font_size));2200string_pos = string_pos.floor();2201draw_string(font, string_pos, text, HORIZONTAL_ALIGNMENT_LEFT, limit - ofs - h_separation, font_size, text_color);22022203draw_line(Point2(limit, 0), Point2(limit, get_size().height), h_line_color, Math::round(EDSCALE));2204}22052206// Marker sections.22072208{2209float scale = timeline->get_zoom_scale();2210int limit_end = get_size().width - timeline->get_buttons_width();22112212PackedStringArray section = editor->get_selected_section();2213if (section.size() == 2) {2214StringName start_marker = section[0];2215StringName end_marker = section[1];2216double start_time = animation->get_marker_time(start_marker);2217double end_time = animation->get_marker_time(end_marker);22182219// When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section.2220AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();2221if (editor->is_marker_moving_selection() && !(player && player->is_playing())) {2222start_time += editor->get_marker_moving_selection_offset();2223end_time += editor->get_marker_moving_selection_offset();2224}22252226if (start_time < animation->get_length() && end_time >= 0) {2227float start_ofs = MAX(0, start_time) - timeline->get_value();2228float end_ofs = MIN(animation->get_length(), end_time) - timeline->get_value();2229start_ofs = start_ofs * scale + limit;2230end_ofs = end_ofs * scale + limit;2231start_ofs = MAX(start_ofs, limit);2232end_ofs = MIN(end_ofs, limit_end);2233Rect2 rect;2234rect.set_position(Vector2(start_ofs, 0));2235rect.set_size(Vector2(end_ofs - start_ofs, get_size().height));22362237draw_rect(rect, Color(1, 0.1, 0.1, 0.2));2238}2239}2240}22412242// Marker overlays.22432244{2245float scale = timeline->get_zoom_scale();2246PackedStringArray markers = animation->get_marker_names();2247for (const StringName marker : markers) {2248double time = animation->get_marker_time(marker);2249if (editor->is_marker_selected(marker) && editor->is_marker_moving_selection()) {2250time += editor->get_marker_moving_selection_offset();2251}2252if (time >= 0) {2253float offset = time - timeline->get_value();2254offset = offset * scale + limit;2255Color marker_color = animation->get_marker_color(marker);2256marker_color.a = 0.2;2257draw_line(Point2(offset, 0), Point2(offset, get_size().height), marker_color, Math::round(EDSCALE));2258}2259}2260}22612262// Keyframes.22632264draw_bg(limit, get_size().width - timeline->get_buttons_width() - outer_margin);22652266{2267float scale = timeline->get_zoom_scale();2268int limit_end = get_size().width - timeline->get_buttons_width() - outer_margin;22692270for (int i = 0; i < animation->track_get_key_count(track); i++) {2271float offset = animation->track_get_key_time(track, i) - timeline->get_value();2272if (editor->is_key_selected(track, i) && editor->is_moving_selection()) {2273offset = offset + editor->get_moving_selection_offset();2274}2275offset = offset * scale + limit;2276if (i < animation->track_get_key_count(track) - 1) {2277float offset_n = animation->track_get_key_time(track, i + 1) - timeline->get_value();2278if (editor->is_key_selected(track, i + 1) && editor->is_moving_selection()) {2279offset_n = offset_n + editor->get_moving_selection_offset();2280}2281offset_n = offset_n * scale + limit;2282float offset_last = limit_end;2283if (i < animation->track_get_key_count(track) - 2) {2284offset_last = animation->track_get_key_time(track, i + 2) - timeline->get_value();2285if (editor->is_key_selected(track, i + 2) && editor->is_moving_selection()) {2286offset_last = offset_last + editor->get_moving_selection_offset();2287}2288offset_last = offset_last * scale + limit;2289}2290int limit_string = (editor->is_key_selected(track, i + 1) && editor->is_moving_selection()) ? int(offset_last) : int(offset_n);2291if (editor->is_key_selected(track, i) && editor->is_moving_selection()) {2292limit_string = int(MAX(limit_end, offset_last));2293}2294draw_key_link(i, scale, int(offset), int(offset_n), limit, limit_end);2295draw_key(i, scale, int(offset), editor->is_key_selected(track, i), limit, limit_string);2296continue;2297}22982299draw_key(i, scale, int(offset), editor->is_key_selected(track, i), limit, limit_end);2300}2301}23022303draw_fg(limit, get_size().width - timeline->get_buttons_width() - outer_margin);23042305// Buttons.23062307{2308Ref<Texture2D> wrap_icon[2] = {2309get_editor_theme_icon(SNAME("InterpWrapClamp")),2310get_editor_theme_icon(SNAME("InterpWrapLoop")),2311};2312Ref<Texture2D> interp_icon[5] = {2313get_editor_theme_icon(SNAME("InterpRaw")),2314get_editor_theme_icon(SNAME("InterpLinear")),2315get_editor_theme_icon(SNAME("InterpCubic")),2316get_editor_theme_icon(SNAME("InterpLinearAngle")),2317get_editor_theme_icon(SNAME("InterpCubicAngle")),2318};2319Ref<Texture2D> cont_icon[3] = {2320get_editor_theme_icon(SNAME("TrackContinuous")),2321get_editor_theme_icon(SNAME("TrackDiscrete")),2322get_editor_theme_icon(SNAME("TrackCapture"))2323};2324Ref<Texture2D> blend_icon[2] = {2325get_editor_theme_icon(SNAME("UseBlendEnable")),2326get_editor_theme_icon(SNAME("UseBlendDisable")),2327};23282329int ofs = get_size().width - timeline->get_buttons_width() - outer_margin;23302331const Ref<Texture2D> down_icon = get_theme_icon(SNAME("select_arrow"), SNAME("Tree"));23322333draw_line(Point2(ofs, 0), Point2(ofs, get_size().height), h_line_color, Math::round(EDSCALE));23342335ofs += h_separation;2336{2337// Callmode.23382339Animation::UpdateMode update_mode;23402341if (animation->track_get_type(track) == Animation::TYPE_VALUE) {2342update_mode = animation->value_track_get_update_mode(track);2343} else {2344update_mode = Animation::UPDATE_CONTINUOUS;2345}23462347Ref<Texture2D> update_icon = cont_icon[update_mode];23482349update_mode_rect.position.x = ofs;2350update_mode_rect.position.y = Math::round((get_size().height - update_icon->get_height()) / 2);2351update_mode_rect.size = update_icon->get_size();23522353if (!animation->track_is_compressed(track) && animation->track_get_type(track) == Animation::TYPE_VALUE) {2354draw_texture(update_icon, update_mode_rect.position);2355}2356if (animation->track_get_type(track) == Animation::TYPE_AUDIO) {2357Ref<Texture2D> use_blend_icon = blend_icon[animation->audio_track_is_use_blend(track) ? 0 : 1];2358Vector2 use_blend_icon_pos = update_mode_rect.position + (update_mode_rect.size - use_blend_icon->get_size()) / 2;2359draw_texture(use_blend_icon, use_blend_icon_pos);2360}2361// Make it easier to click.2362update_mode_rect.position.y = 0;2363update_mode_rect.size.y = get_size().height;23642365ofs += update_icon->get_width() + h_separation / 2;2366update_mode_rect.size.x += h_separation / 2;23672368if (!read_only) {2369if (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_AUDIO) {2370draw_texture(down_icon, Vector2(ofs, (get_size().height - down_icon->get_height()) / 2).round());2371update_mode_rect.size.x += down_icon->get_width();2372} else if (animation->track_get_type(track) == Animation::TYPE_BEZIER) {2373update_mode_rect.size.x += down_icon->get_width();2374update_mode_rect = Rect2();2375} else {2376update_mode_rect = Rect2();2377}2378} else {2379update_mode_rect = Rect2();2380}23812382ofs += down_icon->get_width();2383draw_line(Point2(ofs + h_separation * 0.5, 0), Point2(ofs + h_separation * 0.5, get_size().height), h_line_color, Math::round(EDSCALE));2384ofs += h_separation;2385}23862387{2388// Interp.23892390Animation::InterpolationType interp_mode = animation->track_get_interpolation_type(track);23912392Ref<Texture2D> icon = interp_icon[interp_mode];23932394interp_mode_rect.position.x = ofs;2395interp_mode_rect.position.y = Math::round((get_size().height - icon->get_height()) / 2);2396interp_mode_rect.size = icon->get_size();23972398if (!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)) {2399draw_texture(icon, interp_mode_rect.position);2400}2401// Make it easier to click.2402interp_mode_rect.position.y = 0;2403interp_mode_rect.size.y = get_size().height;24042405ofs += icon->get_width() + h_separation / 2;2406interp_mode_rect.size.x += h_separation / 2;24072408if (!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)) {2409draw_texture(down_icon, Vector2(ofs, (get_size().height - down_icon->get_height()) / 2).round());2410interp_mode_rect.size.x += down_icon->get_width();2411} else {2412interp_mode_rect = Rect2();2413}24142415ofs += down_icon->get_width();2416draw_line(Point2(ofs + h_separation * 0.5, 0), Point2(ofs + h_separation * 0.5, get_size().height), h_line_color, Math::round(EDSCALE));2417ofs += h_separation;2418}24192420{2421// Loop.24222423bool loop_wrap = animation->track_get_interpolation_loop_wrap(track);24242425Ref<Texture2D> icon = wrap_icon[loop_wrap ? 1 : 0];24262427loop_wrap_rect.position.x = ofs;2428loop_wrap_rect.position.y = Math::round((get_size().height - icon->get_height()) / 2);2429loop_wrap_rect.size = icon->get_size();24302431if (!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)) {2432draw_texture(icon, loop_wrap_rect.position);2433}24342435loop_wrap_rect.position.y = 0;2436loop_wrap_rect.size.y = get_size().height;24372438ofs += icon->get_width() + h_separation / 2;2439loop_wrap_rect.size.x += h_separation / 2;24402441if (!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)) {2442draw_texture(down_icon, Vector2(ofs, (get_size().height - down_icon->get_height()) / 2).round());2443loop_wrap_rect.size.x += down_icon->get_width();2444} else {2445loop_wrap_rect = Rect2();2446}24472448ofs += down_icon->get_width();2449draw_line(Point2(ofs + h_separation * 0.5, 0), Point2(ofs + h_separation * 0.5, get_size().height), h_line_color, Math::round(EDSCALE));2450ofs += h_separation;2451}24522453{2454// Erase.24552456Ref<Texture2D> icon = get_editor_theme_icon(animation->track_is_compressed(track) ? SNAME("Lock") : SNAME("Remove"));24572458remove_rect.position.x = ofs + ((get_size().width - ofs) - icon->get_width()) - outer_margin;2459remove_rect.position.y = Math::round((get_size().height - icon->get_height()) / 2);2460remove_rect.size = icon->get_size();24612462if (read_only) {2463draw_texture(icon, remove_rect.position, dc);2464} else {2465draw_texture(icon, remove_rect.position);2466}2467}2468}24692470if (in_group) {2471draw_line(Vector2(timeline->get_name_limit(), get_size().height), get_size(), h_line_color, Math::round(EDSCALE));2472} else {2473draw_line(Vector2(0, get_size().height), get_size(), h_line_color, Math::round(EDSCALE));2474}24752476if (dropping_at != 0) {2477Color drop_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));2478if (dropping_at < 0) {2479draw_line(Vector2(0, 0), Vector2(get_size().width, 0), drop_color, Math::round(EDSCALE));2480} else {2481draw_line(Vector2(0, get_size().height), get_size(), drop_color, Math::round(EDSCALE));2482}2483}2484} break;24852486case NOTIFICATION_MOUSE_ENTER:2487hovered = true;2488queue_redraw();2489break;2490case NOTIFICATION_MOUSE_EXIT:2491hovered = false;2492// When the mouse cursor exits the track, we're no longer hovering any keyframe.2493hovering_key_idx = -1;2494queue_redraw();2495[[fallthrough]];2496case NOTIFICATION_DRAG_END: {2497cancel_drop();2498} break;2499}2500}25012502int AnimationTrackEdit::get_key_height() const {2503if (animation.is_null()) {2504return 0;2505}25062507return type_icon->get_height();2508}25092510Rect2 AnimationTrackEdit::get_key_rect(int p_index, float p_pixels_sec) {2511if (animation.is_null()) {2512return Rect2();2513}2514Rect2 rect = Rect2(-type_icon->get_width() / 2, 0, type_icon->get_width(), get_size().height);25152516// Make it a big easier to click.2517rect.position.x -= rect.size.x * 0.5;2518rect.size.x *= 2;2519return rect;2520}25212522bool AnimationTrackEdit::is_key_selectable_by_distance() const {2523return true;2524}25252526void AnimationTrackEdit::draw_key_link(int p_index, float p_pixels_sec, int p_x, int p_next_x, int p_clip_left, int p_clip_right) {2527if (p_next_x < p_clip_left) {2528return;2529}2530if (p_x > p_clip_right) {2531return;2532}25332534Variant current = animation->track_get_key_value(get_track(), p_index);2535Variant next = animation->track_get_key_value(get_track(), p_index + 1);2536if (current != next || animation->track_get_type(get_track()) == Animation::TrackType::TYPE_METHOD) {2537return;2538}25392540Color color = get_theme_color(SceneStringName(font_color), SNAME("Label"));2541color.a = 0.5;25422543int from_x = MAX(p_x, p_clip_left);2544int to_x = MIN(p_next_x, p_clip_right);25452546draw_line(Point2(from_x + 1, get_size().height / 2), Point2(to_x, get_size().height / 2), color, Math::round(2 * EDSCALE));2547}25482549void AnimationTrackEdit::draw_key(int p_index, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right) {2550if (animation.is_null()) {2551return;2552}25532554if (p_x < p_clip_left || p_x > p_clip_right) {2555return;2556}25572558Ref<Texture2D> icon_to_draw = p_selected ? selected_icon : type_icon;25592560if (animation->track_get_type(track) == Animation::TYPE_VALUE && !Math::is_equal_approx(animation->track_get_key_transition(track, p_index), real_t(1.0))) {2561// Use a different icon for keys with non-linear easing.2562icon_to_draw = get_editor_theme_icon(p_selected ? SNAME("KeyEasedSelected") : SNAME("KeyValueEased"));2563}25642565// Override type icon for invalid value keys, unless selected.2566if (!p_selected && animation->track_get_type(track) == Animation::TYPE_VALUE) {2567const Variant &v = animation->track_get_key_value(track, p_index);2568Variant::Type valid_type = Variant::NIL;2569if (!_is_value_key_valid(v, valid_type)) {2570icon_to_draw = get_editor_theme_icon(SNAME("KeyInvalid"));2571}2572}25732574Vector2 ofs(p_x - icon_to_draw->get_width() / 2, (get_size().height - icon_to_draw->get_height()) / 2);25752576if (animation->track_get_type(track) == Animation::TYPE_METHOD) {2577const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));2578const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));2579Color color = get_theme_color(SceneStringName(font_color), SNAME("Label"));2580color.a = 0.5;25812582Dictionary d = animation->track_get_key_value(track, p_index);2583String text;25842585if (d.has("method")) {2586text += String(d["method"]);2587}2588text += "(";2589Vector<Variant> args;2590if (d.has("args")) {2591args = d["args"];2592}2593for (int i = 0; i < args.size(); i++) {2594if (i > 0) {2595text += ", ";2596}2597text += args[i].get_construct_string();2598}2599text += ")";26002601int limit = ((p_selected && editor->is_moving_selection()) || editor->is_function_name_pressed()) ? 0 : MAX(0, p_clip_right - p_x - icon_to_draw->get_width() * 2);26022603if (limit > 0) {2604draw_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);2605}2606}26072608// Use a different color for the currently hovered key.2609// The color multiplier is chosen to work with both dark and light editor themes,2610// and on both unselected and selected key icons.2611draw_texture(2612icon_to_draw,2613ofs,2614p_index == hovering_key_idx ? get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")) : Color(1, 1, 1));2615}26162617// Helper.2618void AnimationTrackEdit::draw_rect_clipped(const Rect2 &p_rect, const Color &p_color, bool p_filled) {2619int clip_left = timeline->get_name_limit();2620int clip_right = get_size().width - timeline->get_buttons_width();26212622if (p_rect.position.x > clip_right) {2623return;2624}2625if (p_rect.position.x + p_rect.size.x < clip_left) {2626return;2627}2628Rect2 clip = Rect2(clip_left, 0, clip_right - clip_left, get_size().height);2629draw_rect(clip.intersection(p_rect), p_color, p_filled);2630}26312632void AnimationTrackEdit::draw_bg(int p_clip_left, int p_clip_right) {2633}26342635void AnimationTrackEdit::draw_fg(int p_clip_left, int p_clip_right) {2636}26372638void AnimationTrackEdit::draw_texture_region_clipped(const Ref<Texture2D> &p_texture, const Rect2 &p_rect, const Rect2 &p_region) {2639int clip_left = timeline->get_name_limit();2640int clip_right = get_size().width - timeline->get_buttons_width();26412642// Clip left and right.2643if (clip_left > p_rect.position.x + p_rect.size.x) {2644return;2645}2646if (clip_right < p_rect.position.x) {2647return;2648}26492650Rect2 rect = p_rect;2651Rect2 region = p_region;26522653if (clip_left > rect.position.x) {2654int rect_pixels = (clip_left - rect.position.x);2655int region_pixels = rect_pixels * region.size.x / rect.size.x;26562657rect.position.x += rect_pixels;2658rect.size.x -= rect_pixels;26592660region.position.x += region_pixels;2661region.size.x -= region_pixels;2662}26632664if (clip_right < rect.position.x + rect.size.x) {2665int rect_pixels = rect.position.x + rect.size.x - clip_right;2666int region_pixels = rect_pixels * region.size.x / rect.size.x;26672668rect.size.x -= rect_pixels;2669region.size.x -= region_pixels;2670}26712672draw_texture_rect_region(p_texture, rect, region);2673}26742675int AnimationTrackEdit::get_track() const {2676return track;2677}26782679Ref<Animation> AnimationTrackEdit::get_animation() const {2680return animation;2681}26822683void AnimationTrackEdit::set_animation_and_track(const Ref<Animation> &p_animation, int p_track, bool p_read_only) {2684animation = p_animation;2685read_only = p_read_only;26862687track = p_track;2688queue_redraw();26892690ERR_FAIL_INDEX(track, animation->get_track_count());26912692node_path = animation->track_get_path(p_track);2693type_icon = _get_key_type_icon();2694selected_icon = get_editor_theme_icon(SNAME("KeySelected"));2695}26962697NodePath AnimationTrackEdit::get_path() const {2698return node_path;2699}27002701Size2 AnimationTrackEdit::get_minimum_size() const {2702Ref<Texture2D> texture = get_editor_theme_icon(SNAME("Object"));2703const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));2704const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));2705const int separation = get_theme_constant(SNAME("v_separation"), SNAME("ItemList"));27062707int max_h = MAX(texture->get_height(), font->get_height(font_size));2708max_h = MAX(max_h, get_key_height());27092710return Vector2(1, max_h + separation);2711}27122713void AnimationTrackEdit::set_timeline(AnimationTimelineEdit *p_timeline) {2714timeline = p_timeline;2715timeline->set_track_edit(this);2716timeline->connect("zoom_changed", callable_mp(this, &AnimationTrackEdit::_zoom_changed));2717timeline->connect("name_limit_changed", callable_mp(this, &AnimationTrackEdit::_zoom_changed));2718}27192720void AnimationTrackEdit::set_editor(AnimationTrackEditor *p_editor) {2721editor = p_editor;2722}27232724void AnimationTrackEdit::_play_position_draw() {2725if (animation.is_null() || play_position_pos < 0) {2726return;2727}27282729float scale = timeline->get_zoom_scale();2730int h = get_size().height;27312732int px = (-timeline->get_value() + play_position_pos) * scale + timeline->get_name_limit();27332734if (px >= timeline->get_name_limit() && px < (get_size().width - timeline->get_buttons_width())) {2735Color color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));2736play_position->draw_line(Point2(px, 0), Point2(px, h), color, Math::round(2 * EDSCALE));2737}2738}27392740void AnimationTrackEdit::set_play_position(float p_pos) {2741play_position_pos = p_pos;2742play_position->queue_redraw();2743}27442745void AnimationTrackEdit::update_play_position() {2746play_position->queue_redraw();2747}27482749void AnimationTrackEdit::set_root(Node *p_root) {2750root = p_root;2751}27522753void AnimationTrackEdit::_zoom_changed() {2754queue_redraw();2755play_position->queue_redraw();2756}27572758void AnimationTrackEdit::_path_submitted(const String &p_text) {2759EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();2760undo_redo->create_action(TTR("Change Track Path"));2761undo_redo->add_do_method(animation.ptr(), "track_set_path", track, p_text);2762undo_redo->add_undo_method(animation.ptr(), "track_set_path", track, animation->track_get_path(track));2763undo_redo->commit_action();2764path_popup->hide();2765}27662767bool AnimationTrackEdit::_is_value_key_valid(const Variant &p_key_value, Variant::Type &r_valid_type) const {2768if (root == nullptr || !root->has_node_and_resource(animation->track_get_path(track))) {2769return false;2770}2771Ref<Resource> res;2772Vector<StringName> leftover_path;2773Node *node = root->get_node_and_resource(animation->track_get_path(track), res, leftover_path);27742775Object *obj = nullptr;2776if (res.is_valid()) {2777obj = res.ptr();2778} else if (node) {2779obj = node;2780}27812782bool prop_exists = false;2783if (obj) {2784r_valid_type = obj->get_static_property_type_indexed(leftover_path, &prop_exists);2785}27862787return (!prop_exists || Variant::can_convert(p_key_value.get_type(), r_valid_type));2788}27892790Ref<Texture2D> AnimationTrackEdit::_get_key_type_icon() const {2791const Ref<Texture2D> type_icons[9] = {2792get_editor_theme_icon(SNAME("KeyValue")),2793get_editor_theme_icon(SNAME("KeyTrackPosition")),2794get_editor_theme_icon(SNAME("KeyTrackRotation")),2795get_editor_theme_icon(SNAME("KeyTrackScale")),2796get_editor_theme_icon(SNAME("KeyTrackBlendShape")),2797get_editor_theme_icon(SNAME("KeyCall")),2798get_editor_theme_icon(SNAME("KeyBezier")),2799get_editor_theme_icon(SNAME("KeyAudio")),2800get_editor_theme_icon(SNAME("KeyAnimation"))2801};2802return type_icons[animation->track_get_type(track)];2803}28042805Control::CursorShape AnimationTrackEdit::get_cursor_shape(const Point2 &p_pos) const {2806if (command_or_control_pressed && animation->track_get_type(track) == Animation::TYPE_METHOD && hovering_key_idx != -1) {2807return Control::CURSOR_POINTING_HAND;2808}2809return get_default_cursor_shape();2810}28112812String AnimationTrackEdit::get_tooltip(const Point2 &p_pos) const {2813if (check_rect.has_point(p_pos)) {2814return TTR("Toggle this track on/off.");2815}28162817if (icon_rect.has_point(p_pos)) {2818return TTR("Select node in scene.");2819}28202821// Don't overlap track keys if they start at 0.2822if (path_rect.has_point(p_pos + Size2(type_icon->get_width(), 0))) {2823return String(animation->track_get_path(track));2824}28252826if (update_mode_rect.has_point(p_pos)) {2827if (animation->track_get_type(track) == Animation::TYPE_AUDIO) {2828return TTR("Use Blend");2829} else {2830return TTR("Update Mode (How this property is set)");2831}2832}28332834if (interp_mode_rect.has_point(p_pos)) {2835return TTR("Interpolation Mode");2836}28372838if (loop_wrap_rect.has_point(p_pos)) {2839return TTR("Loop Wrap Mode (Interpolate end with beginning on loop)");2840}28412842if (remove_rect.has_point(p_pos)) {2843return TTR("Remove this track.");2844}28452846int limit = timeline->get_name_limit();2847int limit_end = get_size().width - timeline->get_buttons_width();2848// Left Border including space occupied by keyframes on t=0.2849int limit_start_hitbox = limit - type_icon->get_width();28502851if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) {2852int key_idx = -1;2853float key_distance = 1e20;28542855// Select should happen in the opposite order of drawing for more accurate overlap select.2856for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) {2857Rect2 rect = const_cast<AnimationTrackEdit *>(this)->get_key_rect(i, timeline->get_zoom_scale());2858float offset = animation->track_get_key_time(track, i) - timeline->get_value();2859offset = offset * timeline->get_zoom_scale() + limit;2860rect.position.x += offset;28612862if (rect.has_point(p_pos)) {2863if (const_cast<AnimationTrackEdit *>(this)->is_key_selectable_by_distance()) {2864float distance = Math::abs(offset - p_pos.x);2865if (key_idx == -1 || distance < key_distance) {2866key_idx = i;2867key_distance = distance;2868}2869} else {2870// First one does it.2871break;2872}2873}2874}28752876if (key_idx != -1) {2877String text = TTR("Time (s):") + " " + TS->format_number(rtos(Math::snapped(animation->track_get_key_time(track, key_idx), SECOND_DECIMAL))) + "\n";2878switch (animation->track_get_type(track)) {2879case Animation::TYPE_POSITION_3D: {2880Vector3 t = animation->track_get_key_value(track, key_idx);2881text += TTR("Position:") + " " + String(t) + "\n";2882} break;2883case Animation::TYPE_ROTATION_3D: {2884Quaternion t = animation->track_get_key_value(track, key_idx);2885text += TTR("Rotation:") + " " + String(t) + "\n";2886} break;2887case Animation::TYPE_SCALE_3D: {2888Vector3 t = animation->track_get_key_value(track, key_idx);2889text += TTR("Scale:") + " " + String(t) + "\n";2890} break;2891case Animation::TYPE_BLEND_SHAPE: {2892float t = animation->track_get_key_value(track, key_idx);2893text += TTR("Blend Shape:") + " " + itos(t) + "\n";2894} break;2895case Animation::TYPE_VALUE: {2896const Variant &v = animation->track_get_key_value(track, key_idx);2897text += TTR("Type:") + " " + Variant::get_type_name(v.get_type()) + "\n";2898Variant::Type valid_type = Variant::NIL;2899text += TTR("Value:") + " " + String(v);2900if (!_is_value_key_valid(v, valid_type)) {2901text += " " + vformat(TTR("(Invalid, expected type: %s)"), Variant::get_type_name(valid_type));2902}2903text += "\n" + TTR("Easing:") + " " + rtos(animation->track_get_key_transition(track, key_idx));29042905} break;2906case Animation::TYPE_METHOD: {2907Dictionary d = animation->track_get_key_value(track, key_idx);2908if (d.has("method")) {2909text += String(d["method"]);2910}2911text += "(";2912Vector<Variant> args;2913if (d.has("args")) {2914args = d["args"];2915}2916for (int i = 0; i < args.size(); i++) {2917if (i > 0) {2918text += ", ";2919}2920text += args[i].get_construct_string();2921}2922text += ")\n";29232924} break;2925case Animation::TYPE_BEZIER: {2926float h = animation->bezier_track_get_key_value(track, key_idx);2927text += TTR("Value:") + " " + rtos(h) + "\n";2928Vector2 ih = animation->bezier_track_get_key_in_handle(track, key_idx);2929text += TTR("In-Handle:") + " " + String(ih) + "\n";2930Vector2 oh = animation->bezier_track_get_key_out_handle(track, key_idx);2931text += TTR("Out-Handle:") + " " + String(oh) + "\n";2932int hm = animation->bezier_track_get_key_handle_mode(track, key_idx);2933switch (hm) {2934case Animation::HANDLE_MODE_FREE: {2935text += TTR("Handle mode: Free\n");2936} break;2937case Animation::HANDLE_MODE_LINEAR: {2938text += TTR("Handle mode: Linear\n");2939} break;2940case Animation::HANDLE_MODE_BALANCED: {2941text += TTR("Handle mode: Balanced\n");2942} break;2943case Animation::HANDLE_MODE_MIRRORED: {2944text += TTR("Handle mode: Mirrored\n");2945} break;2946}2947} break;2948case Animation::TYPE_AUDIO: {2949String stream_name = "null";2950Ref<Resource> stream = animation->audio_track_get_key_stream(track, key_idx);2951if (stream.is_valid()) {2952if (stream->get_path().is_resource_file()) {2953stream_name = stream->get_path().get_file();2954} else if (!stream->get_name().is_empty()) {2955stream_name = stream->get_name();2956} else {2957stream_name = stream->get_class();2958}2959}29602961text += TTR("Stream:") + " " + stream_name + "\n";2962float so = animation->audio_track_get_key_start_offset(track, key_idx);2963text += TTR("Start (s):") + " " + rtos(so) + "\n";2964float eo = animation->audio_track_get_key_end_offset(track, key_idx);2965text += TTR("End (s):") + " " + rtos(eo) + "\n";2966} break;2967case Animation::TYPE_ANIMATION: {2968String name = animation->animation_track_get_key_animation(track, key_idx);2969text += TTR("Animation Clip:") + " " + name + "\n";2970} break;2971}2972return text;2973}2974}29752976return Control::get_tooltip(p_pos);2977}29782979void AnimationTrackEdit::gui_input(const Ref<InputEvent> &p_event) {2980ERR_FAIL_COND(p_event.is_null());29812982if (p_event->is_pressed()) {2983if (ED_IS_SHORTCUT("animation_editor/duplicate_selected_keys", p_event)) {2984if (!read_only) {2985emit_signal(SNAME("duplicate_request"), -1.0, false);2986}2987accept_event();2988}2989if (ED_IS_SHORTCUT("animation_editor/cut_selected_keys", p_event)) {2990if (!read_only) {2991emit_signal(SNAME("cut_request"));2992}2993accept_event();2994}2995if (ED_IS_SHORTCUT("animation_editor/copy_selected_keys", p_event)) {2996if (!read_only) {2997emit_signal(SNAME("copy_request"));2998}2999accept_event();3000}30013002if (ED_IS_SHORTCUT("animation_editor/paste_keys", p_event)) {3003if (!read_only) {3004emit_signal(SNAME("paste_request"), -1.0, false);3005}3006accept_event();3007}30083009if (ED_IS_SHORTCUT("animation_editor/delete_selection", p_event)) {3010if (!read_only) {3011emit_signal(SNAME("delete_request"));3012}3013accept_event();3014}3015}30163017Ref<InputEventMouseButton> mb = p_event;3018if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {3019Point2 pos = mb->get_position();3020bool no_mod_key_pressed = !mb->is_alt_pressed() && !mb->is_shift_pressed() && !mb->is_command_or_control_pressed();3021if (mb->is_double_click() && !moving_selection && no_mod_key_pressed) {3022int x = pos.x - timeline->get_name_limit();3023float ofs = x / timeline->get_zoom_scale() + timeline->get_value();3024emit_signal(SNAME("timeline_changed"), ofs, false);3025}30263027if (!read_only) {3028if (check_rect.has_point(pos)) {3029EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();3030undo_redo->create_action(TTR("Toggle Track Enabled"));3031undo_redo->add_do_method(animation.ptr(), "track_set_enabled", track, !animation->track_is_enabled(track));3032undo_redo->add_undo_method(animation.ptr(), "track_set_enabled", track, animation->track_is_enabled(track));3033undo_redo->commit_action();3034queue_redraw();3035accept_event();3036}30373038if (icon_rect.has_point(pos)) {3039EditorSelection *editor_selection = EditorNode::get_singleton()->get_editor_selection();3040editor_selection->clear();3041Node *n = root->get_node_or_null(node_path);3042if (n) {3043editor_selection->add_node(n);3044}3045}30463047// Don't overlap track keys if they start at 0.3048if (path_rect.has_point(pos + Size2(type_icon->get_width(), 0))) {3049clicking_on_name = true;3050accept_event();3051}30523053if (update_mode_rect.has_point(pos)) {3054if (!menu) {3055menu = memnew(PopupMenu);3056add_child(menu);3057menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationTrackEdit::_menu_selected));3058}3059menu->clear();3060if (animation->track_get_type(track) == Animation::TYPE_AUDIO) {3061menu->add_icon_item(get_editor_theme_icon(SNAME("UseBlendEnable")), TTR("Use Blend"), MENU_USE_BLEND_ENABLED);3062menu->add_icon_item(get_editor_theme_icon(SNAME("UseBlendDisable")), TTR("Don't Use Blend"), MENU_USE_BLEND_DISABLED);3063} else {3064menu->add_icon_item(get_editor_theme_icon(SNAME("TrackContinuous")), TTR("Continuous"), MENU_CALL_MODE_CONTINUOUS);3065menu->add_icon_item(get_editor_theme_icon(SNAME("TrackDiscrete")), TTR("Discrete"), MENU_CALL_MODE_DISCRETE);3066menu->add_icon_item(get_editor_theme_icon(SNAME("TrackCapture")), TTR("Capture"), MENU_CALL_MODE_CAPTURE);3067}3068menu->reset_size();30693070moving_selection_attempt = false;3071moving_selection = false;30723073Vector2 popup_pos = get_screen_position() + update_mode_rect.position + Vector2(0, update_mode_rect.size.height);3074menu->set_position(popup_pos);3075menu->popup();3076accept_event();3077}30783079if (interp_mode_rect.has_point(pos)) {3080if (!menu) {3081menu = memnew(PopupMenu);3082add_child(menu);3083menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationTrackEdit::_menu_selected));3084}3085menu->clear();3086menu->add_icon_item(get_editor_theme_icon(SNAME("InterpRaw")), TTR("Nearest"), MENU_INTERPOLATION_NEAREST);3087menu->add_icon_item(get_editor_theme_icon(SNAME("InterpLinear")), TTR("Linear"), MENU_INTERPOLATION_LINEAR);3088menu->add_icon_item(get_editor_theme_icon(SNAME("InterpCubic")), TTR("Cubic"), MENU_INTERPOLATION_CUBIC);3089// Check whether it is angle property.3090AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();3091if (ape) {3092AnimationPlayer *ap = ape->get_player();3093if (ap) {3094NodePath npath = animation->track_get_path(track);3095Node *a_ap_root_node = ap->get_node_or_null(ap->get_root_node());3096Node *nd = nullptr;3097// We must test that we have a valid a_ap_root_node before trying to access its content to init the nd Node.3098if (a_ap_root_node) {3099nd = a_ap_root_node->get_node_or_null(NodePath(npath.get_concatenated_names()));3100}3101if (nd) {3102StringName prop = npath.get_concatenated_subnames();3103PropertyInfo prop_info;3104ClassDB::get_property_info(nd->get_class(), prop, &prop_info);3105#ifdef DISABLE_DEPRECATED3106bool is_angle = prop_info.type == Variant::FLOAT && prop_info.hint_string.contains("radians_as_degrees");3107#else3108bool is_angle = prop_info.type == Variant::FLOAT && prop_info.hint_string.contains("radians");3109#endif // DISABLE_DEPRECATED3110if (is_angle) {3111menu->add_icon_item(get_editor_theme_icon(SNAME("InterpLinearAngle")), TTR("Linear Angle"), MENU_INTERPOLATION_LINEAR_ANGLE);3112menu->add_icon_item(get_editor_theme_icon(SNAME("InterpCubicAngle")), TTR("Cubic Angle"), MENU_INTERPOLATION_CUBIC_ANGLE);3113}3114}3115}3116}3117menu->reset_size();31183119moving_selection_attempt = false;3120moving_selection = false;31213122Vector2 popup_pos = get_screen_position() + interp_mode_rect.position + Vector2(0, interp_mode_rect.size.height);3123menu->set_position(popup_pos);3124menu->popup();3125accept_event();3126}31273128if (loop_wrap_rect.has_point(pos)) {3129if (!menu) {3130menu = memnew(PopupMenu);3131add_child(menu);3132menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationTrackEdit::_menu_selected));3133}3134menu->clear();3135menu->add_icon_item(get_editor_theme_icon(SNAME("InterpWrapClamp")), TTR("Clamp Loop Interp"), MENU_LOOP_CLAMP);3136menu->add_icon_item(get_editor_theme_icon(SNAME("InterpWrapLoop")), TTR("Wrap Loop Interp"), MENU_LOOP_WRAP);3137menu->reset_size();31383139moving_selection_attempt = false;3140moving_selection = false;31413142Vector2 popup_pos = get_screen_position() + loop_wrap_rect.position + Vector2(0, loop_wrap_rect.size.height);3143menu->set_position(popup_pos);3144menu->popup();3145accept_event();3146}31473148if (remove_rect.has_point(pos)) {3149emit_signal(SNAME("remove_request"), track);3150accept_event();3151return;3152}3153}31543155if (mb->is_command_or_control_pressed() && _lookup_key(hovering_key_idx)) {3156accept_event();3157return;3158}31593160if (_try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), true)) {3161accept_event();3162}3163}31643165if (!moving_selection && mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {3166Point2 pos = mb->get_position();3167if (pos.x >= timeline->get_name_limit() && pos.x <= get_size().width - timeline->get_buttons_width()) {3168// Can do something with menu too! show insert key.3169float offset = (pos.x - timeline->get_name_limit()) / timeline->get_zoom_scale();3170if (!read_only) {3171if (!menu) {3172menu = memnew(PopupMenu);3173add_child(menu);3174menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationTrackEdit::_menu_selected));3175}31763177bool selected = _try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), false);31783179menu->clear();3180if (animation->track_get_type(track) == Animation::TYPE_METHOD) {3181if (hovering_key_idx != -1) {3182lookup_key_idx = hovering_key_idx;3183menu->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);3184menu->add_separator();3185}3186}3187menu->add_icon_item(get_editor_theme_icon(SNAME("Key")), TTR("Insert Key..."), MENU_KEY_INSERT);3188if (selected || editor->is_selection_active()) {3189menu->add_separator();3190menu->add_icon_item(get_editor_theme_icon(SNAME("Duplicate")), TTR("Duplicate Key(s)"), MENU_KEY_DUPLICATE);3191menu->add_icon_item(get_editor_theme_icon(SNAME("ActionCut")), TTR("Cut Key(s)"), MENU_KEY_CUT);3192menu->add_icon_item(get_editor_theme_icon(SNAME("ActionCopy")), TTR("Copy Key(s)"), MENU_KEY_COPY);3193}3194if (editor->is_key_clipboard_active()) {3195menu->add_icon_item(get_editor_theme_icon(SNAME("ActionPaste")), TTR("Paste Key(s)"), MENU_KEY_PASTE);3196}3197if (selected || editor->is_selection_active()) {3198AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();3199if ((!player->has_animation(SceneStringName(RESET)) || animation != player->get_animation(SceneStringName(RESET))) && editor->can_add_reset_key()) {3200menu->add_icon_item(get_editor_theme_icon(SNAME("Reload")), TTR("Add RESET Value(s)"), MENU_KEY_ADD_RESET);3201}32023203menu->add_separator();3204menu->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Delete Key(s)"), MENU_KEY_DELETE);3205}3206menu->reset_size();32073208moving_selection_attempt = false;3209moving_selection = false;32103211menu->set_position(get_screen_position() + get_local_mouse_position());3212menu->popup();32133214insert_at_pos = offset + timeline->get_value();3215accept_event();3216}3217}3218}32193220if (mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && clicking_on_name) {3221if (!path) {3222path_popup = memnew(Popup);3223path_popup->set_wrap_controls(true);3224add_child(path_popup);3225path = memnew(LineEdit);3226path_popup->add_child(path);3227path->set_anchors_and_offsets_preset(PRESET_FULL_RECT);3228path->connect(SceneStringName(text_submitted), callable_mp(this, &AnimationTrackEdit::_path_submitted));3229}32303231path->set_text(String(animation->track_get_path(track)));3232const Vector2 theme_ofs = path->get_theme_stylebox(CoreStringName(normal), SNAME("LineEdit"))->get_offset();32333234moving_selection_attempt = false;3235moving_selection = false;32363237path_popup->set_position(get_screen_position() + path_rect.position - theme_ofs);3238path_popup->set_size(path_rect.size);3239path_popup->popup();3240path->grab_focus();3241path->set_caret_column(path->get_text().length());3242clicking_on_name = false;3243}32443245if (mb.is_valid() && moving_selection_attempt) {3246if (!mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {3247moving_selection_attempt = false;3248if (moving_selection && moving_selection_effective) {3249if (std::abs(editor->get_moving_selection_offset()) > CMP_EPSILON) {3250emit_signal(SNAME("move_selection_commit"));3251}3252} else if (select_single_attempt != -1) {3253emit_signal(SNAME("select_key"), select_single_attempt, true);3254}3255moving_selection = false;3256select_single_attempt = -1;3257}32583259if (moving_selection && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {3260moving_selection_attempt = false;3261moving_selection = false;3262emit_signal(SNAME("move_selection_cancel"));3263}3264}32653266Ref<InputEventMouseMotion> mm = p_event;3267if (mm.is_valid()) {3268const int previous_hovering_key_idx = hovering_key_idx;32693270command_or_control_pressed = mm->is_command_or_control_pressed();32713272// Hovering compressed keyframes for editing is not possible.3273if (!animation->track_is_compressed(track)) {3274const float scale = timeline->get_zoom_scale();3275const int limit = timeline->get_name_limit();3276const int limit_end = get_size().width - timeline->get_buttons_width();3277// Left Border including space occupied by keyframes on t=0.3278const int limit_start_hitbox = limit - type_icon->get_width();3279const Point2 pos = mm->get_position();32803281if (pos.x >= limit_start_hitbox && pos.x <= limit_end) {3282// Use the same logic as key selection to ensure that hovering accurately represents3283// which key will be selected when clicking.3284int key_idx = -1;3285float key_distance = 1e20;32863287hovering_key_idx = -1;32883289// Hovering should happen in the opposite order of drawing for more accurate overlap hovering.3290for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) {3291Rect2 rect = get_key_rect(i, scale);3292float offset = animation->track_get_key_time(track, i) - timeline->get_value();3293offset = offset * scale + limit;3294rect.position.x += offset;32953296if (rect.has_point(pos)) {3297if (is_key_selectable_by_distance()) {3298const float distance = Math::abs(offset - pos.x);3299if (key_idx == -1 || distance < key_distance) {3300key_idx = i;3301key_distance = distance;3302hovering_key_idx = i;3303}3304} else {3305// First one does it.3306hovering_key_idx = i;3307break;3308}3309}3310}33113312if (hovering_key_idx != previous_hovering_key_idx) {3313// Required to draw keyframe hover feedback on the correct keyframe.3314queue_redraw();3315}3316}3317}3318}33193320if (mm.is_valid() && mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && moving_selection_attempt) {3321if (!moving_selection) {3322moving_selection = true;3323emit_signal(SNAME("move_selection_begin"));3324}33253326float moving_begin_time = ((moving_selection_mouse_begin_x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value();3327float new_time = ((mm->get_position().x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value();3328float delta = new_time - moving_begin_time;3329float snapped_time = editor->snap_time(moving_selection_pivot + delta);33303331float offset = 0.0;3332if (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)) {3333offset = snapped_time - moving_selection_pivot;3334moving_selection_effective = true;3335}33363337emit_signal(SNAME("move_selection"), offset);3338}3339}33403341bool AnimationTrackEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable) {3342if (!animation->track_is_compressed(track)) { // Selecting compressed keyframes for editing is not possible.3343float scale = timeline->get_zoom_scale();3344int limit = timeline->get_name_limit();3345int limit_end = get_size().width - timeline->get_buttons_width();3346// Left Border including space occupied by keyframes on t=0.3347int limit_start_hitbox = limit - type_icon->get_width();33483349if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) {3350int key_idx = -1;3351float key_distance = 1e20;33523353// Select should happen in the opposite order of drawing for more accurate overlap select.3354for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) {3355Rect2 rect = get_key_rect(i, scale);3356float offset = animation->track_get_key_time(track, i) - timeline->get_value();3357offset = offset * scale + limit;3358rect.position.x += offset;33593360if (rect.has_point(p_pos)) {3361if (is_key_selectable_by_distance()) {3362float distance = Math::abs(offset - p_pos.x);3363if (key_idx == -1 || distance < key_distance) {3364key_idx = i;3365key_distance = distance;3366}3367} else {3368// First one does it.3369key_idx = i;3370break;3371}3372}3373}33743375if (key_idx != -1) {3376if (p_aggregate) {3377if (editor->is_key_selected(track, key_idx)) {3378if (p_deselectable) {3379emit_signal(SNAME("deselect_key"), key_idx);3380moving_selection_pivot = 0.0f;3381moving_selection_mouse_begin_x = 0.0f;3382}3383} else {3384emit_signal(SNAME("select_key"), key_idx, false);3385moving_selection_attempt = true;3386moving_selection_effective = false;3387select_single_attempt = -1;3388moving_selection_pivot = animation->track_get_key_time(track, key_idx);3389moving_selection_mouse_begin_x = p_pos.x;3390}3391} else {3392if (!editor->is_key_selected(track, key_idx)) {3393emit_signal(SNAME("select_key"), key_idx, true);3394select_single_attempt = -1;3395} else {3396select_single_attempt = key_idx;3397}33983399moving_selection_attempt = true;3400moving_selection_effective = false;3401moving_selection_pivot = animation->track_get_key_time(track, key_idx);3402moving_selection_mouse_begin_x = p_pos.x;3403}34043405if (read_only) {3406moving_selection_attempt = false;3407moving_selection_pivot = 0.0f;3408moving_selection_mouse_begin_x = 0.0f;3409}3410return true;3411}3412}3413}3414return false;3415}34163417bool AnimationTrackEdit::_lookup_key(int p_key_idx) const {3418if (p_key_idx < 0 || p_key_idx >= animation->track_get_key_count(track)) {3419return false;3420}34213422if (animation->track_get_type(track) == Animation::TYPE_METHOD) {3423Node *target = root->get_node_or_null(animation->track_get_path(track));3424if (target) {3425StringName method = animation->method_track_get_name(track, p_key_idx);3426// First, check every script in the inheritance chain.3427bool found_in_script = false;3428Ref<Script> target_script_ref = target->get_script();3429Script *target_script = target_script_ref.ptr();3430while (target_script) {3431if (target_script->has_method(method)) {3432found_in_script = true;3433// Tell ScriptEditor to show the method's line.3434ScriptEditor::get_singleton()->script_goto_method(target_script, animation->method_track_get_name(track, p_key_idx));3435break;3436}3437target_script = target_script->get_base_script().ptr();3438}34393440if (!found_in_script) {3441// Not found in script, so it must be a native method.3442if (ClassDB::has_method(target->get_class_name(), method)) {3443// Show help page instead.3444ScriptEditor::get_singleton()->goto_help(vformat("class_method:%s:%s", target->get_class_name(), method));3445} else {3446// Still not found, which means the target doesn't have this method. Warn the user.3447WARN_PRINT_ED(TTR(vformat("Failed to lookup method: \"%s\"", method)));3448}3449}3450return true;3451}3452}3453return false;3454}34553456Variant AnimationTrackEdit::get_drag_data(const Point2 &p_point) {3457if (!clicking_on_name || (get_editor()->is_sorting_alphabetically() && !get_editor()->is_grouping_tracks())) {3458return Variant();3459}34603461Dictionary drag_data;3462drag_data["type"] = "animation_track";3463String base_path = String(animation->track_get_path(track));3464base_path = base_path.get_slicec(':', 0); // Remove sub-path.3465drag_data["group"] = base_path;3466drag_data["index"] = track;34673468Button *tb = memnew(Button);3469tb->set_flat(true);3470tb->set_text(path_cache);3471tb->set_button_icon(icon_cache);3472tb->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);3473tb->add_theme_constant_override("icon_max_width", get_theme_constant("class_icon_size", EditorStringName(Editor)));3474set_drag_preview(tb);34753476clicking_on_name = false;34773478return drag_data;3479}34803481bool AnimationTrackEdit::can_drop_data(const Point2 &p_point, const Variant &p_data) const {3482Dictionary d = p_data;3483if (!d.has("type")) {3484return false;3485}34863487String type = d["type"];3488if (type != "animation_track") {3489return false;3490}34913492// Don't allow moving tracks outside their groups.3493if (get_editor()->is_grouping_tracks()) {3494String base_path = String(animation->track_get_path(track));3495base_path = base_path.get_slicec(':', 0); // Remove sub-path.3496if (d["group"] != base_path) {3497return false;3498}3499}35003501if (p_point.y < get_size().height / 2) {3502dropping_at = -1;3503} else {3504dropping_at = 1;3505}35063507const_cast<AnimationTrackEdit *>(this)->queue_redraw();35083509return true;3510}35113512void AnimationTrackEdit::drop_data(const Point2 &p_point, const Variant &p_data) {3513Dictionary d = p_data;3514if (!d.has("type")) {3515return;3516}35173518String type = d["type"];3519if (type != "animation_track") {3520return;3521}35223523// Don't allow moving tracks outside their groups.3524if (get_editor()->is_grouping_tracks()) {3525String base_path = String(animation->track_get_path(track));3526base_path = base_path.get_slicec(':', 0); // Remove sub-path.3527if (d["group"] != base_path) {3528return;3529}3530}35313532int from_track = d["index"];35333534if (dropping_at < 0) {3535emit_signal(SNAME("dropped"), from_track, track);3536} else {3537emit_signal(SNAME("dropped"), from_track, track + 1);3538}3539}35403541void AnimationTrackEdit::_menu_selected(int p_index) {3542switch (p_index) {3543case MENU_CALL_MODE_CONTINUOUS:3544case MENU_CALL_MODE_DISCRETE:3545case MENU_CALL_MODE_CAPTURE: {3546Animation::UpdateMode update_mode = Animation::UpdateMode(p_index);3547EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();3548undo_redo->create_action(TTR("Change Animation Update Mode"));3549undo_redo->add_do_method(animation.ptr(), "value_track_set_update_mode", track, update_mode);3550undo_redo->add_undo_method(animation.ptr(), "value_track_set_update_mode", track, animation->value_track_get_update_mode(track));3551AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();3552if (ape) {3553undo_redo->add_do_method(ape, "_animation_update_key_frame");3554undo_redo->add_undo_method(ape, "_animation_update_key_frame");3555}3556undo_redo->commit_action();3557queue_redraw();35583559} break;3560case MENU_INTERPOLATION_NEAREST:3561case MENU_INTERPOLATION_LINEAR:3562case MENU_INTERPOLATION_CUBIC:3563case MENU_INTERPOLATION_LINEAR_ANGLE:3564case MENU_INTERPOLATION_CUBIC_ANGLE: {3565Animation::InterpolationType interp_mode = Animation::InterpolationType(p_index - MENU_INTERPOLATION_NEAREST);3566EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();3567undo_redo->create_action(TTR("Change Animation Interpolation Mode"));3568undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_type", track, interp_mode);3569undo_redo->add_undo_method(animation.ptr(), "track_set_interpolation_type", track, animation->track_get_interpolation_type(track));3570AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();3571if (ape) {3572undo_redo->add_do_method(ape, "_animation_update_key_frame");3573undo_redo->add_undo_method(ape, "_animation_update_key_frame");3574}3575undo_redo->commit_action();3576queue_redraw();3577} break;3578case MENU_LOOP_WRAP:3579case MENU_LOOP_CLAMP: {3580bool loop_wrap = p_index == MENU_LOOP_WRAP;3581EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();3582undo_redo->create_action(TTR("Change Animation Loop Mode"));3583undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_loop_wrap", track, loop_wrap);3584undo_redo->add_undo_method(animation.ptr(), "track_set_interpolation_loop_wrap", track, animation->track_get_interpolation_loop_wrap(track));3585AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();3586if (ape) {3587undo_redo->add_do_method(ape, "_animation_update_key_frame");3588undo_redo->add_undo_method(ape, "_animation_update_key_frame");3589}3590undo_redo->commit_action();3591queue_redraw();35923593} break;3594case MENU_KEY_INSERT: {3595emit_signal(SNAME("insert_key"), insert_at_pos);3596} break;3597case MENU_KEY_DUPLICATE: {3598emit_signal(SNAME("duplicate_request"), insert_at_pos, true);3599} break;3600case MENU_KEY_CUT: {3601emit_signal(SNAME("cut_request"));3602} break;3603case MENU_KEY_COPY: {3604emit_signal(SNAME("copy_request"));3605} break;3606case MENU_KEY_PASTE: {3607emit_signal(SNAME("paste_request"), insert_at_pos, true);3608} break;3609case MENU_KEY_ADD_RESET: {3610emit_signal(SNAME("create_reset_request"));36113612} break;3613case MENU_KEY_DELETE: {3614emit_signal(SNAME("delete_request"));36153616} break;3617case MENU_KEY_LOOKUP: {3618_lookup_key(lookup_key_idx);3619} break;3620case MENU_USE_BLEND_ENABLED:3621case MENU_USE_BLEND_DISABLED: {3622bool use_blend = p_index == MENU_USE_BLEND_ENABLED;3623EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();3624undo_redo->create_action(TTR("Change Animation Use Blend"));3625undo_redo->add_do_method(animation.ptr(), "audio_track_set_use_blend", track, use_blend);3626undo_redo->add_undo_method(animation.ptr(), "audio_track_set_use_blend", track, animation->audio_track_is_use_blend(track));3627undo_redo->commit_action();3628queue_redraw();3629} break;3630}3631}36323633void AnimationTrackEdit::cancel_drop() {3634if (dropping_at != 0) {3635dropping_at = 0;3636queue_redraw();3637}3638}36393640void AnimationTrackEdit::set_in_group(bool p_enable) {3641in_group = p_enable;3642queue_redraw();3643}36443645void AnimationTrackEdit::append_to_selection(const Rect2 &p_box, bool p_deselection) {3646if (animation->track_is_compressed(track)) {3647return; // Compressed keyframes can't be edited3648}3649// Left Border including space occupied by keyframes on t=0.3650int limit_start_hitbox = timeline->get_name_limit() - type_icon->get_width();3651Rect2 select_rect(limit_start_hitbox, 0, get_size().width - timeline->get_name_limit() - timeline->get_buttons_width(), get_size().height);3652select_rect = select_rect.intersection(p_box);36533654// Select should happen in the opposite order of drawing for more accurate overlap select.3655for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) {3656Rect2 rect = const_cast<AnimationTrackEdit *>(this)->get_key_rect(i, timeline->get_zoom_scale());3657float offset = animation->track_get_key_time(track, i) - timeline->get_value();3658offset = offset * timeline->get_zoom_scale() + timeline->get_name_limit();3659rect.position.x += offset;36603661if (select_rect.intersects(rect)) {3662if (p_deselection) {3663emit_signal(SNAME("deselect_key"), i);3664} else {3665emit_signal(SNAME("select_key"), i, false);3666}3667}3668}3669}36703671void AnimationTrackEdit::_bind_methods() {3672ADD_SIGNAL(MethodInfo("timeline_changed", PropertyInfo(Variant::FLOAT, "position"), PropertyInfo(Variant::BOOL, "timeline_only")));3673ADD_SIGNAL(MethodInfo("remove_request", PropertyInfo(Variant::INT, "track")));3674ADD_SIGNAL(MethodInfo("dropped", PropertyInfo(Variant::INT, "from_track"), PropertyInfo(Variant::INT, "to_track")));3675ADD_SIGNAL(MethodInfo("insert_key", PropertyInfo(Variant::FLOAT, "offset")));3676ADD_SIGNAL(MethodInfo("select_key", PropertyInfo(Variant::INT, "index"), PropertyInfo(Variant::BOOL, "single")));3677ADD_SIGNAL(MethodInfo("deselect_key", PropertyInfo(Variant::INT, "index")));36783679ADD_SIGNAL(MethodInfo("move_selection_begin"));3680ADD_SIGNAL(MethodInfo("move_selection", PropertyInfo(Variant::FLOAT, "offset")));3681ADD_SIGNAL(MethodInfo("move_selection_commit"));3682ADD_SIGNAL(MethodInfo("move_selection_cancel"));36833684ADD_SIGNAL(MethodInfo("duplicate_request", PropertyInfo(Variant::FLOAT, "offset"), PropertyInfo(Variant::BOOL, "is_offset_valid")));3685ADD_SIGNAL(MethodInfo("create_reset_request"));3686ADD_SIGNAL(MethodInfo("copy_request"));3687ADD_SIGNAL(MethodInfo("cut_request"));3688ADD_SIGNAL(MethodInfo("paste_request", PropertyInfo(Variant::FLOAT, "offset"), PropertyInfo(Variant::BOOL, "is_offset_valid")));3689ADD_SIGNAL(MethodInfo("delete_request"));3690}36913692AnimationTrackEdit::AnimationTrackEdit() {3693play_position = memnew(Control);3694play_position->set_mouse_filter(MOUSE_FILTER_PASS);3695add_child(play_position);3696play_position->set_anchors_and_offsets_preset(PRESET_FULL_RECT);3697play_position->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEdit::_play_position_draw));3698set_focus_mode(FOCUS_CLICK);3699set_mouse_filter(MOUSE_FILTER_PASS); // Scroll has to work too for selection.3700}37013702//////////////////////////////////////37033704AnimationTrackEdit *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) {3705if (get_script_instance()) {3706return 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));3707}3708return nullptr;3709}37103711AnimationTrackEdit *AnimationTrackEditPlugin::create_audio_track_edit() {3712if (get_script_instance()) {3713return Object::cast_to<AnimationTrackEdit>(get_script_instance()->call("create_audio_track_edit").operator Object *());3714}3715return nullptr;3716}37173718AnimationTrackEdit *AnimationTrackEditPlugin::create_animation_track_edit(Object *p_object) {3719if (get_script_instance()) {3720return Object::cast_to<AnimationTrackEdit>(get_script_instance()->call("create_animation_track_edit", p_object).operator Object *());3721}3722return nullptr;3723}37243725///////////////////////////////////////37263727void AnimationTrackEditGroup::_notification(int p_what) {3728switch (p_what) {3729case NOTIFICATION_THEME_CHANGED: {3730icon_size = Vector2(1, 1) * get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor));3731} break;37323733case NOTIFICATION_ACCESSIBILITY_UPDATE: {3734RID ae = get_accessibility_element();3735ERR_FAIL_COND(ae.is_null());37363737//TODO3738DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_STATIC_TEXT);3739DisplayServer::get_singleton()->accessibility_update_set_value(ae, TTR(vformat("The %s is not accessible at this time.", "Animation track group")));3740} break;37413742case NOTIFICATION_DRAW: {3743const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));3744const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));3745Color color = get_theme_color(SceneStringName(font_color), SNAME("Label"));37463747const Ref<StyleBox> &stylebox_header = get_theme_stylebox(SNAME("header"), SNAME("AnimationTrackEditGroup"));3748const int outer_margin = get_theme_constant(SNAME("outer_margin"), SNAME("AnimationTrackEdit"));37493750float v_margin_offset = stylebox_header->get_content_margin(SIDE_TOP) - stylebox_header->get_content_margin(SIDE_BOTTOM);37513752const Color h_line_color = get_theme_color(SNAME("h_line_color"), SNAME("AnimationTrackEditGroup"));3753const Color v_line_color = get_theme_color(SNAME("v_line_color"), SNAME("AnimationTrackEditGroup"));3754const int h_separation = get_theme_constant(SNAME("h_separation"), SNAME("AnimationTrackEditGroup"));37553756if (root) {3757Node *n = root->get_node_or_null(node);3758if (n && EditorNode::get_singleton()->get_editor_selection()->is_selected(n)) {3759color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));3760}3761}37623763draw_style_box(stylebox_header, Rect2(Point2(), get_size()));37643765int limit = timeline->get_name_limit();37663767// Section preview.37683769{3770float scale = timeline->get_zoom_scale();3771int limit_end = get_size().width - timeline->get_buttons_width();37723773PackedStringArray section = editor->get_selected_section();3774if (section.size() == 2) {3775StringName start_marker = section[0];3776StringName end_marker = section[1];3777double start_time = editor->get_current_animation()->get_marker_time(start_marker);3778double end_time = editor->get_current_animation()->get_marker_time(end_marker);37793780// When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section.3781AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();3782if (editor->is_marker_moving_selection() && !(player && player->is_playing())) {3783start_time += editor->get_marker_moving_selection_offset();3784end_time += editor->get_marker_moving_selection_offset();3785}37863787if (start_time < editor->get_current_animation()->get_length() && end_time >= 0) {3788float start_ofs = MAX(0, start_time) - timeline->get_value();3789float end_ofs = MIN(editor->get_current_animation()->get_length(), end_time) - timeline->get_value();3790start_ofs = start_ofs * scale + limit;3791end_ofs = end_ofs * scale + limit;3792start_ofs = MAX(start_ofs, limit);3793end_ofs = MIN(end_ofs, limit_end);3794Rect2 rect;3795rect.set_position(Vector2(start_ofs, 0));3796rect.set_size(Vector2(end_ofs - start_ofs, get_size().height));37973798draw_rect(rect, Color(1, 0.1, 0.1, 0.2));3799}3800}3801}38023803// Marker overlays.38043805{3806float scale = timeline->get_zoom_scale();3807PackedStringArray markers = editor->get_current_animation()->get_marker_names();3808for (const StringName marker : markers) {3809double time = editor->get_current_animation()->get_marker_time(marker);3810if (editor->is_marker_selected(marker) && editor->is_marker_moving_selection()) {3811time += editor->get_marker_moving_selection_offset();3812}3813if (time >= 0) {3814float offset = time - timeline->get_value();3815offset = offset * scale + limit;3816Color marker_color = editor->get_current_animation()->get_marker_color(marker);3817marker_color.a = 0.2;3818draw_line(Point2(offset, 0), Point2(offset, get_size().height), marker_color, Math::round(EDSCALE));3819}3820}3821}38223823draw_line(Point2(), Point2(get_size().width, 0), h_line_color, Math::round(EDSCALE));3824draw_line(Point2(timeline->get_name_limit(), 0), Point2(timeline->get_name_limit(), get_size().height), v_line_color, Math::round(EDSCALE));3825draw_line(Point2(get_size().width - timeline->get_buttons_width() - outer_margin, 0), Point2(get_size().width - timeline->get_buttons_width() - outer_margin, get_size().height), v_line_color, Math::round(EDSCALE));38263827int ofs = stylebox_header->get_margin(SIDE_LEFT);3828draw_texture_rect(icon, Rect2(Point2(ofs, (get_size().height - icon_size.y) / 2 + v_margin_offset).round(), icon_size));3829ofs += h_separation + icon_size.x;3830draw_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);38313832int px = (-timeline->get_value() + timeline->get_play_position()) * timeline->get_zoom_scale() + timeline->get_name_limit();38333834if (px >= timeline->get_name_limit() && px < (get_size().width - timeline->get_buttons_width())) {3835const Color accent = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));3836draw_line(Point2(px, 0), Point2(px, get_size().height), accent, Math::round(2 * EDSCALE));3837}3838} break;3839}3840}38413842void AnimationTrackEditGroup::gui_input(const Ref<InputEvent> &p_event) {3843ERR_FAIL_COND(p_event.is_null());38443845Ref<InputEventMouseButton> mb = p_event;3846if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {3847Point2 pos = mb->get_position();3848Rect2 node_name_rect = Rect2(0, 0, timeline->get_name_limit(), get_size().height);38493850if (node_name_rect.has_point(pos)) {3851EditorSelection *editor_selection = EditorNode::get_singleton()->get_editor_selection();3852editor_selection->clear();3853Node *n = root->get_node_or_null(node);3854if (n) {3855editor_selection->add_node(n);3856}3857}3858}3859}38603861void AnimationTrackEditGroup::set_type_and_name(const Ref<Texture2D> &p_type, const String &p_name, const NodePath &p_node) {3862icon = p_type;3863node_name = p_name;3864node = p_node;3865queue_redraw();3866update_minimum_size();3867}38683869Size2 AnimationTrackEditGroup::get_minimum_size() const {3870const Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));3871const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));3872const int separation = get_theme_constant(SNAME("v_separation"), SNAME("ItemList"));38733874const Ref<StyleBox> &header_style = get_theme_stylebox(SNAME("header"), SNAME("AnimationTrackEditGroup"));3875const int content_margin = header_style->get_content_margin(SIDE_TOP) + header_style->get_content_margin(SIDE_BOTTOM);38763877return Vector2(0, MAX(font->get_height(font_size), icon_size.y) + separation + content_margin);3878}38793880String AnimationTrackEditGroup::get_node_name() const {3881return node_name;3882}38833884void AnimationTrackEditGroup::set_timeline(AnimationTimelineEdit *p_timeline) {3885timeline = p_timeline;3886timeline->connect("zoom_changed", callable_mp(this, &AnimationTrackEditGroup::_zoom_changed));3887timeline->connect("name_limit_changed", callable_mp(this, &AnimationTrackEditGroup::_zoom_changed));3888}38893890void AnimationTrackEditGroup::set_root(Node *p_root) {3891root = p_root;3892queue_redraw();3893}38943895void AnimationTrackEditGroup::set_editor(AnimationTrackEditor *p_editor) {3896editor = p_editor;3897}38983899void AnimationTrackEditGroup::_zoom_changed() {3900queue_redraw();3901}39023903AnimationTrackEditGroup::AnimationTrackEditGroup() {3904set_mouse_filter(MOUSE_FILTER_PASS);3905}39063907//////////////////////////////////////39083909void AnimationTrackEditor::add_track_edit_plugin(const Ref<AnimationTrackEditPlugin> &p_plugin) {3910if (track_edit_plugins.has(p_plugin)) {3911return;3912}3913track_edit_plugins.push_back(p_plugin);3914}39153916void AnimationTrackEditor::remove_track_edit_plugin(const Ref<AnimationTrackEditPlugin> &p_plugin) {3917track_edit_plugins.erase(p_plugin);3918}39193920void AnimationTrackEditor::set_animation(const Ref<Animation> &p_anim, bool p_read_only) {3921if (animation != p_anim && _get_track_selected() >= 0) {3922track_edits[_get_track_selected()]->release_focus();3923}3924if (animation.is_valid()) {3925animation->disconnect_changed(callable_mp(this, &AnimationTrackEditor::_animation_changed));3926_clear_selection();3927}3928animation = p_anim;3929read_only = p_read_only;3930timeline->set_animation(p_anim, read_only);39313932marker_edit->set_animation(p_anim, read_only);3933marker_edit->set_play_position(timeline->get_play_position());39343935_cancel_bezier_edit();3936_update_tracks();39373938if (animation.is_valid()) {3939animation->connect_changed(callable_mp(this, &AnimationTrackEditor::_animation_changed));39403941hscroll->show();3942edit->set_disabled(read_only);3943step->set_block_signals(true);39443945_update_step_spinbox();3946step->set_block_signals(false);3947step->set_read_only(false);3948snap_keys->set_disabled(false);3949snap_timeline->set_disabled(false);3950fps_compat->set_disabled(false);3951snap_mode->set_disabled(false);3952auto_fit->set_disabled(false);3953auto_fit_bezier->set_disabled(false);39543955imported_anim_warning->hide();3956for (int i = 0; i < animation->get_track_count(); i++) {3957if (animation->track_is_imported(i)) {3958imported_anim_warning->show();3959break;3960}3961}39623963_check_bezier_exist();3964} else {3965hscroll->hide();3966edit->set_disabled(true);3967step->set_block_signals(true);3968step->set_value(0);3969step->set_block_signals(false);3970step->set_read_only(true);3971snap_keys->set_disabled(true);3972snap_timeline->set_disabled(true);3973fps_compat->set_disabled(true);3974snap_mode->set_disabled(true);3975bezier_edit_icon->set_disabled(true);3976auto_fit->set_disabled(true);3977auto_fit_bezier->set_disabled(true);3978}3979}39803981void AnimationTrackEditor::_check_bezier_exist() {3982bool is_exist = false;3983for (int i = 0; i < animation->get_track_count(); i++) {3984if (animation->track_get_type(i) == Animation::TrackType::TYPE_BEZIER) {3985is_exist = true;3986break;3987}3988}3989if (is_exist) {3990bezier_edit_icon->set_disabled(false);3991} else {3992if (bezier_edit->is_visible()) {3993_cancel_bezier_edit();3994}3995bezier_edit_icon->set_disabled(true);3996}3997}39983999Ref<Animation> AnimationTrackEditor::get_current_animation() const {4000return animation;4001}40024003void AnimationTrackEditor::_root_removed() {4004root = nullptr;4005}40064007void AnimationTrackEditor::set_root(Node *p_root) {4008if (root) {4009root->disconnect(SceneStringName(tree_exiting), callable_mp(this, &AnimationTrackEditor::_root_removed));4010}40114012root = p_root;40134014if (root) {4015root->connect(SceneStringName(tree_exiting), callable_mp(this, &AnimationTrackEditor::_root_removed), CONNECT_ONE_SHOT);4016}40174018_update_tracks();4019}40204021Node *AnimationTrackEditor::get_root() const {4022return root;4023}40244025void AnimationTrackEditor::update_keying() {4026bool keying_enabled = false;40274028EditorSelectionHistory *editor_history = EditorNode::get_singleton()->get_editor_selection_history();4029if (is_visible_in_tree() && animation.is_valid() && editor_history->get_path_size() > 0) {4030Object *obj = ObjectDB::get_instance(editor_history->get_path_object(0));4031keying_enabled = Object::cast_to<Node>(obj) != nullptr || Object::cast_to<MultiNodeEdit>(obj) != nullptr;4032}40334034if (keying_enabled == keying) {4035return;4036}40374038keying = keying_enabled;40394040emit_signal(SNAME("keying_changed"));4041}40424043bool AnimationTrackEditor::has_keying() const {4044return keying;4045}40464047Dictionary AnimationTrackEditor::get_state() const {4048Dictionary state;4049state["fps_mode"] = timeline->is_using_fps();4050state["fps_compat"] = fps_compat->is_pressed();4051state["zoom"] = zoom->get_value();4052state["offset"] = timeline->get_value();4053state["v_scroll"] = scroll->get_v_scroll_bar()->get_value();4054return state;4055}40564057void AnimationTrackEditor::set_state(const Dictionary &p_state) {4058if (p_state.has("fps_mode")) {4059bool fps_mode = p_state["fps_mode"];4060if (fps_mode) {4061snap_mode->select(1);4062} else {4063snap_mode->select(0);4064}4065_snap_mode_changed(snap_mode->get_selected());4066}40674068if (p_state.has("fps_compat")) {4069fps_compat->set_pressed(p_state["fps_compat"]);4070}40714072if (p_state.has("zoom")) {4073zoom->set_value(p_state["zoom"]);4074}40754076if (p_state.has("offset")) {4077timeline->set_value(p_state["offset"]);4078}40794080if (p_state.has("v_scroll")) {4081scroll->get_v_scroll_bar()->set_value(p_state["v_scroll"]);4082}4083}40844085void AnimationTrackEditor::clear() {4086snap_mode->select(EDITOR_GET("editors/animation/default_fps_mode"));4087_snap_mode_changed(snap_mode->get_selected());4088fps_compat->set_pressed(EDITOR_GET("editors/animation/default_fps_compatibility"));4089zoom->set_value(1.0);4090timeline->set_value(0);4091scroll->get_v_scroll_bar()->set_value(0);4092}40934094void AnimationTrackEditor::cleanup() {4095set_animation(Ref<Animation>(), read_only);4096}40974098void AnimationTrackEditor::_name_limit_changed() {4099_redraw_tracks();4100}41014102void AnimationTrackEditor::_timeline_changed(float p_new_pos, bool p_timeline_only) {4103emit_signal(SNAME("timeline_changed"), p_new_pos, p_timeline_only, false);4104}41054106void AnimationTrackEditor::_track_remove_request(int p_track) {4107_animation_track_remove_request(p_track, animation);4108}41094110void AnimationTrackEditor::_animation_track_remove_request(int p_track, Ref<Animation> p_from_animation) {4111if (p_from_animation->track_is_compressed(p_track)) {4112EditorNode::get_singleton()->show_warning(TTR("Compressed tracks can't be edited or removed. Re-import the animation with compression disabled in order to edit."));4113return;4114}4115int idx = p_track;4116if (idx >= 0 && idx < p_from_animation->get_track_count()) {4117EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();4118undo_redo->create_action(TTR("Remove Anim Track"), UndoRedo::MERGE_DISABLE, p_from_animation.ptr());41194120// Remove corresponding reset tracks if they are no longer needed.4121AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();4122if (player->has_animation(SceneStringName(RESET))) {4123Ref<Animation> reset = player->get_animation(SceneStringName(RESET));4124if (reset != p_from_animation) {4125for (int i = 0; i < reset->get_track_count(); i++) {4126if (reset->track_get_path(i) == p_from_animation->track_get_path(p_track)) {4127// Check if the reset track isn't used by other animations.4128bool used = false;4129List<StringName> animation_list;4130player->get_animation_list(&animation_list);41314132for (const StringName &anim_name : animation_list) {4133Ref<Animation> anim = player->get_animation(anim_name);4134if (anim == p_from_animation || anim == reset) {4135continue;4136}41374138for (int j = 0; j < anim->get_track_count(); j++) {4139if (anim->track_get_path(j) == reset->track_get_path(i)) {4140used = true;4141break;4142}4143}41444145if (used) {4146break;4147}4148}41494150if (!used) {4151_animation_track_remove_request(i, reset);4152}4153break;4154}4155}4156}4157}41584159undo_redo->add_do_method(this, "_clear_selection", false);4160undo_redo->add_do_method(p_from_animation.ptr(), "remove_track", idx);4161undo_redo->add_undo_method(p_from_animation.ptr(), "add_track", p_from_animation->track_get_type(idx), idx);4162undo_redo->add_undo_method(p_from_animation.ptr(), "track_set_path", idx, p_from_animation->track_get_path(idx));41634164// TODO interpolation.4165for (int i = 0; i < p_from_animation->track_get_key_count(idx); i++) {4166Variant v = p_from_animation->track_get_key_value(idx, i);4167float time = p_from_animation->track_get_key_time(idx, i);4168float trans = p_from_animation->track_get_key_transition(idx, i);41694170undo_redo->add_undo_method(p_from_animation.ptr(), "track_insert_key", idx, time, v);4171undo_redo->add_undo_method(p_from_animation.ptr(), "track_set_key_transition", idx, i, trans);4172}41734174undo_redo->add_undo_method(p_from_animation.ptr(), "track_set_interpolation_type", idx, p_from_animation->track_get_interpolation_type(idx));4175if (p_from_animation->track_get_type(idx) == Animation::TYPE_VALUE) {4176undo_redo->add_undo_method(p_from_animation.ptr(), "value_track_set_update_mode", idx, p_from_animation->value_track_get_update_mode(idx));4177}4178if (animation->track_get_type(idx) == Animation::TYPE_AUDIO) {4179undo_redo->add_undo_method(animation.ptr(), "audio_track_set_use_blend", idx, animation->audio_track_is_use_blend(idx));4180}41814182undo_redo->commit_action();4183}4184}41854186void AnimationTrackEditor::_track_grab_focus(int p_track) {4187// Don't steal focus if not working with the track editor.4188if (Object::cast_to<AnimationTrackEdit>(get_viewport()->gui_get_focus_owner())) {4189track_edits[p_track]->grab_focus();4190}4191}41924193void AnimationTrackEditor::set_anim_pos(float p_pos) {4194timeline->set_play_position(p_pos);4195marker_edit->set_play_position(p_pos);4196for (int i = 0; i < track_edits.size(); i++) {4197track_edits[i]->set_play_position(p_pos);4198}4199_redraw_groups();4200bezier_edit->set_play_position(p_pos);4201emit_signal(SNAME("timeline_changed"), p_pos, true, true);4202}42034204static bool track_type_is_resettable(Animation::TrackType p_type) {4205switch (p_type) {4206case Animation::TYPE_VALUE:4207[[fallthrough]];4208case Animation::TYPE_BLEND_SHAPE:4209[[fallthrough]];4210case Animation::TYPE_BEZIER:4211[[fallthrough]];4212case Animation::TYPE_POSITION_3D:4213[[fallthrough]];4214case Animation::TYPE_ROTATION_3D:4215[[fallthrough]];4216case Animation::TYPE_SCALE_3D:4217return true;4218default:4219return false;4220}4221}42224223void AnimationTrackEditor::make_insert_queue() {4224insert_data.clear();4225insert_queue = true;4226}42274228void AnimationTrackEditor::commit_insert_queue() {4229bool reset_allowed = true;4230AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();4231if (player->has_animation(SceneStringName(RESET)) && player->get_animation(SceneStringName(RESET)) == animation) {4232// Avoid messing with the reset animation itself.4233reset_allowed = false;4234} else {4235bool some_resettable = false;4236for (const AnimationTrackEditor::InsertData &E : insert_data) {4237if (track_type_is_resettable(E.type)) {4238some_resettable = true;4239break;4240}4241}4242if (!some_resettable) {4243reset_allowed = false;4244}4245}42464247// Organize insert data.4248int num_tracks = 0;4249String last_track_query;4250bool all_bezier = true;4251for (const AnimationTrackEditor::InsertData &E : insert_data) {4252if (E.type != Animation::TYPE_VALUE && E.type != Animation::TYPE_BEZIER) {4253all_bezier = false;4254}42554256if (E.track_idx == -1) {4257++num_tracks;4258last_track_query = E.query;4259}42604261if (E.type != Animation::TYPE_VALUE) {4262continue;4263}42644265switch (E.value.get_type()) {4266case Variant::INT:4267case Variant::FLOAT:4268case Variant::VECTOR2:4269case Variant::VECTOR3:4270case Variant::QUATERNION:4271case Variant::PLANE:4272case Variant::COLOR: {4273// Valid.4274} break;4275default: {4276all_bezier = false;4277}4278}4279}42804281// Skip the confirmation dialog if the user holds Shift while clicking the key icon.4282// If `confirm_insert_track` editor setting is disabled, the behavior is reversed.4283bool confirm_insert = EDITOR_GET("editors/animation/confirm_insert_track");4284if ((Input::get_singleton()->is_key_pressed(Key::SHIFT) != confirm_insert) && num_tracks > 0) {4285String dialog_text;42864287// Potentially a new key, does not exist.4288if (num_tracks == 1) {4289// TRANSLATORS: %s will be replaced by a phrase describing the target of track.4290dialog_text = vformat(TTR("Create new track for %s and insert key?"), last_track_query);4291} else {4292dialog_text = vformat(TTR("Create %d new tracks and insert keys?"), num_tracks);4293}42944295if (confirm_insert) {4296dialog_text += +"\n\n" + TTR("Hold Shift when clicking the key icon to skip this dialog.");4297}4298insert_confirm_text->set_text(dialog_text);42994300insert_confirm_bezier->set_visible(all_bezier);4301insert_confirm_reset->set_visible(reset_allowed);43024303insert_confirm->set_ok_button_text(TTR("Create"));4304insert_confirm->popup_centered();4305} else {4306_insert_track(reset_allowed && EDITOR_GET("editors/animation/default_create_reset_tracks"), all_bezier && EDITOR_GET("editors/animation/default_create_bezier_tracks"));4307}43084309insert_queue = false;4310}43114312void AnimationTrackEditor::_query_insert(const InsertData &p_id) {4313if (!insert_queue) {4314insert_data.clear();4315}43164317for (const InsertData &E : insert_data) {4318// Prevent insertion of multiple tracks.4319if (E.path == p_id.path && E.type == p_id.type) {4320return; // Already inserted a track this frame.4321}4322}43234324insert_data.push_back(p_id);43254326// Without queue, commit immediately.4327if (!insert_queue) {4328commit_insert_queue();4329}4330}43314332void AnimationTrackEditor::_insert_track(bool p_reset_wanted, bool p_create_beziers) {4333EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();4334undo_redo->create_action(TTR("Animation Insert Key"));43354336Ref<Animation> reset_anim;4337if (p_reset_wanted) {4338reset_anim = _create_and_get_reset_animation();4339}43404341TrackIndices next_tracks(animation.ptr(), reset_anim.ptr());4342bool advance = false;4343while (insert_data.size()) {4344if (insert_data.front()->get().advance) {4345advance = true;4346}4347next_tracks = _confirm_insert(insert_data.front()->get(), next_tracks, p_reset_wanted, reset_anim, p_create_beziers);4348insert_data.pop_front();4349}43504351undo_redo->commit_action();43524353if (advance) {4354_edit_menu_pressed(EDIT_GOTO_NEXT_STEP_TIMELINE_ONLY);4355}4356}43574358void AnimationTrackEditor::insert_transform_key(Node3D *p_node, const String &p_sub, const Animation::TrackType p_type, const Variant &p_value) {4359ERR_FAIL_NULL(root);4360ERR_FAIL_COND_MSG(4361(p_type != Animation::TYPE_POSITION_3D && p_type != Animation::TYPE_ROTATION_3D && p_type != Animation::TYPE_SCALE_3D),4362"Track type must be Position/Rotation/Scale 3D.");4363if (!keying) {4364return;4365}4366if (animation.is_null()) {4367return;4368}43694370// Let's build a node path.4371String path = String(root->get_path_to(p_node, true));4372if (!p_sub.is_empty()) {4373path += ":" + p_sub;4374}43754376NodePath np = path;43774378int track_idx = -1;43794380for (int i = 0; i < animation->get_track_count(); i++) {4381if (animation->track_get_path(i) != np) {4382continue;4383}4384if (animation->track_get_type(i) != p_type) {4385continue;4386}4387track_idx = i;4388}43894390InsertData id;4391id.path = np;4392// TRANSLATORS: This describes the target of new animation track, will be inserted into another string.4393id.query = vformat(TTR("node '%s'"), p_node->get_name());4394id.advance = false;4395id.track_idx = track_idx;4396id.value = p_value;4397id.type = p_type;4398_query_insert(id);4399}44004401bool AnimationTrackEditor::has_track(Node3D *p_node, const String &p_sub, const Animation::TrackType p_type) {4402ERR_FAIL_NULL_V(root, false);4403if (!keying) {4404return false;4405}4406if (animation.is_null()) {4407return false;4408}44094410// Let's build a node path.4411String path = String(root->get_path_to(p_node, true));4412if (!p_sub.is_empty()) {4413path += ":" + p_sub;4414}44154416int track_id = animation->find_track(path, p_type);4417if (track_id >= 0) {4418return true;4419}4420return false;4421}44224423void AnimationTrackEditor::_insert_animation_key(NodePath p_path, const Variant &p_value) {4424String path = String(p_path);44254426// Animation property is a special case, always creates an animation track.4427for (int i = 0; i < animation->get_track_count(); i++) {4428String np = String(animation->track_get_path(i));44294430if (path == np && animation->track_get_type(i) == Animation::TYPE_ANIMATION) {4431// Exists.4432InsertData id;4433id.path = path;4434id.track_idx = i;4435id.value = p_value;4436id.type = Animation::TYPE_ANIMATION;4437// TRANSLATORS: This describes the target of new animation track, will be inserted into another string.4438id.query = TTR("animation");4439id.advance = false;4440// Dialog insert.4441_query_insert(id);4442return;4443}4444}44454446InsertData id;4447id.path = path;4448id.track_idx = -1;4449id.value = p_value;4450id.type = Animation::TYPE_ANIMATION;4451id.query = TTR("animation");4452id.advance = false;4453// Dialog insert.4454_query_insert(id);4455}44564457void AnimationTrackEditor::insert_node_value_key(Node *p_node, const String &p_property, bool p_only_if_exists, bool p_advance) {4458ERR_FAIL_NULL(root);44594460// Let's build a node path.4461String path = String(root->get_path_to(p_node, true));44624463// Get the value from the subpath.4464Vector<StringName> subpath = NodePath(p_property).get_as_property_path().get_subnames();4465Variant value = p_node->get_indexed(subpath);44664467if (Object::cast_to<AnimationPlayer>(p_node) && p_property == "current_animation") {4468if (p_node == AnimationPlayerEditor::get_singleton()->get_player()) {4469EditorNode::get_singleton()->show_warning(TTR("AnimationPlayer can't animate itself, only other players."));4470return;4471}4472_insert_animation_key(path, value);4473return;4474}44754476EditorSelectionHistory *history = EditorNode::get_singleton()->get_editor_selection_history();4477for (int i = 1; i < history->get_path_size(); i++) {4478String prop = history->get_path_property(i);4479ERR_FAIL_COND(prop.is_empty());4480path += ":" + prop;4481}44824483path += ":" + p_property;44844485NodePath np = path;44864487// Locate track.44884489bool inserted = false;44904491for (int i = 0; i < animation->get_track_count(); i++) {4492if (animation->track_get_type(i) == Animation::TYPE_VALUE) {4493if (animation->track_get_path(i) != np) {4494continue;4495}44964497InsertData id;4498id.path = np;4499id.track_idx = i;4500id.value = value;4501id.type = Animation::TYPE_VALUE;4502// TRANSLATORS: This describes the target of new animation track, will be inserted into another string.4503id.query = vformat(TTR("property '%s'"), p_property);4504id.advance = p_advance;4505// Dialog insert.4506_query_insert(id);4507inserted = true;4508} else if (animation->track_get_type(i) == Animation::TYPE_BEZIER) {4509Variant actual_value;4510String track_path = String(animation->track_get_path(i));4511if (track_path == String(np)) {4512actual_value = value; // All good.4513} else {4514int sep = track_path.rfind_char(':');4515if (sep != -1) {4516String base_path = track_path.substr(0, sep);4517if (base_path == String(np)) {4518String value_name = track_path.substr(sep + 1);4519actual_value = value.get(value_name);4520} else {4521continue;4522}4523} else {4524continue;4525}4526}45274528InsertData id;4529id.path = animation->track_get_path(i);4530id.track_idx = i;4531id.value = actual_value;4532id.type = Animation::TYPE_BEZIER;4533id.query = vformat(TTR("property '%s'"), p_property);4534id.advance = p_advance;4535// Dialog insert.4536_query_insert(id);4537inserted = true;4538}4539}45404541if (inserted || p_only_if_exists) {4542return;4543}4544InsertData id;4545id.path = np;4546id.track_idx = -1;4547id.value = value;4548id.type = Animation::TYPE_VALUE;4549id.query = vformat(TTR("property '%s'"), p_property);4550id.advance = p_advance;4551// Dialog insert.4552_query_insert(id);4553}45544555PackedStringArray AnimationTrackEditor::get_selected_section() const {4556return marker_edit->get_selected_section();4557}45584559bool AnimationTrackEditor::is_marker_selected(const StringName &p_marker) const {4560return marker_edit->is_marker_selected(p_marker);4561}45624563bool AnimationTrackEditor::is_marker_moving_selection() const {4564return marker_edit->is_moving_selection();4565}45664567float AnimationTrackEditor::get_marker_moving_selection_offset() const {4568return marker_edit->get_moving_selection_offset();4569}45704571void AnimationTrackEditor::insert_value_key(const String &p_property, bool p_advance) {4572EditorSelectionHistory *history = EditorNode::get_singleton()->get_editor_selection_history();45734574ERR_FAIL_NULL(root);4575ERR_FAIL_COND(history->get_path_size() == 0);4576Object *obj = ObjectDB::get_instance(history->get_path_object(0));45774578Ref<MultiNodeEdit> multi_node_edit(obj);4579if (multi_node_edit.is_valid()) {4580Node *edited_scene = EditorNode::get_singleton()->get_edited_scene();4581ERR_FAIL_NULL(edited_scene);45824583make_insert_queue();45844585for (int i = 0; i < multi_node_edit->get_node_count(); ++i) {4586Node *node = edited_scene->get_node(multi_node_edit->get_node(i));4587insert_node_value_key(node, p_property, false, p_advance);4588}45894590commit_insert_queue();4591} else {4592Node *node = Object::cast_to<Node>(obj);4593ERR_FAIL_NULL(node);45944595make_insert_queue();4596insert_node_value_key(node, p_property, false, p_advance);4597commit_insert_queue();4598}4599}46004601Ref<Animation> AnimationTrackEditor::_create_and_get_reset_animation() {4602AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();4603if (player->has_animation(SceneStringName(RESET))) {4604return player->get_animation(SceneStringName(RESET));4605} else {4606Ref<AnimationLibrary> al;4607AnimationMixer *mixer = AnimationPlayerEditor::get_singleton()->fetch_mixer_for_library();4608if (mixer) {4609if (!mixer->has_animation_library("")) {4610al.instantiate();4611mixer->add_animation_library("", al);4612} else {4613al = mixer->get_animation_library("");4614}4615}4616Ref<Animation> reset_anim;4617reset_anim.instantiate();4618reset_anim->set_length(ANIM_MIN_LENGTH);4619EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();4620undo_redo->add_do_method(al.ptr(), "add_animation", SceneStringName(RESET), reset_anim);4621undo_redo->add_do_method(AnimationPlayerEditor::get_singleton(), "_animation_player_changed", player);4622undo_redo->add_undo_method(al.ptr(), "remove_animation", SceneStringName(RESET));4623undo_redo->add_undo_method(AnimationPlayerEditor::get_singleton(), "_animation_player_changed", player);4624return reset_anim;4625}4626}46274628void AnimationTrackEditor::_confirm_insert_list() {4629EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();4630undo_redo->create_action(TTR("Animation Insert Key"));46314632bool create_reset = insert_confirm_reset->is_visible() && insert_confirm_reset->is_pressed();4633Ref<Animation> reset_anim;4634if (create_reset) {4635reset_anim = _create_and_get_reset_animation();4636}46374638TrackIndices next_tracks(animation.ptr(), reset_anim.ptr());4639bool advance = false;4640while (insert_data.size()) {4641if (insert_data.front()->get().advance) {4642advance = true;4643}4644next_tracks = _confirm_insert(insert_data.front()->get(), next_tracks, create_reset, reset_anim, insert_confirm_bezier->is_pressed());4645insert_data.pop_front();4646}46474648undo_redo->commit_action();46494650if (advance) {4651_edit_menu_pressed(EDIT_GOTO_NEXT_STEP_TIMELINE_ONLY);4652}4653}46544655PropertyInfo AnimationTrackEditor::_find_hint_for_track(int p_idx, NodePath &r_base_path, Variant *r_current_val) {4656r_base_path = NodePath();4657ERR_FAIL_COND_V(animation.is_null(), PropertyInfo());4658ERR_FAIL_INDEX_V(p_idx, animation->get_track_count(), PropertyInfo());46594660if (!root) {4661return PropertyInfo();4662}46634664NodePath path = animation->track_get_path(p_idx);46654666if (!root->has_node_and_resource(path)) {4667return PropertyInfo();4668}46694670Ref<Resource> res;4671Vector<StringName> leftover_path;4672Node *node = root->get_node_and_resource(path, res, leftover_path, true);46734674if (node) {4675r_base_path = node->get_path();4676}46774678if (leftover_path.is_empty()) {4679if (r_current_val) {4680if (res.is_valid()) {4681*r_current_val = res;4682} else if (node) {4683*r_current_val = node;4684}4685}4686return PropertyInfo();4687}46884689Variant property_info_base;4690if (res.is_valid()) {4691property_info_base = res;4692if (r_current_val) {4693*r_current_val = res->get_indexed(leftover_path);4694}4695} else if (node) {4696property_info_base = node;4697if (r_current_val) {4698*r_current_val = node->get_indexed(leftover_path);4699}4700}47014702if (property_info_base.is_null()) {4703WARN_PRINT(vformat("Could not determine track hint for '%s:%s' because its base property is null.",4704String(path.get_concatenated_names()), String(path.get_concatenated_subnames())));4705return PropertyInfo();4706}47074708List<PropertyInfo> pinfo;4709property_info_base.get_property_list(&pinfo);47104711for (const PropertyInfo &E : pinfo) {4712if (E.name == leftover_path[leftover_path.size() - 1]) {4713return E;4714}4715}47164717return PropertyInfo();4718}47194720static Vector<String> _get_bezier_subindices_for_type(Variant::Type p_type, bool *r_valid = nullptr) {4721Vector<String> subindices;4722if (r_valid) {4723*r_valid = true;4724}4725switch (p_type) {4726case Variant::INT: {4727subindices.push_back("");4728} break;4729case Variant::FLOAT: {4730subindices.push_back("");4731} break;4732case Variant::VECTOR2: {4733subindices.push_back(":x");4734subindices.push_back(":y");4735} break;4736case Variant::VECTOR3: {4737subindices.push_back(":x");4738subindices.push_back(":y");4739subindices.push_back(":z");4740} break;4741case Variant::QUATERNION: {4742subindices.push_back(":x");4743subindices.push_back(":y");4744subindices.push_back(":z");4745subindices.push_back(":w");4746} break;4747case Variant::COLOR: {4748subindices.push_back(":r");4749subindices.push_back(":g");4750subindices.push_back(":b");4751subindices.push_back(":a");4752} break;4753case Variant::PLANE: {4754subindices.push_back(":x");4755subindices.push_back(":y");4756subindices.push_back(":z");4757subindices.push_back(":d");4758} break;4759case Variant::NIL: {4760subindices.push_back(""); // Hack: it is probably float since non-numeric types are filtered in the selection window.4761} break;4762default: {4763if (r_valid) {4764*r_valid = false;4765}4766}4767}47684769return subindices;4770}47714772AnimationTrackEditor::TrackIndices AnimationTrackEditor::_confirm_insert(InsertData p_id, TrackIndices p_next_tracks, bool p_reset_wanted, Ref<Animation> p_reset_anim, bool p_create_beziers) {4773bool created = false;47744775bool create_normal_track = p_id.track_idx < 0;4776bool create_reset_track = p_reset_wanted && track_type_is_resettable(p_id.type);47774778Animation::UpdateMode update_mode = Animation::UPDATE_DISCRETE;4779Animation::InterpolationType interp_type = Animation::INTERPOLATION_LINEAR;4780bool loop_wrap = true;4781if (create_normal_track || create_reset_track) {4782if (p_id.type == Animation::TYPE_VALUE || p_id.type == Animation::TYPE_BEZIER) {4783_fetch_value_track_options(p_id.path, &update_mode, &interp_type, &loop_wrap);4784}4785}47864787EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();4788if (create_normal_track) {4789if (p_create_beziers) {4790bool valid;4791Vector<String> subindices = _get_bezier_subindices_for_type(p_id.value.get_type(), &valid);4792if (valid) {4793for (int i = 0; i < subindices.size(); i++) {4794InsertData id = p_id;4795id.type = Animation::TYPE_BEZIER;4796id.value = subindices[i].is_empty() ? p_id.value : p_id.value.get(subindices[i].substr(1));4797id.path = String(p_id.path) + subindices[i];4798p_next_tracks = _confirm_insert(id, p_next_tracks, p_reset_wanted, p_reset_anim, false);4799}48004801return p_next_tracks;4802}4803}4804created = true;48054806p_id.track_idx = p_next_tracks.normal;48074808undo_redo->add_do_method(animation.ptr(), "add_track", p_id.type);4809undo_redo->add_do_method(animation.ptr(), "track_set_path", p_id.track_idx, p_id.path);4810undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_type", p_id.track_idx, interp_type);4811undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_loop_wrap", p_id.track_idx, loop_wrap);4812if (p_id.type == Animation::TYPE_VALUE) {4813undo_redo->add_do_method(animation.ptr(), "value_track_set_update_mode", p_id.track_idx, update_mode);4814}4815}48164817float time = p_id.time == FLT_MAX ? timeline->get_play_position() : p_id.time;4818Variant value;48194820switch (p_id.type) {4821case Animation::TYPE_POSITION_3D:4822case Animation::TYPE_ROTATION_3D:4823case Animation::TYPE_SCALE_3D:4824case Animation::TYPE_BLEND_SHAPE:4825case Animation::TYPE_VALUE:4826case Animation::TYPE_AUDIO:4827case Animation::TYPE_ANIMATION: {4828value = p_id.value;48294830} break;4831case Animation::TYPE_BEZIER: {4832int existing = -1;4833if (p_id.track_idx < animation->get_track_count()) {4834existing = animation->track_find_key(p_id.track_idx, time, Animation::FIND_MODE_APPROX);4835}48364837if (existing != -1) {4838Array arr = animation->track_get_key_value(p_id.track_idx, existing);4839arr[0] = p_id.value;4840value = arr;4841} else {4842value = animation->make_default_bezier_key(p_id.value);4843}4844bezier_edit_icon->set_disabled(false);4845} break;4846default: {4847// Other track types shouldn't use this code path.4848DEV_ASSERT(false);4849}4850}48514852undo_redo->add_do_method(animation.ptr(), "track_insert_key", p_id.track_idx, time, value);4853if (!created && p_id.type == Animation::TYPE_BEZIER) {4854undo_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);4855}48564857if (created) {4858// Just remove the track.4859undo_redo->add_undo_method(this, "_clear_selection", false);4860undo_redo->add_undo_method(animation.ptr(), "remove_track", animation->get_track_count());4861p_next_tracks.normal++;4862} else {4863undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", p_id.track_idx, time);4864int existing = animation->track_find_key(p_id.track_idx, time, Animation::FIND_MODE_APPROX);4865if (existing != -1) {4866Variant v = animation->track_get_key_value(p_id.track_idx, existing);4867float trans = animation->track_get_key_transition(p_id.track_idx, existing);4868undo_redo->add_undo_method(animation.ptr(), "track_insert_key", p_id.track_idx, time, v, trans);4869}4870}48714872if (create_reset_track) {4873Animation *reset_anim = p_reset_anim.ptr();4874for (int i = 0; i < reset_anim->get_track_count(); i++) {4875if (reset_anim->track_get_path(i) == p_id.path) {4876create_reset_track = false;4877break;4878}4879}4880if (create_reset_track) {4881undo_redo->add_do_method(reset_anim, "add_track", p_id.type);4882undo_redo->add_do_method(reset_anim, "track_set_path", p_next_tracks.reset, p_id.path);4883if (p_id.type == Animation::TYPE_VALUE) {4884undo_redo->add_do_method(reset_anim, "value_track_set_update_mode", p_next_tracks.reset, update_mode);4885}4886undo_redo->add_do_method(reset_anim, "track_set_interpolation_type", p_next_tracks.reset, interp_type);4887undo_redo->add_do_method(reset_anim, "track_insert_key", p_next_tracks.reset, 0.0f, value);4888undo_redo->add_undo_method(reset_anim, "remove_track", reset_anim->get_track_count());4889p_next_tracks.reset++;4890}4891}48924893return p_next_tracks;4894}48954896void AnimationTrackEditor::show_select_node_warning(bool p_show) {4897info_message->set_visible(p_show);4898}48994900void AnimationTrackEditor::show_dummy_player_warning(bool p_show) {4901dummy_player_warning->set_visible(p_show);4902}49034904void AnimationTrackEditor::show_inactive_player_warning(bool p_show) {4905inactive_player_warning->set_visible(p_show);4906}49074908bool AnimationTrackEditor::is_key_selected(int p_track, int p_key) const {4909SelectedKey sk;4910sk.key = p_key;4911sk.track = p_track;49124913return selection.has(sk);4914}49154916bool AnimationTrackEditor::is_selection_active() const {4917return selection.size();4918}49194920bool AnimationTrackEditor::is_key_clipboard_active() const {4921return key_clipboard.keys.size();4922}49234924bool AnimationTrackEditor::is_snap_timeline_enabled() const {4925return snap_timeline->is_pressed() ^ Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL);4926}49274928bool AnimationTrackEditor::is_snap_keys_enabled() const {4929return snap_keys->is_pressed() ^ Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL);4930}49314932bool AnimationTrackEditor::is_bezier_editor_active() const {4933return bezier_edit->is_visible();4934}49354936bool AnimationTrackEditor::can_add_reset_key() const {4937for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {4938const Animation::TrackType track_type = animation->track_get_type(E.key.track);4939if (track_type != Animation::TYPE_ANIMATION && track_type != Animation::TYPE_AUDIO && track_type != Animation::TYPE_METHOD) {4940return true;4941}4942}4943return false;4944}49454946void AnimationTrackEditor::_on_filter_updated(const String &p_filter) {4947emit_signal(SNAME("filter_changed"));4948}49494950void AnimationTrackEditor::_update_tracks() {4951int selected = _get_track_selected();49524953while (track_vbox->get_child_count()) {4954memdelete(track_vbox->get_child(0));4955}49564957timeline->set_track_edit(nullptr);49584959track_edits.clear();4960groups.clear();49614962if (animation.is_null()) {4963return;4964}49654966bool file_read_only = false;4967if (!animation->get_path().is_resource_file()) {4968int srpos = animation->get_path().find("::");4969if (srpos != -1) {4970String base = animation->get_path().substr(0, srpos);4971if (ResourceLoader::get_resource_type(base) == "PackedScene") {4972if (!get_tree()->get_edited_scene_root() || get_tree()->get_edited_scene_root()->get_scene_file_path() != base) {4973file_read_only = true;4974}4975} else {4976if (FileAccess::exists(base + ".import")) {4977file_read_only = true;4978}4979}4980}4981} else {4982if (FileAccess::exists(animation->get_path() + ".import")) {4983file_read_only = true;4984}4985}49864987RBMap<String, VBoxContainer *> group_sort;4988LocalVector<VBoxContainer *> group_containers;49894990bool use_grouping = !view_group->is_pressed();4991bool use_filter = selected_filter->is_pressed();4992bool use_alphabetic_sorting = alphabetic_sorting->is_pressed();49934994AnimationTrackEdit *selected_track_edit = nullptr;49954996for (int i = 0; i < animation->get_track_count(); i++) {4997AnimationTrackEdit *track_edit = nullptr;49984999// Find hint and info for plugin.50005001if (use_filter) {5002NodePath path = animation->track_get_path(i);50035004if (root) {5005Node *node = root->get_node_or_null(path);5006if (!node) {5007continue; // No node, no filter.5008}5009if (!EditorNode::get_singleton()->get_editor_selection()->is_selected(node)) {5010continue; // Skip track due to not selected.5011}5012}5013}50145015String filter_text = timeline->filter_track->get_text();50165017if (!filter_text.is_empty()) {5018String target = String(animation->track_get_path(i));5019if (!target.containsn(filter_text)) {5020continue;5021}5022}50235024if (animation->track_get_type(i) == Animation::TYPE_VALUE) {5025NodePath path = animation->track_get_path(i);50265027if (root && root->has_node_and_resource(path)) {5028Ref<Resource> res;5029NodePath base_path;5030Vector<StringName> leftover_path;5031Node *node = root->get_node_and_resource(path, res, leftover_path, true);5032PropertyInfo pinfo = _find_hint_for_track(i, base_path);50335034Object *object = node;5035if (res.is_valid()) {5036object = res.ptr();5037}50385039if (object && !leftover_path.is_empty()) {5040if (pinfo.name.is_empty()) {5041pinfo.name = leftover_path[leftover_path.size() - 1];5042}50435044for (int j = 0; j < track_edit_plugins.size(); j++) {5045track_edit = track_edit_plugins.write[j]->create_value_track_edit(object, pinfo.type, pinfo.name, pinfo.hint, pinfo.hint_string, pinfo.usage);5046if (track_edit) {5047break;5048}5049}5050}5051}5052}5053if (animation->track_get_type(i) == Animation::TYPE_AUDIO) {5054for (int j = 0; j < track_edit_plugins.size(); j++) {5055track_edit = track_edit_plugins.write[j]->create_audio_track_edit();5056if (track_edit) {5057break;5058}5059}5060}50615062if (animation->track_get_type(i) == Animation::TYPE_ANIMATION) {5063NodePath path = animation->track_get_path(i);50645065Node *node = nullptr;5066if (root) {5067node = root->get_node_or_null(path);5068}50695070if (node && Object::cast_to<AnimationPlayer>(node)) {5071for (int j = 0; j < track_edit_plugins.size(); j++) {5072track_edit = track_edit_plugins.write[j]->create_animation_track_edit(node);5073if (track_edit) {5074break;5075}5076}5077}5078}50795080if (track_edit == nullptr) {5081// No valid plugin_found.5082track_edit = memnew(AnimationTrackEdit);5083}50845085track_edits.push_back(track_edit);50865087if (use_grouping) {5088String base_path = String(animation->track_get_path(i));5089base_path = base_path.get_slicec(':', 0); // Remove sub-path.50905091if (!group_sort.has(base_path)) {5092AnimationTrackEditGroup *g = memnew(AnimationTrackEditGroup);5093Ref<Texture2D> icon = get_editor_theme_icon(SNAME("Node"));5094String name = base_path;5095String tooltip;5096if (root) {5097Node *n = root->get_node_or_null(base_path);5098if (n) {5099icon = EditorNode::get_singleton()->get_object_icon(n, "Node");5100name = n->get_name();5101tooltip = String(root->get_path_to(n));5102}5103}51045105g->set_type_and_name(icon, name, animation->track_get_path(i));5106g->set_root(root);5107g->set_tooltip_text(tooltip);5108g->set_timeline(timeline);5109g->set_editor(this);5110groups.push_back(g);5111VBoxContainer *vb = memnew(VBoxContainer);5112vb->add_theme_constant_override("separation", 0);5113vb->add_child(g);5114group_sort[base_path] = vb;5115group_containers.push_back(vb);5116}51175118track_edit->set_in_group(true);5119group_sort[base_path]->add_child(track_edit);51205121} else {5122track_edit->set_in_group(false);5123}51245125track_edit->set_timeline(timeline);5126track_edit->set_root(root);5127track_edit->set_animation_and_track(animation, i, file_read_only);5128track_edit->set_play_position(timeline->get_play_position());5129track_edit->set_editor(this);51305131if (selected == i) {5132selected_track_edit = track_edit;5133}51345135track_edit->connect("timeline_changed", callable_mp(this, &AnimationTrackEditor::_timeline_changed));5136track_edit->connect("remove_request", callable_mp(this, &AnimationTrackEditor::_track_remove_request), CONNECT_DEFERRED);5137track_edit->connect("dropped", callable_mp(this, &AnimationTrackEditor::_dropped_track), CONNECT_DEFERRED);5138track_edit->connect("insert_key", callable_mp(this, &AnimationTrackEditor::_insert_key_from_track).bind(i), CONNECT_DEFERRED);5139track_edit->connect("select_key", callable_mp(this, &AnimationTrackEditor::_key_selected).bind(i), CONNECT_DEFERRED);5140track_edit->connect("deselect_key", callable_mp(this, &AnimationTrackEditor::_key_deselected).bind(i), CONNECT_DEFERRED);5141track_edit->connect("move_selection_begin", callable_mp(this, &AnimationTrackEditor::_move_selection_begin));5142track_edit->connect("move_selection", callable_mp(this, &AnimationTrackEditor::_move_selection));5143track_edit->connect("move_selection_commit", callable_mp(this, &AnimationTrackEditor::_move_selection_commit));5144track_edit->connect("move_selection_cancel", callable_mp(this, &AnimationTrackEditor::_move_selection_cancel));51455146track_edit->connect("duplicate_request", callable_mp(this, &AnimationTrackEditor::_anim_duplicate_keys).bind(i), CONNECT_DEFERRED);5147track_edit->connect("cut_request", callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_CUT_KEYS), CONNECT_DEFERRED);5148track_edit->connect("copy_request", callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_COPY_KEYS), CONNECT_DEFERRED);5149track_edit->connect("paste_request", callable_mp(this, &AnimationTrackEditor::_anim_paste_keys).bind(i), CONNECT_DEFERRED);5150track_edit->connect("create_reset_request", callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_ADD_RESET_KEY), CONNECT_DEFERRED);5151track_edit->connect("delete_request", callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_DELETE_SELECTION), CONNECT_DEFERRED);5152}51535154if (use_grouping) {5155if (use_alphabetic_sorting) {5156struct GroupAlphaCompare {5157bool operator()(const VBoxContainer *p_lhs, const VBoxContainer *p_rhs) const {5158String lhs_node_name = Object::cast_to<AnimationTrackEditGroup>(p_lhs->get_child(0))->get_node_name();5159String rhs_node_name = Object::cast_to<AnimationTrackEditGroup>(p_rhs->get_child(0))->get_node_name();5160return lhs_node_name < rhs_node_name;5161}5162};51635164group_containers.sort_custom<GroupAlphaCompare>();5165}51665167for (VBoxContainer *vb : group_containers) {5168track_vbox->add_child(vb);5169}51705171} else {5172if (use_alphabetic_sorting) {5173struct TrackAlphaCompare {5174bool operator()(const AnimationTrackEdit *p_lhs, const AnimationTrackEdit *p_rhs) const {5175String lhs_leaf = (String)p_lhs->get_path().slice(-p_lhs->get_path().get_subname_count() - 1);5176String rhs_leaf = (String)p_rhs->get_path().slice(-p_rhs->get_path().get_subname_count() - 1);5177return lhs_leaf < rhs_leaf;5178}5179};51805181track_edits.sort_custom<TrackAlphaCompare>();5182}51835184for (AnimationTrackEdit *track_edit : track_edits) {5185track_vbox->add_child(track_edit);5186}5187}51885189if (selected_track_edit != nullptr) {5190selected_track_edit->grab_focus();5191}5192}51935194void AnimationTrackEditor::_redraw_tracks() {5195for (int i = 0; i < track_edits.size(); i++) {5196track_edits[i]->queue_redraw();5197}5198}51995200void AnimationTrackEditor::_redraw_groups() {5201for (int i = 0; i < groups.size(); i++) {5202groups[i]->queue_redraw();5203}5204}52055206void AnimationTrackEditor::_animation_changed() {5207if (animation_changing_awaiting_update) {5208return; // All will be updated, don't bother with anything.5209}52105211_check_bezier_exist();52125213if (key_edit) {5214if (key_edit->setting) {5215// If editing a key, just redraw the edited track, makes refresh less costly.5216if (key_edit->track < track_edits.size()) {5217if (animation->track_get_type(key_edit->track) == Animation::TYPE_BEZIER) {5218bezier_edit->queue_redraw();5219} else {5220track_edits[key_edit->track]->queue_redraw();5221}5222}5223return;5224} else {5225_update_key_edit();5226}5227}52285229animation_changing_awaiting_update = true;5230callable_mp(this, &AnimationTrackEditor::_animation_update).call_deferred();5231}52325233void AnimationTrackEditor::_snap_mode_changed(int p_mode) {5234bool use_fps = p_mode == 1;5235timeline->set_use_fps(use_fps);5236if (key_edit) {5237key_edit->set_use_fps(use_fps);5238}5239marker_edit->set_use_fps(use_fps);5240// 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.5241step->set_step(use_fps ? FPS_DECIMAL : SECOND_DECIMAL);5242if (use_fps) {5243fps_compat->hide();5244} else {5245fps_compat->show();5246}5247_update_step_spinbox();5248}52495250void AnimationTrackEditor::_update_step_spinbox() {5251if (animation.is_null()) {5252return;5253}5254step->set_block_signals(true);52555256if (timeline->is_using_fps()) {5257if (animation->get_step() == 0.0) {5258step->set_value(0.0);5259} else {5260step->set_value(1.0 / animation->get_step());5261}5262} else {5263step->set_value(animation->get_step());5264}52655266step->set_block_signals(false);5267_update_snap_unit();5268}52695270void AnimationTrackEditor::_update_fps_compat_mode(bool p_enabled) {5271_update_snap_unit();5272}52735274void AnimationTrackEditor::_update_nearest_fps_label() {5275bool is_fps_invalid = nearest_fps == 0;5276if (is_fps_invalid) {5277nearest_fps_label->hide();5278} else {5279nearest_fps_label->show();5280nearest_fps_label->set_text(vformat(TTR("Nearest FPS: %d"), nearest_fps));5281}5282}52835284void AnimationTrackEditor::_animation_update() {5285timeline->queue_redraw();5286timeline->update_values();52875288bool same = true;52895290if (animation.is_null()) {5291return;5292}52935294if (track_edits.size() == animation->get_track_count()) {5295// Check tracks are the same.52965297for (int i = 0; i < track_edits.size(); i++) {5298if (track_edits[i]->get_path() != animation->track_get_path(i)) {5299same = false;5300break;5301}5302}5303} else {5304same = false;5305}53065307if (same) {5308_redraw_tracks();5309_redraw_groups();5310} else {5311_update_tracks();5312}53135314bezier_edit->queue_redraw();53155316_update_step_spinbox();5317emit_signal(SNAME("animation_step_changed"), animation->get_step());5318emit_signal(SNAME("animation_len_changed"), animation->get_length());53195320animation_changing_awaiting_update = false;5321}53225323MenuButton *AnimationTrackEditor::get_edit_menu() {5324return edit;5325}53265327void AnimationTrackEditor::_notification(int p_what) {5328switch (p_what) {5329case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {5330if (!EditorSettings::get_singleton()->check_changed_settings_in_group("editors/panning")) {5331break;5332}5333[[fallthrough]];5334}5335case NOTIFICATION_ENTER_TREE: {5336panner->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")));5337panner->setup_warped_panning(get_viewport(), EDITOR_GET("editors/panning/warped_mouse_panning"));5338} break;5339case NOTIFICATION_THEME_CHANGED: {5340zoom_icon->set_texture(get_editor_theme_icon(SNAME("Zoom")));5341bezier_edit_icon->set_button_icon(get_editor_theme_icon(SNAME("EditBezier")));5342snap_timeline->set_button_icon(get_editor_theme_icon(SNAME("SnapTimeline")));5343snap_keys->set_button_icon(get_editor_theme_icon(SNAME("SnapKeys")));5344fps_compat->set_button_icon(get_editor_theme_icon(SNAME("FPS")));5345view_group->set_button_icon(get_editor_theme_icon(view_group->is_pressed() ? SNAME("AnimationTrackList") : SNAME("AnimationTrackGroup")));5346function_name_toggler->set_button_icon(get_editor_theme_icon(SNAME("MemberMethod")));5347selected_filter->set_button_icon(get_editor_theme_icon(SNAME("AnimationFilter")));5348alphabetic_sorting->set_button_icon(get_editor_theme_icon(SNAME("Sort")));5349imported_anim_warning->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning")));5350dummy_player_warning->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning")));5351inactive_player_warning->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning")));5352main_panel->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SceneStringName(panel), SNAME("Tree")));5353edit->get_popup()->set_item_icon(edit->get_popup()->get_item_index(EDIT_APPLY_RESET), get_editor_theme_icon(SNAME("Reload")));5354auto_fit->set_button_icon(get_editor_theme_icon(SNAME("AnimationAutoFit")));5355auto_fit_bezier->set_button_icon(get_editor_theme_icon(SNAME("AnimationAutoFitBezier")));53565357const int timeline_separation = get_theme_constant(SNAME("timeline_v_separation"), SNAME("AnimationTrackEditor"));5358timeline_vbox->add_theme_constant_override("separation", timeline_separation);53595360const int track_separation = get_theme_constant(SNAME("track_v_separation"), SNAME("AnimationTrackEditor"));5361track_vbox->add_theme_constant_override("separation", track_separation);53625363function_name_toggler->add_theme_color_override("icon_pressed_color", get_theme_color("icon_disabled_color", EditorStringName(Editor)));5364} break;53655366case NOTIFICATION_READY: {5367EditorNode::get_singleton()->get_editor_selection()->connect("selection_changed", callable_mp(this, &AnimationTrackEditor::_selection_changed));5368} break;53695370case NOTIFICATION_VISIBILITY_CHANGED: {5371update_keying();5372} break;5373}5374}53755376void AnimationTrackEditor::_update_scroll(double) {5377_redraw_tracks();5378_redraw_groups();5379marker_edit->queue_redraw();5380}53815382void AnimationTrackEditor::_update_step(double p_new_step) {5383if (animation.is_null()) {5384return;5385}53865387_update_snap_unit();53885389EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();5390undo_redo->create_action(TTR("Change Animation Step"));5391double step_value = p_new_step;5392if (timeline->is_using_fps()) {5393if (step_value != 0.0) {5394// 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.5395step_value = 1.0 / MIN(1000.0, p_new_step);5396}5397timeline->queue_redraw();5398}5399undo_redo->add_do_method(animation.ptr(), "set_step", step_value);5400undo_redo->add_undo_method(animation.ptr(), "set_step", animation->get_step());5401step->set_block_signals(true);5402undo_redo->commit_action();5403step->set_block_signals(false);5404emit_signal(SNAME("animation_step_changed"), step_value);5405}54065407void AnimationTrackEditor::_update_length(double p_new_len) {5408emit_signal(SNAME("animation_len_changed"), p_new_len);5409}54105411void AnimationTrackEditor::_dropped_track(int p_from_track, int p_to_track) {5412if (p_from_track == p_to_track || p_from_track == p_to_track - 1) {5413return;5414}54155416_clear_selection(true);5417EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();5418undo_redo->create_action(TTR("Rearrange Tracks"));5419undo_redo->add_do_method(animation.ptr(), "track_move_to", p_from_track, p_to_track);5420// Take into account that the position of the tracks that come after the one removed will change.5421int to_track_real = p_to_track > p_from_track ? p_to_track - 1 : p_to_track;5422undo_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);5423undo_redo->add_do_method(this, "_track_grab_focus", to_track_real);5424undo_redo->add_undo_method(this, "_track_grab_focus", p_from_track);5425undo_redo->commit_action();5426}54275428void AnimationTrackEditor::_new_track_node_selected(NodePath p_path) {5429ERR_FAIL_NULL(root);5430Node *node = get_node_or_null(p_path);5431ERR_FAIL_NULL(node);5432NodePath path_to = root->get_path_to(node, true);54335434if (adding_track_type == Animation::TYPE_BLEND_SHAPE && !node->is_class("MeshInstance3D")) {5435EditorNode::get_singleton()->show_warning(TTR("Blend Shape tracks only apply to MeshInstance3D nodes."));5436return;5437}54385439if ((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")) {5440EditorNode::get_singleton()->show_warning(TTR("Position/Rotation/Scale 3D tracks only apply to 3D-based nodes."));5441return;5442}54435444switch (adding_track_type) {5445case Animation::TYPE_VALUE: {5446adding_track_path = path_to;5447prop_selector->set_type_filter(Vector<Variant::Type>());5448prop_selector->select_property_from_instance(node);5449} break;5450case Animation::TYPE_BLEND_SHAPE: {5451adding_track_path = path_to;5452Vector<Variant::Type> filter;5453filter.push_back(Variant::FLOAT);5454prop_selector->set_type_filter(filter);5455prop_selector->select_property_from_instance(node);5456} break;5457case Animation::TYPE_POSITION_3D:5458case Animation::TYPE_ROTATION_3D:5459case Animation::TYPE_SCALE_3D:5460case Animation::TYPE_METHOD: {5461EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();5462undo_redo->create_action(TTR("Add Track"));5463undo_redo->add_do_method(animation.ptr(), "add_track", adding_track_type);5464undo_redo->add_do_method(animation.ptr(), "track_set_path", animation->get_track_count(), path_to);5465undo_redo->add_undo_method(animation.ptr(), "remove_track", animation->get_track_count());5466undo_redo->commit_action();54675468} break;5469case Animation::TYPE_BEZIER: {5470Vector<Variant::Type> filter;5471filter.push_back(Variant::INT);5472filter.push_back(Variant::FLOAT);5473filter.push_back(Variant::VECTOR2);5474filter.push_back(Variant::VECTOR3);5475filter.push_back(Variant::QUATERNION);5476filter.push_back(Variant::PLANE);5477filter.push_back(Variant::COLOR);54785479adding_track_path = path_to;5480prop_selector->set_type_filter(filter);5481prop_selector->select_property_from_instance(node);5482} break;5483case Animation::TYPE_AUDIO: {5484if (!node->is_class("AudioStreamPlayer") && !node->is_class("AudioStreamPlayer2D") && !node->is_class("AudioStreamPlayer3D")) {5485EditorNode::get_singleton()->show_warning(TTR("Audio tracks can only point to nodes of type:\n-AudioStreamPlayer\n-AudioStreamPlayer2D\n-AudioStreamPlayer3D"));5486return;5487}54885489EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();5490undo_redo->create_action(TTR("Add Track"));5491undo_redo->add_do_method(animation.ptr(), "add_track", adding_track_type);5492undo_redo->add_do_method(animation.ptr(), "track_set_path", animation->get_track_count(), path_to);5493undo_redo->add_undo_method(animation.ptr(), "remove_track", animation->get_track_count());5494undo_redo->commit_action();54955496} break;5497case Animation::TYPE_ANIMATION: {5498if (!node->is_class("AnimationPlayer")) {5499EditorNode::get_singleton()->show_warning(TTR("Animation tracks can only point to AnimationPlayer nodes."));5500return;5501}55025503if (node == AnimationPlayerEditor::get_singleton()->get_player()) {5504EditorNode::get_singleton()->show_warning(TTR("AnimationPlayer can't animate itself, only other players."));5505return;5506}55075508EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();5509undo_redo->create_action(TTR("Add Track"));5510undo_redo->add_do_method(animation.ptr(), "add_track", adding_track_type);5511undo_redo->add_do_method(animation.ptr(), "track_set_path", animation->get_track_count(), path_to);5512undo_redo->add_undo_method(animation.ptr(), "remove_track", animation->get_track_count());5513undo_redo->commit_action();55145515} break;5516}5517}55185519void AnimationTrackEditor::_add_track(int p_type) {5520AnimationPlayer *ap = AnimationPlayerEditor::get_singleton()->get_player();5521if (!ap) {5522ERR_FAIL_EDMSG("No AnimationPlayer is currently being edited.");5523}5524Node *root_node = ap->get_node_or_null(ap->get_root_node());5525if (!root_node) {5526EditorNode::get_singleton()->show_warning(TTR("Not possible to add a new track without a root"));5527return;5528}5529adding_track_type = p_type;5530Vector<StringName> valid_types;5531switch (adding_track_type) {5532case Animation::TYPE_BLEND_SHAPE: {5533// Blend Shape is a property of MeshInstance3D.5534valid_types.push_back(SNAME("MeshInstance3D"));5535} break;5536case Animation::TYPE_POSITION_3D:5537case Animation::TYPE_ROTATION_3D:5538case Animation::TYPE_SCALE_3D: {5539// 3D Properties come from nodes inheriting Node3D.5540valid_types.push_back(SNAME("Node3D"));5541} break;5542case Animation::TYPE_AUDIO: {5543valid_types.push_back(SNAME("AudioStreamPlayer"));5544valid_types.push_back(SNAME("AudioStreamPlayer2D"));5545valid_types.push_back(SNAME("AudioStreamPlayer3D"));5546} break;5547case Animation::TYPE_ANIMATION: {5548valid_types.push_back(SNAME("AnimationPlayer"));5549} break;5550}5551pick_track->set_valid_types(valid_types);5552pick_track->popup_scenetree_dialog(nullptr, root_node);5553pick_track->get_filter_line_edit()->clear();5554pick_track->get_filter_line_edit()->grab_focus();5555}55565557void AnimationTrackEditor::_fetch_value_track_options(const NodePath &p_path, Animation::UpdateMode *r_update_mode, Animation::InterpolationType *r_interpolation_type, bool *r_loop_wrap) {5558AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();5559if (player->has_animation(SceneStringName(RESET))) {5560Ref<Animation> reset_anim = player->get_animation(SceneStringName(RESET));5561int rt = reset_anim->find_track(p_path, Animation::TrackType::TYPE_VALUE);5562if (rt >= 0) {5563*r_update_mode = reset_anim->value_track_get_update_mode(rt);5564*r_interpolation_type = reset_anim->track_get_interpolation_type(rt);5565*r_loop_wrap = reset_anim->track_get_interpolation_loop_wrap(rt);5566return;5567}5568rt = reset_anim->find_track(p_path, Animation::TrackType::TYPE_BEZIER);5569if (rt >= 0) {5570*r_interpolation_type = reset_anim->track_get_interpolation_type(rt);5571*r_loop_wrap = reset_anim->track_get_interpolation_loop_wrap(rt);5572return;5573}5574}55755576// Hack.5577NodePath np;5578animation->add_track(Animation::TYPE_VALUE);5579animation->track_set_path(animation->get_track_count() - 1, p_path);5580PropertyInfo h = _find_hint_for_track(animation->get_track_count() - 1, np);5581animation->remove_track(animation->get_track_count() - 1); // Hack.5582switch (h.type) {5583case Variant::FLOAT:5584case Variant::VECTOR2:5585case Variant::RECT2:5586case Variant::VECTOR3:5587case Variant::TRANSFORM2D:5588case Variant::VECTOR4:5589case Variant::PLANE:5590case Variant::QUATERNION:5591case Variant::AABB:5592case Variant::BASIS:5593case Variant::TRANSFORM3D:5594case Variant::PROJECTION:5595case Variant::COLOR:5596case Variant::PACKED_FLOAT32_ARRAY:5597case Variant::PACKED_FLOAT64_ARRAY:5598case Variant::PACKED_VECTOR2_ARRAY:5599case Variant::PACKED_VECTOR3_ARRAY:5600case Variant::PACKED_COLOR_ARRAY:5601case Variant::PACKED_VECTOR4_ARRAY: {5602*r_update_mode = Animation::UPDATE_CONTINUOUS;5603} break;5604default: {5605}5606}5607}56085609void AnimationTrackEditor::_new_track_property_selected(const String &p_name) {5610String full_path = String(adding_track_path) + ":" + p_name;56115612EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();56135614Animation::UpdateMode update_mode = Animation::UPDATE_DISCRETE;5615Animation::InterpolationType interp_type = Animation::INTERPOLATION_LINEAR;5616bool loop_wrap = true;5617_fetch_value_track_options(full_path, &update_mode, &interp_type, &loop_wrap);5618if (adding_track_type == Animation::TYPE_BEZIER) {5619Vector<String> subindices;5620{5621// Hack.5622NodePath np;5623animation->add_track(Animation::TYPE_VALUE);5624animation->track_set_path(animation->get_track_count() - 1, full_path);5625PropertyInfo h = _find_hint_for_track(animation->get_track_count() - 1, np);5626animation->remove_track(animation->get_track_count() - 1); // Hack.5627bool valid;5628subindices = _get_bezier_subindices_for_type(h.type, &valid);5629if (!valid) {5630EditorNode::get_singleton()->show_warning(TTR("Invalid track for Bezier (no suitable sub-properties)"));5631return;5632}5633}56345635undo_redo->create_action(TTR("Add Bezier Track"));5636int base_track = animation->get_track_count();5637for (int i = 0; i < subindices.size(); i++) {5638int track_idx = base_track + i;5639undo_redo->add_do_method(animation.ptr(), "add_track", adding_track_type);5640undo_redo->add_do_method(animation.ptr(), "track_set_path", track_idx, full_path + subindices[i]);5641undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_type", track_idx, interp_type);5642undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_loop_wrap", track_idx, loop_wrap);5643undo_redo->add_undo_method(animation.ptr(), "remove_track", base_track);5644}5645undo_redo->commit_action();5646} else {5647bool is_blend_shape = adding_track_type == Animation::TYPE_BLEND_SHAPE;5648if (is_blend_shape) {5649PackedStringArray split = p_name.split("/");5650if (!split.is_empty()) {5651full_path = String(adding_track_path) + ":" + split[split.size() - 1];5652}5653}5654undo_redo->create_action(TTR("Add Track"));5655undo_redo->add_do_method(animation.ptr(), "add_track", adding_track_type);5656undo_redo->add_do_method(animation.ptr(), "track_set_path", animation->get_track_count(), full_path);5657undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_type", animation->get_track_count(), interp_type);5658undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_loop_wrap", animation->get_track_count(), loop_wrap);5659if (!is_blend_shape) {5660undo_redo->add_do_method(animation.ptr(), "value_track_set_update_mode", animation->get_track_count(), update_mode);5661}5662undo_redo->add_undo_method(animation.ptr(), "remove_track", animation->get_track_count());5663undo_redo->commit_action();5664}5665}56665667void AnimationTrackEditor::_timeline_value_changed(double) {5668timeline->update_play_position();56695670_redraw_tracks();5671for (int i = 0; i < track_edits.size(); i++) {5672track_edits[i]->update_play_position();5673}5674_redraw_groups();56755676bezier_edit->queue_redraw();5677bezier_edit->update_play_position();56785679marker_edit->update_play_position();5680}56815682int AnimationTrackEditor::_get_track_selected() {5683for (int i = 0; i < track_edits.size(); i++) {5684if (track_edits[i]->has_focus()) {5685return i;5686}5687}56885689return -1;5690}56915692void AnimationTrackEditor::_insert_key_from_track(float p_ofs, int p_track) {5693ERR_FAIL_INDEX(p_track, animation->get_track_count());56945695if (snap_keys->is_pressed() && step->get_value() != 0) {5696p_ofs = snap_time(p_ofs);5697}5698while (animation->track_find_key(p_track, p_ofs, Animation::FIND_MODE_APPROX) != -1) { // Make sure insertion point is valid.5699p_ofs += SECOND_DECIMAL;5700}57015702Node *node = root->get_node_or_null(animation->track_get_path(p_track));5703if (!node) {5704EditorNode::get_singleton()->show_warning(TTR("Track path is invalid, so can't add a key."));5705return;5706}57075708// Special handling for this one.5709if (animation->track_get_type(p_track) == Animation::TYPE_METHOD) {5710method_selector->select_method_from_instance(node);57115712insert_key_from_track_call_ofs = p_ofs;5713insert_key_from_track_call_track = p_track;5714return;5715}57165717InsertData id;5718id.path = animation->track_get_path(p_track);5719id.advance = false;5720id.track_idx = p_track;5721id.type = animation->track_get_type(p_track);5722// TRANSLATORS: This describes the target of new animation track, will be inserted into another string.5723id.query = vformat(TTR("node '%s'"), node->get_name());5724id.time = p_ofs;5725// id.value is filled in each case handled below.57265727switch (animation->track_get_type(p_track)) {5728case Animation::TYPE_POSITION_3D: {5729Node3D *base = Object::cast_to<Node3D>(node);57305731if (!base) {5732EditorNode::get_singleton()->show_warning(TTR("Track is not of type Node3D, can't insert key"));5733return;5734}57355736id.value = base->get_position();5737} break;5738case Animation::TYPE_ROTATION_3D: {5739Node3D *base = Object::cast_to<Node3D>(node);57405741if (!base) {5742EditorNode::get_singleton()->show_warning(TTR("Track is not of type Node3D, can't insert key"));5743return;5744}57455746id.value = base->get_transform().basis.operator Quaternion();5747} break;5748case Animation::TYPE_SCALE_3D: {5749Node3D *base = Object::cast_to<Node3D>(node);57505751if (!base) {5752EditorNode::get_singleton()->show_warning(TTR("Track is not of type Node3D, can't insert key"));5753return;5754}57555756id.value = base->get_scale();5757} break;5758case Animation::TYPE_BLEND_SHAPE: {5759MeshInstance3D *base = Object::cast_to<MeshInstance3D>(node);57605761if (!base) {5762EditorNode::get_singleton()->show_warning(TTR("Track is not of type MeshInstance3D, can't insert key"));5763return;5764}57655766id.value = base->get_blend_shape_value(base->find_blend_shape_by_name(id.path.get_subname(0)));5767} break;5768case Animation::TYPE_VALUE: {5769NodePath bp;5770_find_hint_for_track(p_track, bp, &id.value);5771} break;5772case Animation::TYPE_METHOD: {5773Node *base = root->get_node_or_null(animation->track_get_path(p_track));5774ERR_FAIL_NULL(base);57755776method_selector->select_method_from_instance(base);57775778insert_key_from_track_call_ofs = p_ofs;5779insert_key_from_track_call_track = p_track;57805781} break;5782case Animation::TYPE_BEZIER: {5783NodePath bp;5784Variant value;5785_find_hint_for_track(p_track, bp, &value);5786id.value = animation->make_default_bezier_key(value);5787} break;5788case Animation::TYPE_AUDIO: {5789Dictionary ak;5790ak["stream"] = Ref<Resource>();5791ak["start_offset"] = 0;5792ak["end_offset"] = 0;57935794id.value = ak;5795} break;5796case Animation::TYPE_ANIMATION: {5797id.value = StringName("[stop]");5798} break;5799default: {5800// All track types should be handled by now.5801DEV_ASSERT(false);5802}5803}58045805_query_insert(id);5806}58075808void AnimationTrackEditor::_add_method_key(const String &p_method) {5809if (!root->has_node(animation->track_get_path(insert_key_from_track_call_track))) {5810EditorNode::get_singleton()->show_warning(TTR("Track path is invalid, so can't add a method key."));5811return;5812}5813Node *base = root->get_node_or_null(animation->track_get_path(insert_key_from_track_call_track));5814ERR_FAIL_NULL(base);58155816List<MethodInfo> minfo;5817base->get_method_list(&minfo);58185819for (const MethodInfo &E : minfo) {5820if (E.name == p_method) {5821Dictionary d;5822d["method"] = p_method;5823Array params;5824int64_t first_defarg = E.arguments.size() - E.default_arguments.size();58255826for (int64_t i = 0; i < E.arguments.size(); ++i) {5827if (i >= first_defarg) {5828Variant arg = E.default_arguments[i - first_defarg];5829params.push_back(arg);5830} else {5831Callable::CallError ce;5832Variant arg;5833Variant::construct(E.arguments[i].type, arg, nullptr, 0, ce);5834params.push_back(arg);5835}5836}5837d["args"] = params;58385839EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();5840undo_redo->create_action(TTR("Add Method Track Key"));5841undo_redo->add_do_method(animation.ptr(), "track_insert_key", insert_key_from_track_call_track, insert_key_from_track_call_ofs, d);5842undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);5843undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", insert_key_from_track_call_track, insert_key_from_track_call_ofs);5844undo_redo->commit_action();58455846return;5847}5848}58495850EditorNode::get_singleton()->show_warning(TTR("Method not found in object:") + " " + p_method);5851}58525853void AnimationTrackEditor::_key_selected(int p_key, bool p_single, int p_track) {5854ERR_FAIL_INDEX(p_track, animation->get_track_count());5855ERR_FAIL_INDEX(p_key, animation->track_get_key_count(p_track));58565857SelectedKey sk;5858sk.key = p_key;5859sk.track = p_track;58605861if (p_single) {5862_clear_selection();5863}58645865KeyInfo ki;5866ki.pos = animation->track_get_key_time(p_track, p_key);5867selection[sk] = ki;58685869_redraw_tracks();5870_update_key_edit();58715872marker_edit->_clear_selection(marker_edit->is_selection_active());5873}58745875void AnimationTrackEditor::_key_deselected(int p_key, int p_track) {5876ERR_FAIL_INDEX(p_track, animation->get_track_count());5877ERR_FAIL_INDEX(p_key, animation->track_get_key_count(p_track));58785879SelectedKey sk;5880sk.key = p_key;5881sk.track = p_track;58825883selection.erase(sk);58845885_redraw_tracks();5886_update_key_edit();5887}58885889void AnimationTrackEditor::_move_selection_begin() {5890moving_selection = true;5891moving_selection_offset = 0;5892}58935894void AnimationTrackEditor::_move_selection(float p_offset) {5895moving_selection_offset = p_offset;5896_redraw_tracks();5897}58985899struct _AnimMoveRestore {5900int track = 0;5901float time = 0;5902Variant key;5903float transition = 0;5904};5905// Used for undo/redo.59065907void AnimationTrackEditor::_clear_key_edit() {5908if (key_edit) {5909// If key edit is the object being inspected, remove it first.5910if (InspectorDock::get_inspector_singleton()->get_edited_object() == key_edit) {5911EditorNode::get_singleton()->push_item(nullptr);5912}59135914// Then actually delete it.5915memdelete(key_edit);5916key_edit = nullptr;5917}59185919if (multi_key_edit) {5920if (InspectorDock::get_inspector_singleton()->get_edited_object() == multi_key_edit) {5921EditorNode::get_singleton()->push_item(nullptr);5922}59235924memdelete(multi_key_edit);5925multi_key_edit = nullptr;5926}5927}59285929void AnimationTrackEditor::_clear_selection(bool p_update) {5930selection.clear();59315932if (p_update) {5933_redraw_tracks();5934}59355936_clear_key_edit();5937}59385939void AnimationTrackEditor::_update_key_edit() {5940_clear_key_edit();5941if (animation.is_null()) {5942return;5943}59445945if (selection.size() == 1) {5946key_edit = memnew(AnimationTrackKeyEdit);5947key_edit->animation = animation;5948key_edit->animation_read_only = read_only;5949key_edit->track = selection.front()->key().track;5950key_edit->use_fps = timeline->is_using_fps();5951key_edit->editor = this;59525953int key_id = selection.front()->key().key;5954if (key_id >= animation->track_get_key_count(key_edit->track)) {5955_clear_key_edit();5956return; // Probably in the process of rearranging the keys.5957}5958float ofs = animation->track_get_key_time(key_edit->track, key_id);5959key_edit->key_ofs = ofs;5960key_edit->root_path = root;59615962NodePath np;5963key_edit->hint = _find_hint_for_track(key_edit->track, np);5964key_edit->base = np;59655966EditorNode::get_singleton()->push_item(key_edit);5967} else if (selection.size() > 1) {5968multi_key_edit = memnew(AnimationMultiTrackKeyEdit);5969multi_key_edit->animation = animation;5970multi_key_edit->animation_read_only = read_only;5971multi_key_edit->editor = this;59725973RBMap<int, List<float>> key_ofs_map;5974RBMap<int, NodePath> base_map;5975int first_track = -1;5976for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {5977int track = E.key.track;5978if (first_track < 0) {5979first_track = track;5980}59815982if (!key_ofs_map.has(track)) {5983key_ofs_map[track] = List<float>();5984base_map[track] = NodePath();5985}59865987int key_id = E.key.key;5988if (key_id >= animation->track_get_key_count(track)) {5989_clear_key_edit();5990return; // Probably in the process of rearranging the keys.5991}5992key_ofs_map[track].push_back(animation->track_get_key_time(track, E.key.key));5993}5994multi_key_edit->key_ofs_map = key_ofs_map;5995multi_key_edit->base_map = base_map;5996multi_key_edit->hint = _find_hint_for_track(first_track, base_map[first_track]);5997multi_key_edit->use_fps = timeline->is_using_fps();5998multi_key_edit->root_path = root;59996000EditorNode::get_singleton()->push_item(multi_key_edit);6001}6002}60036004void AnimationTrackEditor::_clear_selection_for_anim(const Ref<Animation> &p_anim) {6005if (animation != p_anim) {6006return;6007}60086009_clear_selection();6010}60116012void AnimationTrackEditor::_select_at_anim(const Ref<Animation> &p_anim, int p_track, float p_pos) {6013if (animation != p_anim) {6014return;6015}60166017int idx = animation->track_find_key(p_track, p_pos, Animation::FIND_MODE_APPROX);6018ERR_FAIL_COND(idx < 0);60196020SelectedKey sk;6021sk.track = p_track;6022sk.key = idx;6023KeyInfo ki;6024ki.pos = p_pos;60256026selection.insert(sk, ki);6027_update_key_edit();60286029marker_edit->_clear_selection(marker_edit->is_selection_active());6030}60316032void AnimationTrackEditor::_move_selection_commit() {6033EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();6034undo_redo->create_action(TTR("Animation Move Keys"));60356036List<_AnimMoveRestore> to_restore;60376038float motion = moving_selection_offset;6039// 1 - remove the keys.6040for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6041undo_redo->add_do_method(animation.ptr(), "track_remove_key", E->key().track, E->key().key);6042}6043// 2 - Remove overlapped keys.6044for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6045float newtime = E->get().pos + motion;6046int idx = animation->track_find_key(E->key().track, newtime, Animation::FIND_MODE_APPROX);6047if (idx == -1) {6048continue;6049}6050SelectedKey sk;6051sk.key = idx;6052sk.track = E->key().track;6053if (selection.has(sk)) {6054continue; // Already in selection, don't save.6055}60566057undo_redo->add_do_method(animation.ptr(), "track_remove_key_at_time", E->key().track, newtime);6058_AnimMoveRestore amr;60596060amr.key = animation->track_get_key_value(E->key().track, idx);6061amr.track = E->key().track;6062amr.time = newtime;6063amr.transition = animation->track_get_key_transition(E->key().track, idx);60646065to_restore.push_back(amr);6066}60676068// 3 - Move the keys (Reinsert them).6069for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6070float newpos = E->get().pos + motion;6071undo_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));6072}60736074// 4 - (Undo) Remove inserted keys.6075for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6076float newpos = E->get().pos + motion;6077undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", E->key().track, newpos);6078}60796080// 5 - (Undo) Reinsert keys.6081for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6082undo_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));6083}60846085// 6 - (Undo) Reinsert overlapped keys.6086for (_AnimMoveRestore &amr : to_restore) {6087undo_redo->add_undo_method(animation.ptr(), "track_insert_key", amr.track, amr.time, amr.key, amr.transition);6088}60896090undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);6091undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);60926093// 7 - Reselect.6094for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6095float oldpos = E->get().pos;6096float newpos = oldpos + motion;60976098undo_redo->add_do_method(this, "_select_at_anim", animation, E->key().track, newpos);6099undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, oldpos);6100}61016102moving_selection = false;6103undo_redo->add_do_method(this, "_redraw_tracks");6104undo_redo->add_undo_method(this, "_redraw_tracks");61056106// Update key frame.6107AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();6108if (ape) {6109undo_redo->add_do_method(ape, "_animation_update_key_frame");6110undo_redo->add_undo_method(ape, "_animation_update_key_frame");6111}61126113undo_redo->commit_action();6114}61156116void AnimationTrackEditor::_move_selection_cancel() {6117moving_selection = false;6118_redraw_tracks();6119}61206121bool AnimationTrackEditor::is_moving_selection() const {6122return moving_selection;6123}61246125float AnimationTrackEditor::get_moving_selection_offset() const {6126return moving_selection_offset;6127}61286129void AnimationTrackEditor::_box_selection_draw() {6130const Rect2 selection_rect = Rect2(Point2(), box_selection->get_size());6131box_selection->draw_rect(selection_rect, get_theme_color(SNAME("box_selection_fill_color"), EditorStringName(Editor)));6132box_selection->draw_rect(selection_rect, get_theme_color(SNAME("box_selection_stroke_color"), EditorStringName(Editor)), false, Math::round(EDSCALE));6133}61346135void AnimationTrackEditor::_scroll_input(const Ref<InputEvent> &p_event) {6136if (!box_selecting) {6137if (panner->gui_input(p_event, scroll->get_global_rect())) {6138scroll->accept_event();6139return;6140}6141}61426143Ref<InputEventMouseButton> mb = p_event;61446145if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT) {6146if (mb->is_pressed()) {6147box_selecting = true;6148box_selecting_from = scroll->get_global_transform().xform(mb->get_position());6149box_select_rect = Rect2();6150} else if (box_selecting) {6151if (box_selection->is_visible_in_tree()) {6152// Only if moved.6153for (int i = 0; i < track_edits.size(); i++) {6154Rect2 local_rect = box_select_rect;6155local_rect.position -= track_edits[i]->get_global_position();6156track_edits[i]->append_to_selection(local_rect, mb->is_command_or_control_pressed());6157}61586159if (_get_track_selected() == -1 && track_edits.size() > 0) { // Minimal hack to make shortcuts work.6160track_edits[track_edits.size() - 1]->grab_focus();6161}6162} else if (!mb->is_command_or_control_pressed() && !mb->is_shift_pressed()) {6163_clear_selection(true); // Clear it.6164}61656166box_selection->hide();6167box_selecting = false;6168}6169}61706171Ref<InputEventMouseMotion> mm = p_event;61726173if (mm.is_valid() && box_selecting) {6174if (!mm->get_button_mask().has_flag(MouseButtonMask::LEFT)) {6175// No longer.6176box_selection->hide();6177box_selecting = false;6178return;6179}61806181if (!box_selection->is_visible_in_tree()) {6182if (!mm->is_command_or_control_pressed() && !mm->is_shift_pressed()) {6183_clear_selection(true);6184}6185box_selection->show();6186}61876188Vector2 from = box_selecting_from;6189Vector2 to = scroll->get_global_transform().xform(mm->get_position());61906191box_selecting_to = to;61926193if (from.x > to.x) {6194SWAP(from.x, to.x);6195}61966197if (from.y > to.y) {6198SWAP(from.y, to.y);6199}62006201Rect2 rect(from, to - from);6202box_selection->set_rect(Rect2(from - scroll->get_global_position(), rect.get_size()));6203box_select_rect = rect;6204}6205}62066207void AnimationTrackEditor::_toggle_bezier_edit() {6208if (bezier_edit->is_visible()) {6209_cancel_bezier_edit();6210} else {6211int track_count = animation->get_track_count();6212for (int i = 0; i < track_count; ++i) {6213if (animation->track_get_type(i) == Animation::TrackType::TYPE_BEZIER) {6214_bezier_edit(i);6215return;6216}6217}6218}6219}62206221void AnimationTrackEditor::_scroll_changed(const Vector2 &p_val) {6222if (box_selecting) {6223const Vector2 scroll_difference = p_val - prev_scroll_position;62246225Vector2 from = box_selecting_from - scroll_difference;6226Vector2 to = box_selecting_to;62276228box_selecting_from = from;62296230if (from.x > to.x) {6231SWAP(from.x, to.x);6232}62336234if (from.y > to.y) {6235SWAP(from.y, to.y);6236}62376238Rect2 rect(from, to - from);6239box_selection->set_rect(Rect2(from - scroll->get_global_position(), rect.get_size()));6240box_select_rect = rect;6241}62426243prev_scroll_position = p_val;6244}62456246void AnimationTrackEditor::_v_scroll_changed(float p_val) {6247_scroll_changed(Vector2(prev_scroll_position.x, p_val));6248}62496250void AnimationTrackEditor::_h_scroll_changed(float p_val) {6251_scroll_changed(Vector2(p_val, prev_scroll_position.y));6252}62536254void AnimationTrackEditor::_pan_callback(Vector2 p_scroll_vec, Ref<InputEvent> p_event) {6255Ref<InputEventWithModifiers> iewm = p_event;6256if (iewm.is_valid() && iewm->is_alt_pressed()) {6257if (p_scroll_vec.x < 0 || p_scroll_vec.y < 0) {6258goto_prev_step(true);6259} else {6260goto_next_step(true);6261}6262} else {6263timeline->set_value(timeline->get_value() - p_scroll_vec.x / timeline->get_zoom_scale());6264scroll->set_v_scroll(scroll->get_v_scroll() - p_scroll_vec.y);6265}6266}62676268void AnimationTrackEditor::_zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref<InputEvent> p_event) {6269timeline->_zoom_callback(p_zoom_factor, p_origin, p_event);6270}62716272void AnimationTrackEditor::_cancel_bezier_edit() {6273bezier_edit->hide();6274box_selection_container->show();6275bezier_edit_icon->set_pressed(false);6276auto_fit->show();6277auto_fit_bezier->hide();6278}62796280void AnimationTrackEditor::_bezier_edit(int p_for_track) {6281_clear_selection(); // Bezier probably wants to use a separate selection mode.6282bezier_edit->set_root(root);6283bezier_edit->set_animation_and_track(animation, p_for_track, read_only);6284box_selection_container->hide();6285bezier_edit->show();6286auto_fit->hide();6287auto_fit_bezier->show();6288// Search everything within the track and curve - edit it.6289}62906291void 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) {6292ERR_FAIL_NULL(p_anim);6293p_anim->bezier_track_set_key_handle_mode(p_track, p_index, p_mode, p_set_mode);6294}62956296void 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) {6297ERR_FAIL_NULL(p_anim);6298int index = p_anim->track_find_key(p_track, p_time, Animation::FIND_MODE_APPROX);6299ERR_FAIL_COND(index < 0);6300_bezier_track_set_key_handle_mode(p_anim, p_track, index, p_mode, p_set_mode);6301}63026303void AnimationTrackEditor::_anim_duplicate_keys(float p_ofs, bool p_ofs_valid, int p_track) {6304if (selection.size() && animation.is_valid()) {6305int top_track = 0x7FFFFFFF;6306float top_time = 1e10;6307for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6308const SelectedKey &sk = E->key();63096310float t = animation->track_get_key_time(sk.track, sk.key);6311if (t < top_time) {6312top_time = t;6313}6314if (sk.track < top_track) {6315top_track = sk.track;6316}6317}6318ERR_FAIL_COND(top_track == 0x7FFFFFFF || top_time == 1e10);63196320int start_track = p_track;6321if (p_track == -1) { // Duplicating from shortcut or Edit menu.6322bool is_valid_track_selected = _get_track_selected() >= 0 && _get_track_selected() < animation->get_track_count();6323start_track = is_valid_track_selected ? _get_track_selected() : top_track;6324}63256326bool all_compatible = true;63276328for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6329const SelectedKey &sk = E->key();6330int dst_track = sk.track + (start_track - top_track);63316332if (dst_track < 0 || dst_track >= animation->get_track_count()) {6333all_compatible = false;6334break;6335}63366337Variant::Type value_type = animation->track_get_key_value(sk.track, sk.key).get_type();6338Animation::TrackType track_type = animation->track_get_type(sk.track);6339if (!_is_track_compatible(dst_track, value_type, track_type)) {6340all_compatible = false;6341break;6342}6343}63446345ERR_FAIL_COND_MSG(!all_compatible, "Duplicate failed: Not all animation keys were compatible with their target tracks");63466347EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();6348undo_redo->create_action(TTR("Animation Duplicate Keys"));63496350List<Pair<int, float>> new_selection_values;63516352for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6353const SelectedKey &sk = E->key();63546355float t = animation->track_get_key_time(sk.track, sk.key);6356float insert_pos = p_ofs_valid ? p_ofs : timeline->get_play_position();63576358if (p_ofs_valid) {6359if (snap_keys->is_pressed() && step->get_value() != 0) {6360insert_pos = snap_time(insert_pos);6361}6362}63636364float dst_time = t + (insert_pos - top_time);6365int dst_track = sk.track + (start_track - top_track);63666367if (dst_track < 0 || dst_track >= animation->get_track_count()) {6368continue;6369}63706371int existing_idx = animation->track_find_key(dst_track, dst_time, Animation::FIND_MODE_APPROX);63726373Variant value = animation->track_get_key_value(sk.track, sk.key);6374bool key_is_bezier = animation->track_get_type(sk.track) == Animation::TYPE_BEZIER;6375bool track_is_bezier = animation->track_get_type(dst_track) == Animation::TYPE_BEZIER;6376if (key_is_bezier && !track_is_bezier) {6377value = AnimationBezierTrackEdit::get_bezier_key_value(value);6378} else if (!key_is_bezier && track_is_bezier) {6379value = animation->make_default_bezier_key(value);6380}63816382undo_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));6383undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", dst_track, dst_time);63846385Pair<int, float> p;6386p.first = dst_track;6387p.second = dst_time;6388new_selection_values.push_back(p);63896390if (existing_idx != -1) {6391undo_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));6392}6393}63946395undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);6396undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);63976398// Reselect duplicated.6399RBMap<SelectedKey, KeyInfo> new_selection;6400for (const Pair<int, float> &E : new_selection_values) {6401undo_redo->add_do_method(this, "_select_at_anim", animation, E.first, E.second);6402}6403for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6404undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, E->get().pos);6405}64066407undo_redo->add_do_method(this, "_redraw_tracks");6408undo_redo->add_undo_method(this, "_redraw_tracks");6409undo_redo->commit_action();6410}6411}64126413void AnimationTrackEditor::_anim_copy_keys(bool p_cut) {6414if (is_selection_active() && animation.is_valid()) {6415int top_track = 0x7FFFFFFF;6416float top_time = 1e10;64176418for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6419const SelectedKey &sk = E->key();64206421float t = animation->track_get_key_time(sk.track, sk.key);6422if (t < top_time) {6423top_time = t;6424}6425if (sk.track < top_track) {6426top_track = sk.track;6427}6428}64296430ERR_FAIL_COND(top_track == 0x7FFFFFFF || top_time == 1e10);64316432_set_key_clipboard(top_track, top_time, selection);64336434if (p_cut) {6435EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();6436undo_redo->create_action(TTR("Animation Cut Keys"), UndoRedo::MERGE_DISABLE, animation.ptr());6437undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);6438undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);6439for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6440int track_idx = E->key().track;6441int key_idx = E->key().key;6442float time = E->value().pos;6443undo_redo->add_do_method(animation.ptr(), "track_remove_key_at_time", track_idx, time);6444undo_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));6445}6446for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6447undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, E->value().pos);6448}6449undo_redo->commit_action();6450}6451}6452}64536454void AnimationTrackEditor::_set_key_clipboard(int p_top_track, float p_top_time, RBMap<SelectedKey, KeyInfo> &p_keys) {6455key_clipboard.keys.clear();6456key_clipboard.top_track = p_top_track;6457for (RBMap<SelectedKey, KeyInfo>::Element *E = p_keys.back(); E; E = E->prev()) {6458KeyClipboard::Key k;6459k.value = animation->track_get_key_value(E->key().track, E->key().key);6460k.transition = animation->track_get_key_transition(E->key().track, E->key().key);6461k.time = E->value().pos - p_top_time;6462k.track = E->key().track - p_top_track;6463k.track_type = animation->track_get_type(E->key().track);64646465key_clipboard.keys.push_back(k);6466}6467}64686469void AnimationTrackEditor::_anim_paste_keys(float p_ofs, bool p_ofs_valid, int p_track) {6470if (is_key_clipboard_active() && animation.is_valid()) {6471int start_track = p_track;6472if (p_track == -1) { // Pasting from shortcut or Edit menu.6473bool is_valid_track_selected = _get_track_selected() >= 0 && _get_track_selected() < animation->get_track_count();6474start_track = is_valid_track_selected ? _get_track_selected() : key_clipboard.top_track;6475}64766477bool all_compatible = true;64786479for (int i = 0; i < key_clipboard.keys.size(); i++) {6480const KeyClipboard::Key key = key_clipboard.keys[i];64816482int dst_track = key.track + start_track;64836484if (dst_track < 0 || dst_track >= animation->get_track_count()) {6485all_compatible = false;6486break;6487}64886489if (!_is_track_compatible(dst_track, key.value.get_type(), key.track_type)) {6490all_compatible = false;6491break;6492}6493}64946495ERR_FAIL_COND_MSG(!all_compatible, "Paste failed: Not all animation keys were compatible with their target tracks");64966497EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();6498undo_redo->create_action(TTR("Animation Paste Keys"));6499List<Pair<int, float>> new_selection_values;65006501for (int i = 0; i < key_clipboard.keys.size(); i++) {6502const KeyClipboard::Key key = key_clipboard.keys[i];65036504float insert_pos = p_ofs_valid ? p_ofs : timeline->get_play_position();65056506if (p_ofs_valid) {6507if (snap_keys->is_pressed() && step->get_value() != 0) {6508insert_pos = snap_time(insert_pos);6509}6510}65116512float dst_time = key.time + insert_pos;6513int dst_track = key.track + start_track;65146515int existing_idx = animation->track_find_key(dst_track, dst_time, Animation::FIND_MODE_APPROX);65166517Variant value = key.value;6518bool key_is_bezier = key.track_type == Animation::TYPE_BEZIER;6519bool track_is_bezier = animation->track_get_type(dst_track) == Animation::TYPE_BEZIER;6520if (key_is_bezier && !track_is_bezier) {6521value = AnimationBezierTrackEdit::get_bezier_key_value(value);6522} else if (!key_is_bezier && track_is_bezier) {6523value = animation->make_default_bezier_key(value);6524}65256526undo_redo->add_do_method(animation.ptr(), "track_insert_key", dst_track, dst_time, value, key.transition);6527undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", dst_track, dst_time);65286529Pair<int, float> p;6530p.first = dst_track;6531p.second = dst_time;6532new_selection_values.push_back(p);65336534if (existing_idx != -1) {6535undo_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));6536}6537}65386539undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);6540undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);65416542// Reselect pasted.6543for (const Pair<int, float> &E : new_selection_values) {6544undo_redo->add_do_method(this, "_select_at_anim", animation, E.first, E.second);6545}6546for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6547undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, E->get().pos);6548}65496550undo_redo->add_do_method(this, "_redraw_tracks");6551undo_redo->add_undo_method(this, "_redraw_tracks");6552undo_redo->commit_action();6553}6554}65556556bool AnimationTrackEditor::_is_track_compatible(int p_target_track_idx, Variant::Type p_source_value_type, Animation::TrackType p_source_track_type) {6557if (animation.is_valid()) {6558Animation::TrackType target_track_type = animation->track_get_type(p_target_track_idx);6559bool track_types_equal = target_track_type == p_source_track_type;6560bool 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;6561bool is_source_bezier = p_source_track_type == Animation::TYPE_BEZIER;6562switch (target_track_type) {6563case Animation::TYPE_POSITION_3D:6564case Animation::TYPE_SCALE_3D:6565return p_source_value_type == Variant::VECTOR3;6566case Animation::TYPE_ROTATION_3D:6567return p_source_value_type == Variant::QUATERNION;6568case Animation::TYPE_BEZIER:6569return track_types_equal || p_source_value_type == Variant::FLOAT;6570case Animation::TYPE_VALUE:6571if (track_types_equal || is_source_vector3_type || is_source_bezier) {6572bool path_valid = false;6573Variant::Type property_type = Variant::NIL;65746575AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();6576if (ape) {6577AnimationPlayer *ap = ape->get_player();6578if (ap) {6579NodePath npath = animation->track_get_path(p_target_track_idx);6580Node *a_ap_root_node = ap->get_node(ap->get_root_node());6581Node *nd = nullptr;6582// We must test that we have a valid a_ap_root_node before trying to access its content to init the nd Node.6583if (a_ap_root_node) {6584nd = a_ap_root_node->get_node(NodePath(npath.get_concatenated_names()));6585}6586if (nd) {6587StringName prop = npath.get_concatenated_subnames();6588PropertyInfo prop_info;6589path_valid = ClassDB::get_property_info(nd->get_class(), prop, &prop_info);6590property_type = prop_info.type;6591}6592}6593}65946595if (path_valid) {6596if (is_source_bezier) {6597p_source_value_type = Variant::FLOAT;6598}6599return property_type == p_source_value_type;6600} else {6601if (animation->track_get_key_count(p_target_track_idx) > 0) {6602Variant::Type first_key_type = animation->track_get_key_value(p_target_track_idx, 0).get_type();6603return first_key_type == p_source_value_type;6604}6605return true; // Type is Undefined.6606}6607}6608return false;6609default: // Works for TYPE_ANIMATION; TYPE_AUDIO; TYPE_CALL_METHOD; BLEND_SHAPE.6610return track_types_equal;6611}6612}6613return false;6614}66156616void AnimationTrackEditor::_edit_menu_about_to_popup() {6617AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();6618edit->get_popup()->set_item_disabled(edit->get_popup()->get_item_index(EDIT_APPLY_RESET), !player->can_apply_reset());66196620bool has_length = false;6621for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {6622if (animation->track_get_type(E.key.track) == Animation::TYPE_AUDIO && animation->audio_track_get_key_stream(E.key.track, E.key.key).is_valid()) {6623has_length = true;6624break;6625}6626}6627edit->get_popup()->set_item_disabled(edit->get_popup()->get_item_index(EDIT_SET_START_OFFSET), !has_length);6628edit->get_popup()->set_item_disabled(edit->get_popup()->get_item_index(EDIT_SET_END_OFFSET), !has_length);6629}66306631void AnimationTrackEditor::goto_prev_step(bool p_from_mouse_event) {6632if (animation.is_null()) {6633return;6634}6635float anim_step = animation->get_step();6636if (anim_step == 0.0) {6637anim_step = 1.0;6638}6639if (p_from_mouse_event && Input::get_singleton()->is_key_pressed(Key::SHIFT)) {6640// Use more precise snapping when holding Shift.6641// This is used when scrobbling the timeline using Alt + Mouse wheel.6642anim_step *= 0.25;6643}66446645float pos = timeline->get_play_position();6646pos = Math::snapped(pos - anim_step, anim_step);6647if (pos < 0.0) {6648pos = 0.0;6649}6650set_anim_pos(pos);6651_timeline_changed(pos, false);6652}66536654void AnimationTrackEditor::goto_next_step(bool p_from_mouse_event, bool p_timeline_only) {6655if (animation.is_null()) {6656return;6657}6658float anim_step = animation->get_step();6659if (anim_step == 0.0) {6660anim_step = 1.0;6661}6662if (p_from_mouse_event && Input::get_singleton()->is_key_pressed(Key::SHIFT)) {6663// Use more precise snapping when holding Shift.6664// This is used when scrobbling the timeline using Alt + Mouse wheel.6665// Do not use precise snapping when using the menu action or keyboard shortcut,6666// as the default keyboard shortcut requires pressing Shift.6667anim_step *= 0.25;6668}66696670float pos = timeline->get_play_position();66716672pos = Math::snapped(pos + anim_step, anim_step);6673if (pos > animation->get_length()) {6674pos = animation->get_length();6675}6676set_anim_pos(pos);66776678_timeline_changed(pos, p_timeline_only);6679}66806681void AnimationTrackEditor::_edit_menu_pressed(int p_option) {6682switch (p_option) {6683case EDIT_COPY_TRACKS: {6684track_copy_select->clear();6685TreeItem *troot = track_copy_select->create_item();66866687for (int i = 0; i < animation->get_track_count(); i++) {6688NodePath path = animation->track_get_path(i);6689Node *node = nullptr;66906691if (root) {6692node = root->get_node_or_null(path);6693}66946695String text;6696Ref<Texture2D> icon = get_editor_theme_icon(SNAME("Node"));6697if (node) {6698if (has_theme_icon(node->get_class(), EditorStringName(EditorIcons))) {6699icon = get_editor_theme_icon(node->get_class());6700}67016702text = node->get_name();6703Vector<StringName> sn = path.get_subnames();6704for (int j = 0; j < sn.size(); j++) {6705text += ".";6706text += sn[j];6707}67086709path = NodePath(node->get_path().get_names(), path.get_subnames(), true); // Store full path instead for copying.6710} else {6711text = String(path);6712int sep = text.find_char(':');6713if (sep != -1) {6714text = text.substr(sep + 1);6715}6716}67176718String track_type;6719switch (animation->track_get_type(i)) {6720case Animation::TYPE_POSITION_3D:6721track_type = TTR("Position");6722break;6723case Animation::TYPE_ROTATION_3D:6724track_type = TTR("Rotation");6725break;6726case Animation::TYPE_SCALE_3D:6727track_type = TTR("Scale");6728break;6729case Animation::TYPE_BLEND_SHAPE:6730track_type = TTR("BlendShape");6731break;6732case Animation::TYPE_METHOD:6733track_type = TTR("Methods");6734break;6735case Animation::TYPE_BEZIER:6736track_type = TTR("Bezier");6737break;6738case Animation::TYPE_AUDIO:6739track_type = TTR("Audio");6740break;6741default: {6742};6743}6744if (!track_type.is_empty()) {6745text += vformat(" (%s)", track_type);6746}67476748TreeItem *it = track_copy_select->create_item(troot);6749it->set_editable(0, true);6750it->set_selectable(0, true);6751it->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);6752it->set_icon(0, icon);6753it->set_text(0, text);6754Dictionary md;6755md["track_idx"] = i;6756md["path"] = path;6757it->set_metadata(0, md);6758}67596760track_copy_dialog->popup_centered(Size2(350, 500) * EDSCALE);6761} break;6762case EDIT_COPY_TRACKS_CONFIRM: {6763track_clipboard.clear();6764TreeItem *tree_root = track_copy_select->get_root();6765if (tree_root) {6766TreeItem *it = tree_root->get_first_child();6767while (it) {6768Dictionary md = it->get_metadata(0);6769int idx = md["track_idx"];6770if (it->is_checked(0) && idx >= 0 && idx < animation->get_track_count()) {6771TrackClipboard tc;6772tc.base_path = animation->track_get_path(idx);6773tc.full_path = md["path"];6774tc.track_type = animation->track_get_type(idx);6775tc.interp_type = animation->track_get_interpolation_type(idx);6776if (tc.track_type == Animation::TYPE_VALUE) {6777tc.update_mode = animation->value_track_get_update_mode(idx);6778}6779if (tc.track_type == Animation::TYPE_AUDIO) {6780tc.use_blend = animation->audio_track_is_use_blend(idx);6781}6782tc.loop_wrap = animation->track_get_interpolation_loop_wrap(idx);6783tc.enabled = animation->track_is_enabled(idx);6784for (int i = 0; i < animation->track_get_key_count(idx); i++) {6785TrackClipboard::Key k;6786k.time = animation->track_get_key_time(idx, i);6787k.value = animation->track_get_key_value(idx, i);6788k.transition = animation->track_get_key_transition(idx, i);6789tc.keys.push_back(k);6790}6791track_clipboard.push_back(tc);6792}6793it = it->get_next();6794}6795}6796} break;6797case EDIT_PASTE_TRACKS: {6798if (track_clipboard.is_empty()) {6799EditorNode::get_singleton()->show_warning(TTR("Clipboard is empty!"));6800break;6801}68026803int base_track = animation->get_track_count();6804EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();6805undo_redo->create_action(TTR("Paste Tracks"));6806for (int i = 0; i < track_clipboard.size(); i++) {6807undo_redo->add_do_method(animation.ptr(), "add_track", track_clipboard[i].track_type);6808Node *exists = nullptr;6809NodePath path = track_clipboard[i].base_path;68106811if (root) {6812NodePath np = track_clipboard[i].full_path;6813exists = root->get_node_or_null(np);6814if (exists) {6815path = NodePath(root->get_path_to(exists).get_names(), track_clipboard[i].full_path.get_subnames(), false);6816}6817}68186819undo_redo->add_do_method(animation.ptr(), "track_set_path", base_track, path);6820undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_type", base_track, track_clipboard[i].interp_type);6821undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_loop_wrap", base_track, track_clipboard[i].loop_wrap);6822undo_redo->add_do_method(animation.ptr(), "track_set_enabled", base_track, track_clipboard[i].enabled);6823if (track_clipboard[i].track_type == Animation::TYPE_VALUE) {6824undo_redo->add_do_method(animation.ptr(), "value_track_set_update_mode", base_track, track_clipboard[i].update_mode);6825}6826if (track_clipboard[i].track_type == Animation::TYPE_AUDIO) {6827undo_redo->add_do_method(animation.ptr(), "audio_track_set_use_blend", base_track, track_clipboard[i].use_blend);6828}68296830for (int j = 0; j < track_clipboard[i].keys.size(); j++) {6831undo_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);6832}68336834undo_redo->add_undo_method(animation.ptr(), "remove_track", animation->get_track_count());68356836base_track++;6837}68386839undo_redo->commit_action();6840} break;6841case EDIT_SCALE_SELECTION: {6842scale_dialog->popup_centered(Size2(200, 100) * EDSCALE);6843scale->get_line_edit()->grab_focus();6844scale_from_cursor = false;6845} break;6846case EDIT_SCALE_FROM_CURSOR: {6847scale_dialog->popup_centered(Size2(200, 100) * EDSCALE);6848scale->get_line_edit()->grab_focus();6849scale_from_cursor = true;6850} break;6851case EDIT_SCALE_CONFIRM: {6852if (selection.is_empty()) {6853return;6854}68556856float from_t = 1e20;6857float to_t = -1e20;6858float len = -1e20;6859float pivot = 0;68606861for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {6862float t = animation->track_get_key_time(E.key.track, E.key.key);6863if (t < from_t) {6864from_t = t;6865}6866if (t > to_t) {6867to_t = t;6868}6869}68706871len = to_t - from_t;6872if (scale_from_cursor) {6873pivot = timeline->get_play_position();6874} else {6875pivot = from_t;6876}68776878float s = scale->get_value();6879ERR_FAIL_COND_MSG(s == 0, "Can't scale to 0.");68806881EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();6882undo_redo->create_action(TTR("Animation Scale Keys"));68836884List<_AnimMoveRestore> to_restore;68856886// 1 - Remove the keys.6887for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6888undo_redo->add_do_method(animation.ptr(), "track_remove_key", E->key().track, E->key().key);6889}6890// 2 - Remove overlapped keys.6891for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6892float newtime = (E->get().pos - from_t) * s + from_t;6893int idx = animation->track_find_key(E->key().track, newtime, Animation::FIND_MODE_APPROX);6894if (idx == -1) {6895continue;6896}6897SelectedKey sk;6898sk.key = idx;6899sk.track = E->key().track;6900if (selection.has(sk)) {6901continue; // Already in selection, don't save.6902}69036904undo_redo->add_do_method(animation.ptr(), "track_remove_key_at_time", E->key().track, newtime);6905_AnimMoveRestore amr;69066907amr.key = animation->track_get_key_value(E->key().track, idx);6908amr.track = E->key().track;6909amr.time = newtime;6910amr.transition = animation->track_get_key_transition(E->key().track, idx);69116912to_restore.push_back(amr);6913}69146915#define NEW_POS(m_ofs) (((s > 0) ? m_ofs : from_t + (len - (m_ofs - from_t))) - pivot) * Math::abs(s) + pivot6916// 3 - Move the keys (re insert them).6917for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6918float newpos = NEW_POS(E->get().pos);6919undo_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));6920}69216922// 4 - (Undo) Remove inserted keys.6923for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6924float newpos = NEW_POS(E->get().pos);6925undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", E->key().track, newpos);6926}69276928// 5 - (Undo) Reinsert keys.6929for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6930undo_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));6931}69326933// 6 - (Undo) Reinsert overlapped keys.6934for (_AnimMoveRestore &amr : to_restore) {6935undo_redo->add_undo_method(animation.ptr(), "track_insert_key", amr.track, amr.time, amr.key, amr.transition);6936}69376938undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);6939undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);69406941// 7 - Reselect.6942for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {6943float oldpos = E->get().pos;6944float newpos = NEW_POS(oldpos);6945if (newpos >= 0) {6946undo_redo->add_do_method(this, "_select_at_anim", animation, E->key().track, newpos);6947}6948undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, oldpos);6949}6950#undef NEW_POS69516952undo_redo->add_do_method(this, "_redraw_tracks");6953undo_redo->add_undo_method(this, "_redraw_tracks");6954undo_redo->commit_action();6955} break;69566957case EDIT_SET_START_OFFSET: {6958EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();6959undo_redo->create_action(TTR("Animation Set Start Offset"), UndoRedo::MERGE_ENDS);6960for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {6961if (animation->track_get_type(E.key.track) != Animation::TYPE_AUDIO) {6962continue;6963}6964Ref<AudioStream> stream = animation->audio_track_get_key_stream(E.key.track, E.key.key);6965if (stream.is_null()) {6966continue;6967}6968double len = stream->get_length() - animation->audio_track_get_key_end_offset(E.key.track, E.key.key);6969real_t prev_offset = animation->audio_track_get_key_start_offset(E.key.track, E.key.key);6970double prev_time = animation->track_get_key_time(E.key.track, E.key.key);6971float cur_time = timeline->get_play_position();6972float diff = prev_offset + cur_time - prev_time;6973float destination = cur_time - MIN(0, diff);6974if (diff >= len || animation->track_find_key(E.key.track, destination, Animation::FIND_MODE_EXACT) >= 0) {6975continue;6976}6977undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_start_offset", E.key.track, E.key.key, diff);6978undo_redo->add_do_method(animation.ptr(), "track_set_key_time", E.key.track, E.key.key, destination);6979undo_redo->add_undo_method(animation.ptr(), "track_set_key_time", E.key.track, E.key.key, prev_time);6980undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_start_offset", E.key.track, E.key.key, prev_offset);6981}6982undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);6983undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);6984undo_redo->commit_action();6985} break;6986case EDIT_SET_END_OFFSET: {6987EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();6988undo_redo->create_action(TTR("Animation Set End Offset"), UndoRedo::MERGE_ENDS);6989for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {6990if (animation->track_get_type(E.key.track) != Animation::TYPE_AUDIO) {6991continue;6992}6993Ref<AudioStream> stream = animation->audio_track_get_key_stream(E.key.track, E.key.key);6994if (stream.is_null()) {6995continue;6996}6997double len = stream->get_length() - animation->audio_track_get_key_start_offset(E.key.track, E.key.key);6998real_t prev_offset = animation->audio_track_get_key_end_offset(E.key.track, E.key.key);6999double prev_time = animation->track_get_key_time(E.key.track, E.key.key);7000float cur_time = timeline->get_play_position();7001float diff = prev_time + len - cur_time;7002if (diff >= len) {7003continue;7004}7005undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_end_offset", E.key.track, E.key.key, diff);7006undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_end_offset", E.key.track, E.key.key, prev_offset);7007}7008undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);7009undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);7010undo_redo->commit_action();7011} break;70127013case EDIT_EASE_SELECTION: {7014ease_dialog->popup_centered(Size2(200, 100) * EDSCALE);7015} break;7016case EDIT_EASE_CONFIRM: {7017EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7018undo_redo->create_action(TTR("Make Easing Keys"));70197020Tween::TransitionType transition_type = static_cast<Tween::TransitionType>(transition_selection->get_selected_id());7021Tween::EaseType ease_type = static_cast<Tween::EaseType>(ease_selection->get_selected_id());7022float fps = ease_fps->get_value();7023double dur_step = 1.0 / fps;70247025// Organize track and key.7026HashMap<int, Vector<int>> keymap;7027Vector<int> tracks;7028for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {7029if (!tracks.has(E.key.track)) {7030tracks.append(E.key.track);7031}7032}7033for (int i = 0; i < tracks.size(); i++) {7034switch (animation->track_get_type(tracks[i])) {7035case Animation::TYPE_VALUE:7036case Animation::TYPE_POSITION_3D:7037case Animation::TYPE_ROTATION_3D:7038case Animation::TYPE_SCALE_3D:7039case Animation::TYPE_BLEND_SHAPE: {7040Vector<int> keys;7041for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {7042if (E.key.track == tracks[i]) {7043keys.append(E.key.key);7044}7045}7046keys.sort();7047keymap.insert(tracks[i], keys);7048} break;7049default: {7050} break;7051}7052}70537054// Make easing.7055HashMap<int, Vector<int>>::Iterator E = keymap.begin();7056while (E) {7057int track = E->key;7058Vector<int> keys = E->value;7059int len = keys.size() - 1;70607061// Special case for angle interpolation.7062bool is_using_angle = animation->track_get_interpolation_type(track) == Animation::INTERPOLATION_LINEAR_ANGLE || animation->track_get_interpolation_type(track) == Animation::INTERPOLATION_CUBIC_ANGLE;70637064// Make insert queue.7065Vector<Pair<real_t, Variant>> insert_queue_new;7066for (int i = 0; i < len; i++) {7067// Check neighboring keys.7068if (keys[i] + 1 == keys[i + 1]) {7069double from_t = animation->track_get_key_time(track, keys[i]);7070double to_t = animation->track_get_key_time(track, keys[i + 1]);7071Variant from_v = animation->track_get_key_value(track, keys[i]);7072Variant to_v = animation->track_get_key_value(track, keys[i + 1]);7073if (is_using_angle) {7074real_t a = from_v;7075real_t b = to_v;7076real_t to_diff = std::fmod(b - a, Math::TAU);7077to_v = a + std::fmod(2.0 * to_diff, Math::TAU) - to_diff;7078}7079Variant delta_v = Animation::subtract_variant(to_v, from_v);7080double duration = to_t - from_t;7081double fixed_duration = duration - UNIT_EPSILON; // Prevent to overwrap keys...7082for (double delta_t = dur_step; delta_t < fixed_duration; delta_t += dur_step) {7083Pair<real_t, Variant> keydata;7084keydata.first = from_t + delta_t;7085keydata.second = Tween::interpolate_variant(from_v, delta_v, delta_t, duration, transition_type, ease_type);7086insert_queue_new.append(keydata);7087}7088}7089}70907091// Do insertion.7092for (int i = 0; i < insert_queue_new.size(); i++) {7093undo_redo->add_do_method(animation.ptr(), "track_insert_key", track, insert_queue_new[i].first, insert_queue_new[i].second);7094undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", track, insert_queue_new[i].first);7095}70967097++E;7098}70997100undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);7101undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);7102undo_redo->add_do_method(this, "_redraw_tracks");7103undo_redo->add_undo_method(this, "_redraw_tracks");7104undo_redo->commit_action();7105_update_key_edit();71067107} break;71087109case EDIT_DUPLICATE_SELECTED_KEYS: {7110if (bezier_edit->is_visible()) {7111bezier_edit->duplicate_selected_keys(-1.0, false);7112break;7113}7114_anim_duplicate_keys(-1.0, false, -1.0);7115} break;7116case EDIT_CUT_KEYS: {7117if (bezier_edit->is_visible()) {7118bezier_edit->copy_selected_keys(true);7119break;7120}7121_anim_copy_keys(true);7122} break;7123case EDIT_COPY_KEYS: {7124if (bezier_edit->is_visible()) {7125bezier_edit->copy_selected_keys(false);7126break;7127}7128_anim_copy_keys(false);7129} break;7130case EDIT_PASTE_KEYS: {7131_anim_paste_keys(-1.0, false, -1.0);7132} break;7133case EDIT_MOVE_FIRST_SELECTED_KEY_TO_CURSOR: {7134if (moving_selection || selection.is_empty()) {7135break;7136}7137real_t from_t = 1e20;7138for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {7139real_t t = animation->track_get_key_time(E.key.track, E.key.key);7140if (t < from_t) {7141from_t = t;7142}7143}7144_move_selection_begin();7145_move_selection(timeline->get_play_position() - from_t);7146_move_selection_commit();7147} break;7148case EDIT_MOVE_LAST_SELECTED_KEY_TO_CURSOR: {7149if (moving_selection || selection.is_empty()) {7150break;7151}7152real_t to_t = -1e20;7153for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {7154real_t t = animation->track_get_key_time(E.key.track, E.key.key);7155if (t > to_t) {7156to_t = t;7157}7158}7159_move_selection_begin();7160_move_selection(timeline->get_play_position() - to_t);7161_move_selection_commit();7162} break;7163case EDIT_ADD_RESET_KEY: {7164EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7165undo_redo->create_action(TTR("Animation Add RESET Keys"));71667167Ref<Animation> reset = _create_and_get_reset_animation();7168int reset_tracks = reset->get_track_count();7169HashSet<int> tracks_added;71707171for (const KeyValue<SelectedKey, KeyInfo> &E : selection) {7172const SelectedKey &sk = E.key;71737174const Animation::TrackType track_type = animation->track_get_type(E.key.track);7175if (track_type == Animation::TYPE_ANIMATION || track_type == Animation::TYPE_AUDIO || track_type == Animation::TYPE_METHOD) {7176continue;7177}71787179// Only add one key per track.7180if (tracks_added.has(sk.track)) {7181continue;7182}7183tracks_added.insert(sk.track);71847185int dst_track = -1;71867187const NodePath &path = animation->track_get_path(sk.track);7188for (int i = 0; i < reset->get_track_count(); i++) {7189if (reset->track_get_path(i) == path) {7190dst_track = i;7191break;7192}7193}71947195int existing_idx = -1;7196if (dst_track == -1) {7197// If adding multiple tracks, make sure that correct track is referenced.7198dst_track = reset_tracks;7199reset_tracks++;72007201undo_redo->add_do_method(reset.ptr(), "add_track", animation->track_get_type(sk.track));7202undo_redo->add_do_method(reset.ptr(), "track_set_path", dst_track, path);7203undo_redo->add_undo_method(reset.ptr(), "remove_track", dst_track);7204} else {7205existing_idx = reset->track_find_key(dst_track, 0, Animation::FIND_MODE_APPROX);7206}72077208if (animation->track_get_type(sk.track) == Animation::TYPE_VALUE) {7209undo_redo->add_do_method(reset.ptr(), "value_track_set_update_mode", dst_track, animation->value_track_get_update_mode(sk.track));7210}7211if (animation->track_get_type(sk.track) == Animation::TYPE_AUDIO) {7212undo_redo->add_do_method(reset.ptr(), "audio_track_set_use_blend", dst_track, animation->audio_track_is_use_blend(sk.track));7213}7214undo_redo->add_do_method(reset.ptr(), "track_set_interpolation_type", dst_track, animation->track_get_interpolation_type(sk.track));7215undo_redo->add_do_method(reset.ptr(), "track_set_interpolation_loop_wrap", dst_track, animation->track_get_interpolation_loop_wrap(sk.track));72167217undo_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));7218undo_redo->add_undo_method(reset.ptr(), "track_remove_key_at_time", dst_track, 0);72197220if (existing_idx != -1) {7221undo_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));7222}7223}72247225undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);7226undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);7227undo_redo->add_do_method(this, "_redraw_tracks");7228undo_redo->add_undo_method(this, "_redraw_tracks");7229undo_redo->commit_action();72307231} break;7232case EDIT_DELETE_SELECTION: {7233if (bezier_edit->is_visible()) {7234bezier_edit->delete_selection();7235break;7236}72377238if (selection.size()) {7239EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7240undo_redo->create_action(TTR("Animation Delete Keys"));72417242for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {7243undo_redo->add_do_method(animation.ptr(), "track_remove_key", E->key().track, E->key().key);7244undo_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));7245}7246undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);7247undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);7248undo_redo->add_do_method(this, "_redraw_tracks");7249undo_redo->add_undo_method(this, "_redraw_tracks");7250undo_redo->commit_action();7251_update_key_edit();7252}7253} break;7254case EDIT_GOTO_NEXT_STEP_TIMELINE_ONLY:7255case EDIT_GOTO_NEXT_STEP: {7256goto_next_step(false, p_option == EDIT_GOTO_NEXT_STEP_TIMELINE_ONLY);7257} break;7258case EDIT_GOTO_PREV_STEP: {7259goto_prev_step(false);7260} break;72617262case EDIT_APPLY_RESET: {7263AnimationPlayerEditor::get_singleton()->get_player()->apply_reset(true);7264} break;72657266case EDIT_BAKE_ANIMATION: {7267bake_dialog->popup_centered(Size2(200, 100) * EDSCALE);7268} break;7269case EDIT_BAKE_ANIMATION_CONFIRM: {7270EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7271undo_redo->create_action(TTR("Bake Animation as Linear Keys"));72727273int track_len = animation->get_track_count();7274bool b_trs = bake_trs->is_pressed();7275bool b_bs = bake_blendshape->is_pressed();7276bool b_v = bake_value->is_pressed();72777278double anim_len = animation->get_length() + CMP_EPSILON; // For end key.7279float fps = bake_fps->get_value();7280double dur_step = 1.0 / fps;72817282for (int i = 0; i < track_len; i++) {7283bool do_bake = false;7284Animation::TrackType type = animation->track_get_type(i);7285do_bake |= b_trs && (type == Animation::TYPE_POSITION_3D || type == Animation::TYPE_ROTATION_3D || type == Animation::TYPE_SCALE_3D);7286do_bake |= b_bs && type == Animation::TYPE_BLEND_SHAPE;7287do_bake |= b_v && type == Animation::TYPE_VALUE;7288if (do_bake && !animation->track_is_compressed(i)) {7289Animation::InterpolationType it = animation->track_get_interpolation_type(i);7290if (it == Animation::INTERPOLATION_NEAREST) {7291continue; // Nearest interpolation cannot be baked.7292}72937294// Special case for angle interpolation.7295bool is_using_angle = it == Animation::INTERPOLATION_LINEAR_ANGLE || it == Animation::INTERPOLATION_CUBIC_ANGLE;72967297// Make insert queue.7298Vector<Pair<real_t, Variant>> insert_queue_new;72997300switch (type) {7301case Animation::TYPE_POSITION_3D: {7302for (double delta_t = 0.0; delta_t <= anim_len; delta_t += dur_step) {7303Pair<real_t, Variant> keydata;7304keydata.first = delta_t;7305Vector3 v;7306animation->try_position_track_interpolate(i, delta_t, &v);7307keydata.second = v;7308insert_queue_new.append(keydata);7309}7310} break;7311case Animation::TYPE_ROTATION_3D: {7312for (double delta_t = 0.0; delta_t <= anim_len; delta_t += dur_step) {7313Pair<real_t, Variant> keydata;7314keydata.first = delta_t;7315Quaternion v;7316animation->try_rotation_track_interpolate(i, delta_t, &v);7317keydata.second = v;7318insert_queue_new.append(keydata);7319}7320} break;7321case Animation::TYPE_SCALE_3D: {7322for (double delta_t = 0.0; delta_t <= anim_len; delta_t += dur_step) {7323Pair<real_t, Variant> keydata;7324keydata.first = delta_t;7325Vector3 v;7326animation->try_scale_track_interpolate(i, delta_t, &v);7327keydata.second = v;7328insert_queue_new.append(keydata);7329}7330} break;7331case Animation::TYPE_BLEND_SHAPE: {7332for (double delta_t = 0.0; delta_t <= anim_len; delta_t += dur_step) {7333Pair<real_t, Variant> keydata;7334keydata.first = delta_t;7335float v;7336animation->try_blend_shape_track_interpolate(i, delta_t, &v);7337keydata.second = v;7338insert_queue_new.append(keydata);7339}7340} break;7341case Animation::TYPE_VALUE: {7342for (double delta_t = 0.0; delta_t < anim_len; delta_t += dur_step) {7343Pair<real_t, Variant> keydata;7344keydata.first = delta_t;7345keydata.second = animation->value_track_interpolate(i, delta_t);7346insert_queue_new.append(keydata);7347}7348} break;7349default: {7350} break;7351}73527353// Cleanup keys.7354int key_len = animation->track_get_key_count(i);7355for (int j = key_len - 1; j >= 0; j--) {7356undo_redo->add_do_method(animation.ptr(), "track_remove_key", i, j);7357}73587359// Insert keys.7360undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_type", i, is_using_angle ? Animation::INTERPOLATION_LINEAR_ANGLE : Animation::INTERPOLATION_LINEAR);7361for (int j = insert_queue_new.size() - 1; j >= 0; j--) {7362undo_redo->add_do_method(animation.ptr(), "track_insert_key", i, insert_queue_new[j].first, insert_queue_new[j].second);7363undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", i, insert_queue_new[j].first);7364}73657366// Undo methods.7367undo_redo->add_undo_method(animation.ptr(), "track_set_interpolation_type", i, animation->track_get_interpolation_type(i));7368for (int j = key_len - 1; j >= 0; j--) {7369undo_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));7370}7371}7372}73737374undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);7375undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);7376undo_redo->add_do_method(this, "_redraw_tracks");7377undo_redo->add_undo_method(this, "_redraw_tracks");7378undo_redo->commit_action();7379_update_key_edit();73807381} break;73827383case EDIT_OPTIMIZE_ANIMATION: {7384optimize_dialog->popup_centered(Size2(250, 180) * EDSCALE);73857386} break;7387case EDIT_OPTIMIZE_ANIMATION_CONFIRM: {7388animation->optimize(optimize_velocity_error->get_value(), optimize_angular_error->get_value(), optimize_precision_error->get_value());7389_redraw_tracks();7390_update_key_edit();7391EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7392undo_redo->clear_history(undo_redo->get_history_id_for_object(animation.ptr()));7393undo_redo->clear_history(undo_redo->get_history_id_for_object(this));73947395} break;7396case EDIT_CLEAN_UP_ANIMATION: {7397cleanup_dialog->popup_centered(Size2(300, 0) * EDSCALE);73987399} break;7400case EDIT_CLEAN_UP_ANIMATION_CONFIRM: {7401if (cleanup_all->is_pressed()) {7402List<StringName> names;7403AnimationPlayerEditor::get_singleton()->get_player()->get_animation_list(&names);7404for (const StringName &E : names) {7405_cleanup_animation(AnimationPlayerEditor::get_singleton()->get_player()->get_animation(E));7406}7407} else {7408_cleanup_animation(animation);7409}74107411} break;7412}7413}74147415void AnimationTrackEditor::_cleanup_animation(Ref<Animation> p_animation) {7416_clear_selection();7417for (int i = 0; i < p_animation->get_track_count(); i++) {7418if (!root->has_node_and_resource(p_animation->track_get_path(i))) {7419continue;7420}7421Ref<Resource> res;7422Vector<StringName> leftover_path;7423Node *node = root->get_node_and_resource(p_animation->track_get_path(i), res, leftover_path);74247425bool prop_exists = false;7426Variant::Type valid_type = Variant::NIL;7427Object *obj = nullptr;74287429if (res.is_valid()) {7430obj = res.ptr();7431} else if (node) {7432obj = node;7433}74347435if (obj && p_animation->track_get_type(i) == Animation::TYPE_VALUE) {7436valid_type = obj->get_static_property_type_indexed(leftover_path, &prop_exists);7437}74387439if (!obj && cleanup_tracks->is_pressed()) {7440p_animation->remove_track(i);7441i--;7442continue;7443}74447445if (cleanup_keys_with_trimming_head->is_pressed()) {7446// Check is necessary because if there is already a key in position 0, it should not be replaced.7447if (p_animation->track_get_type(i) == Animation::TYPE_AUDIO && p_animation->track_find_key(i, 0, Animation::FIND_MODE_EXACT) < 0) {7448for (int j = 0; j < p_animation->track_get_key_count(i); j++) {7449double t = p_animation->track_get_key_time(i, j);7450if (t < 0) {7451if (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)) {7452Ref<AudioStream> stream = p_animation->audio_track_get_key_stream(i, j);7453double len = stream->get_length() - p_animation->audio_track_get_key_end_offset(i, j);7454double prev_offset = p_animation->audio_track_get_key_start_offset(i, j);7455double prev_time = p_animation->track_get_key_time(i, j);7456double diff = prev_offset - prev_time;7457if (diff >= len) {7458p_animation->track_remove_key(i, j);7459j--;7460continue;7461}7462p_animation->audio_track_set_key_start_offset(i, j, diff);7463p_animation->track_set_key_time(i, j, 0);7464} else {7465p_animation->track_remove_key(i, j);7466j--;7467}7468}7469}7470} else {7471for (int j = 0; j < p_animation->track_get_key_count(i); j++) {7472double t = p_animation->track_get_key_time(i, j);7473if (t < 0) {7474p_animation->track_remove_key(i, j);7475j--;7476}7477}7478}7479}74807481if (cleanup_keys_with_trimming_end->is_pressed()) {7482if (p_animation->track_get_type(i) == Animation::TYPE_AUDIO) {7483for (int j = 0; j < p_animation->track_get_key_count(i); j++) {7484double t = p_animation->track_get_key_time(i, j);7485if (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()))) {7486Ref<AudioStream> stream = animation->audio_track_get_key_stream(i, j);7487double len = stream->get_length() - animation->audio_track_get_key_start_offset(i, j);7488if (t + len < p_animation->get_length()) {7489continue;7490}7491double prev_time = animation->track_get_key_time(i, j);7492double diff = prev_time + len - p_animation->get_length();7493if (diff >= len) {7494p_animation->track_remove_key(i, j);7495j--;7496continue;7497}7498p_animation->audio_track_set_key_end_offset(i, j, diff);7499} else if (t > p_animation->get_length()) {7500p_animation->track_remove_key(i, j);7501j--;7502}7503}7504} else {7505for (int j = 0; j < p_animation->track_get_key_count(i); j++) {7506double t = p_animation->track_get_key_time(i, j);7507if (t > p_animation->get_length()) {7508p_animation->track_remove_key(i, j);7509j--;7510}7511}7512}7513}75147515if (!prop_exists || p_animation->track_get_type(i) != Animation::TYPE_VALUE || !cleanup_keys->is_pressed()) {7516continue;7517}75187519for (int j = 0; j < p_animation->track_get_key_count(i); j++) {7520Variant v = p_animation->track_get_key_value(i, j);75217522if (!Variant::can_convert(v.get_type(), valid_type)) {7523p_animation->track_remove_key(i, j);7524j--;7525}7526}75277528if (p_animation->track_get_key_count(i) == 0 && cleanup_tracks->is_pressed()) {7529p_animation->remove_track(i);7530i--;7531}7532}75337534EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();7535undo_redo->clear_history(undo_redo->get_history_id_for_object(animation.ptr()));7536undo_redo->clear_history(undo_redo->get_history_id_for_object(this));7537_update_tracks();7538}75397540void AnimationTrackEditor::_toggle_function_names() {7541_redraw_tracks();7542}75437544void AnimationTrackEditor::_view_group_toggle() {7545_update_tracks();7546view_group->set_button_icon(get_editor_theme_icon(view_group->is_pressed() ? SNAME("AnimationTrackList") : SNAME("AnimationTrackGroup")));7547bezier_edit->set_filtered(selected_filter->is_pressed());7548}75497550bool AnimationTrackEditor::is_grouping_tracks() {7551if (!view_group) {7552return false;7553}75547555return !view_group->is_pressed();7556}75577558bool AnimationTrackEditor::is_sorting_alphabetically() {7559return alphabetic_sorting->is_pressed();7560}75617562bool AnimationTrackEditor::is_function_name_pressed() {7563return function_name_toggler->is_pressed();7564}75657566void AnimationTrackEditor::_auto_fit() {7567timeline->auto_fit();7568}75697570void AnimationTrackEditor::_auto_fit_bezier() {7571timeline->auto_fit();75727573if (bezier_edit->is_visible()) {7574bezier_edit->auto_fit_vertically();7575}7576}75777578void AnimationTrackEditor::_selection_changed() {7579if (selected_filter->is_pressed()) {7580_update_tracks(); // Needs updating.7581} else {7582_redraw_tracks();7583_redraw_groups();7584}7585}75867587void AnimationTrackEditor::_update_snap_unit() {7588nearest_fps = 0;75897590if (step->get_value() <= 0) {7591snap_unit = 0;7592_update_nearest_fps_label();7593return; // Avoid zero div.7594}75957596if (timeline->is_using_fps()) {7597snap_unit = 1.0 / step->get_value();7598} else {7599if (fps_compat->is_pressed()) {7600snap_unit = CLAMP(step->get_value(), 0.0, 1.0);7601if (!Math::is_zero_approx(snap_unit)) {7602real_t fps = Math::round(1.0 / snap_unit);7603nearest_fps = int(fps);7604snap_unit = 1.0 / fps;7605}7606} else {7607snap_unit = step->get_value();7608}7609}7610_update_nearest_fps_label();7611}76127613float AnimationTrackEditor::snap_time(float p_value, bool p_relative) {7614if (is_snap_keys_enabled()) {7615double current_snap = snap_unit;7616if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) {7617// Use more precise snapping when holding Shift.7618current_snap *= 0.25;7619}76207621if (p_relative) {7622double rel = Math::fmod(timeline->get_value(), current_snap);7623p_value = Math::snapped(p_value + rel, current_snap) - rel;7624} else {7625p_value = Math::snapped(p_value, current_snap);7626}7627}76287629return p_value;7630}76317632float AnimationTrackEditor::get_snap_unit() {7633return snap_unit;7634}76357636void AnimationTrackEditor::_show_imported_anim_warning() {7637// It looks terrible on a single line but the TTR extractor doesn't support line breaks yet.7638EditorNode::get_singleton()->show_warning(7639TTR("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\"."));7640}76417642void AnimationTrackEditor::_show_dummy_player_warning() {7643EditorNode::get_singleton()->show_warning(7644TTR("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."));7645}76467647void AnimationTrackEditor::_show_inactive_player_warning() {7648EditorNode::get_singleton()->show_warning(7649TTR("AnimationPlayer is inactive. The playback will not be processed."));7650}76517652void AnimationTrackEditor::_select_all_tracks_for_copy() {7653TreeItem *track = track_copy_select->get_root()->get_first_child();7654if (!track) {7655return;7656}76577658bool all_selected = true;7659while (track) {7660if (!track->is_checked(0)) {7661all_selected = false;7662}76637664track = track->get_next();7665}76667667track = track_copy_select->get_root()->get_first_child();7668while (track) {7669track->set_checked(0, !all_selected);7670track = track->get_next();7671}7672}76737674void AnimationTrackEditor::_bind_methods() {7675ClassDB::bind_method("_track_grab_focus", &AnimationTrackEditor::_track_grab_focus);7676ClassDB::bind_method("_redraw_tracks", &AnimationTrackEditor::_redraw_tracks);7677ClassDB::bind_method("_clear_selection_for_anim", &AnimationTrackEditor::_clear_selection_for_anim);7678ClassDB::bind_method("_select_at_anim", &AnimationTrackEditor::_select_at_anim);7679ClassDB::bind_method("_clear_selection", &AnimationTrackEditor::_clear_selection);76807681ClassDB::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));7682ClassDB::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));76837684ADD_SIGNAL(MethodInfo("timeline_changed", PropertyInfo(Variant::FLOAT, "position"), PropertyInfo(Variant::BOOL, "timeline_only"), PropertyInfo(Variant::BOOL, "update_position_only")));7685ADD_SIGNAL(MethodInfo("keying_changed"));7686ADD_SIGNAL(MethodInfo("animation_len_changed", PropertyInfo(Variant::FLOAT, "len")));7687ADD_SIGNAL(MethodInfo("animation_step_changed", PropertyInfo(Variant::FLOAT, "step")));7688}76897690void AnimationTrackEditor::_pick_track_filter_text_changed(const String &p_newtext) {7691TreeItem *root_item = pick_track->get_scene_tree()->get_scene_tree()->get_root();76927693Vector<Node *> select_candidates;7694Node *to_select = nullptr;76957696String filter = pick_track->get_filter_line_edit()->get_text();76977698_pick_track_select_recursive(root_item, filter, select_candidates);76997700if (!select_candidates.is_empty()) {7701for (int i = 0; i < select_candidates.size(); ++i) {7702Node *candidate = select_candidates[i];77037704if (((String)candidate->get_name()).to_lower().begins_with(filter.to_lower())) {7705to_select = candidate;7706break;7707}7708}77097710if (!to_select) {7711to_select = select_candidates[0];7712}7713}77147715pick_track->get_scene_tree()->set_selected(to_select);7716}77177718void AnimationTrackEditor::_pick_track_select_recursive(TreeItem *p_item, const String &p_filter, Vector<Node *> &p_select_candidates) {7719if (!p_item) {7720return;7721}77227723NodePath np = p_item->get_metadata(0);7724Node *node = get_node_or_null(np);77257726if (node && !p_filter.is_empty() && ((String)node->get_name()).containsn(p_filter)) {7727p_select_candidates.push_back(node);7728}77297730TreeItem *c = p_item->get_first_child();77317732while (c) {7733_pick_track_select_recursive(c, p_filter, p_select_candidates);7734c = c->get_next();7735}7736}77377738AnimationTrackEditor::AnimationTrackEditor() {7739main_panel = memnew(PanelContainer);7740main_panel->set_focus_mode(FOCUS_ALL); // Allow panel to have focus so that shortcuts work as expected.7741add_child(main_panel);7742main_panel->set_v_size_flags(SIZE_EXPAND_FILL);7743HBoxContainer *timeline_scroll = memnew(HBoxContainer);7744main_panel->add_child(timeline_scroll);7745timeline_scroll->set_v_size_flags(SIZE_EXPAND_FILL);77467747timeline_vbox = memnew(VBoxContainer);7748timeline_scroll->add_child(timeline_vbox);7749timeline_vbox->set_v_size_flags(SIZE_EXPAND_FILL);7750timeline_vbox->set_h_size_flags(SIZE_EXPAND_FILL);77517752info_message = memnew(Label);7753info_message->set_focus_mode(FOCUS_ACCESSIBILITY);7754info_message->set_text(TTR("Select an AnimationPlayer node to create and edit animations."));7755info_message->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);7756info_message->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);7757info_message->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);7758info_message->set_custom_minimum_size(Size2(100 * EDSCALE, 0));7759info_message->set_anchors_and_offsets_preset(PRESET_FULL_RECT, PRESET_MODE_KEEP_SIZE, 8 * EDSCALE);7760main_panel->add_child(info_message);77617762timeline = memnew(AnimationTimelineEdit);7763timeline_vbox->add_child(timeline);7764timeline->connect("timeline_changed", callable_mp(this, &AnimationTrackEditor::_timeline_changed));7765timeline->connect("name_limit_changed", callable_mp(this, &AnimationTrackEditor::_name_limit_changed));7766timeline->connect("track_added", callable_mp(this, &AnimationTrackEditor::_add_track));7767timeline->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_timeline_value_changed));7768timeline->connect("length_changed", callable_mp(this, &AnimationTrackEditor::_update_length));7769timeline->connect("filter_changed", callable_mp(this, &AnimationTrackEditor::_update_tracks));77707771panner.instantiate();7772panner->set_scroll_zoom_factor(AnimationTimelineEdit::SCROLL_ZOOM_FACTOR_IN);7773panner->set_callbacks(callable_mp(this, &AnimationTrackEditor::_pan_callback), callable_mp(this, &AnimationTrackEditor::_zoom_callback));77747775box_selection_container = memnew(Control);7776box_selection_container->set_v_size_flags(SIZE_EXPAND_FILL);7777box_selection_container->set_clip_contents(true);7778timeline_vbox->add_child(box_selection_container);77797780marker_edit = memnew(AnimationMarkerEdit);7781timeline->get_child(0)->add_child(marker_edit);7782marker_edit->set_editor(this);7783marker_edit->set_timeline(timeline);7784marker_edit->set_h_size_flags(SIZE_EXPAND_FILL);7785marker_edit->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT);7786marker_edit->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_redraw_groups));7787marker_edit->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_redraw_tracks));77887789scroll = memnew(ScrollContainer);7790box_selection_container->add_child(scroll);7791scroll->set_anchors_and_offsets_preset(PRESET_FULL_RECT);77927793VScrollBar *sb = scroll->get_v_scroll_bar();7794scroll->remove_child(sb);7795timeline_scroll->add_child(sb); // Move here so timeline and tracks are always aligned.7796scroll->set_focus_mode(FOCUS_CLICK);7797scroll->connect(SceneStringName(gui_input), callable_mp(this, &AnimationTrackEditor::_scroll_input));7798scroll->connect(SceneStringName(focus_exited), callable_mp(panner.ptr(), &ViewPanner::release_pan_key));77997800scroll->get_v_scroll_bar()->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_v_scroll_changed));7801scroll->get_h_scroll_bar()->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_h_scroll_changed));78027803bezier_edit = memnew(AnimationBezierTrackEdit);7804timeline_vbox->add_child(bezier_edit);7805bezier_edit->set_editor(this);7806bezier_edit->set_timeline(timeline);7807bezier_edit->hide();7808bezier_edit->set_v_size_flags(SIZE_EXPAND_FILL);7809bezier_edit->connect("timeline_changed", callable_mp(this, &AnimationTrackEditor::_timeline_changed));78107811timeline_vbox->set_custom_minimum_size(Size2(0, 150) * EDSCALE);78127813hscroll = memnew(HScrollBar);7814hscroll->share(timeline);7815hscroll->hide();7816hscroll->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_update_scroll));7817timeline_vbox->add_child(hscroll);7818timeline->set_hscroll(hscroll);78197820track_vbox = memnew(VBoxContainer);7821scroll->add_child(track_vbox);7822track_vbox->set_h_size_flags(SIZE_EXPAND_FILL);7823scroll->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);78247825HFlowContainer *bottom_hf = memnew(HFlowContainer);7826add_child(bottom_hf);78277828imported_anim_warning = memnew(Button);7829imported_anim_warning->hide();7830imported_anim_warning->set_text(TTR("Imported Scene"));7831imported_anim_warning->set_tooltip_text(TTR("Warning: Editing imported animation"));7832imported_anim_warning->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_show_imported_anim_warning));7833bottom_hf->add_child(imported_anim_warning);78347835dummy_player_warning = memnew(Button);7836dummy_player_warning->hide();7837dummy_player_warning->set_text(TTR("Dummy Player"));7838dummy_player_warning->set_tooltip_text(TTR("Warning: Editing dummy AnimationPlayer"));7839dummy_player_warning->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_show_dummy_player_warning));7840bottom_hf->add_child(dummy_player_warning);78417842inactive_player_warning = memnew(Button);7843inactive_player_warning->hide();7844inactive_player_warning->set_text(TTR("Inactive Player"));7845inactive_player_warning->set_tooltip_text(TTR("Warning: AnimationPlayer is inactive"));7846inactive_player_warning->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_show_inactive_player_warning));7847bottom_hf->add_child(inactive_player_warning);78487849Control *spacer = memnew(Control);7850spacer->set_mouse_filter(MOUSE_FILTER_PASS);7851spacer->set_h_size_flags(SIZE_EXPAND_FILL);7852bottom_hf->add_child(spacer);78537854Label *bezier_key_default_label = memnew(Label);7855bezier_key_default_label->set_text(TTR("Bezier Default Mode:"));7856bottom_hf->add_child(bezier_key_default_label);78577858bezier_key_mode = memnew(OptionButton);7859bezier_key_mode->add_item(TTR("Free"));7860bezier_key_mode->add_item(TTR("Linear"));7861bezier_key_mode->add_item(TTR("Balanced"));7862bezier_key_mode->add_item(TTR("Mirrored"));7863bezier_key_mode->set_tooltip_text(TTR("Set the default behavior of new bezier keys."));7864bezier_key_mode->select(2);78657866bottom_hf->add_child(bezier_key_mode);7867bottom_hf->add_child(memnew(VSeparator));78687869bezier_edit_icon = memnew(Button);7870bezier_edit_icon->set_flat(true);7871bezier_edit_icon->set_disabled(true);7872bezier_edit_icon->set_toggle_mode(true);7873bezier_edit_icon->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_toggle_bezier_edit));7874bezier_edit_icon->set_tooltip_text(TTR("Toggle between the bezier curve editor and track editor."));78757876bottom_hf->add_child(bezier_edit_icon);78777878function_name_toggler = memnew(Button);7879function_name_toggler->set_flat(true);7880function_name_toggler->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_toggle_function_names));7881function_name_toggler->set_shortcut(ED_SHORTCUT("animation_editor/toggle_function_names", TTRC("Toggle method names")));7882function_name_toggler->set_toggle_mode(true);7883function_name_toggler->set_shortcut_in_tooltip(false);7884function_name_toggler->set_tooltip_text(TTRC("Toggle function names in the track editor."));78857886bottom_hf->add_child(function_name_toggler);78877888selected_filter = memnew(Button);7889selected_filter->set_flat(true);7890selected_filter->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_view_group_toggle)); // Same function works the same.7891selected_filter->set_toggle_mode(true);7892selected_filter->set_tooltip_text(TTR("Only show tracks from nodes selected in tree."));78937894bottom_hf->add_child(selected_filter);78957896alphabetic_sorting = memnew(Button);7897alphabetic_sorting->set_flat(true);7898alphabetic_sorting->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_update_tracks));7899alphabetic_sorting->set_toggle_mode(true);7900alphabetic_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."));79017902bottom_hf->add_child(alphabetic_sorting);79037904view_group = memnew(Button);7905view_group->set_flat(true);7906view_group->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_view_group_toggle));7907view_group->set_toggle_mode(true);7908view_group->set_tooltip_text(TTR("Group tracks by node or display them as plain list."));79097910bottom_hf->add_child(view_group);7911bottom_hf->add_child(memnew(VSeparator));79127913snap_timeline = memnew(Button);7914snap_timeline->set_flat(true);7915bottom_hf->add_child(snap_timeline);7916snap_timeline->set_disabled(true);7917snap_timeline->set_toggle_mode(true);7918snap_timeline->set_pressed(false);7919snap_timeline->set_tooltip_text(TTR("Apply snapping to timeline cursor."));79207921snap_keys = memnew(Button);7922snap_keys->set_flat(true);7923bottom_hf->add_child(snap_keys);7924snap_keys->set_disabled(true);7925snap_keys->set_toggle_mode(true);7926snap_keys->set_pressed(true);7927snap_keys->set_tooltip_text(TTR("Apply snapping to selected key(s)."));79287929fps_compat = memnew(Button);7930fps_compat->set_flat(true);7931bottom_hf->add_child(fps_compat);7932fps_compat->set_disabled(true);7933fps_compat->set_toggle_mode(true);7934fps_compat->set_pressed(true);7935fps_compat->set_tooltip_text(TTR("Apply snapping to the nearest integer FPS."));7936fps_compat->connect(SceneStringName(toggled), callable_mp(this, &AnimationTrackEditor::_update_fps_compat_mode));79377938nearest_fps_label = memnew(Label);7939nearest_fps_label->set_focus_mode(FOCUS_ACCESSIBILITY);7940nearest_fps_label->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);7941bottom_hf->add_child(nearest_fps_label);79427943step = memnew(EditorSpinSlider);7944step->set_min(0);7945step->set_max(1000000);7946step->set_step(SECOND_DECIMAL);7947step->set_hide_slider(true);7948step->set_custom_minimum_size(Size2(100, 0) * EDSCALE);7949step->set_tooltip_text(TTR("Animation step value."));7950step->set_accessibility_name(TTRC("Animation step value."));7951bottom_hf->add_child(step);7952step->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_update_step));7953step->set_read_only(true);79547955snap_mode = memnew(OptionButton);7956snap_mode->add_item(TTR("Seconds"));7957snap_mode->add_item(TTR("FPS"));7958snap_mode->set_accessibility_name(TTRC("Snap Mode"));7959snap_mode->set_disabled(true);7960bottom_hf->add_child(snap_mode);7961snap_mode->connect(SceneStringName(item_selected), callable_mp(this, &AnimationTrackEditor::_snap_mode_changed));79627963bottom_hf->add_child(memnew(VSeparator));79647965HBoxContainer *zoom_hb = memnew(HBoxContainer);7966zoom_icon = memnew(TextureRect);7967zoom_icon->set_v_size_flags(SIZE_SHRINK_CENTER);7968zoom_hb->add_child(zoom_icon);7969zoom = memnew(HSlider);7970zoom->set_step(0.01);7971zoom->set_min(0.0);7972zoom->set_max(2.0);7973zoom->set_value(1.0);7974zoom->set_custom_minimum_size(Size2(200, 0) * EDSCALE);7975zoom->set_v_size_flags(SIZE_SHRINK_CENTER);7976zoom->set_accessibility_name(TTRC("Zoom"));7977zoom_hb->add_child(zoom);7978bottom_hf->add_child(zoom_hb);7979timeline->set_zoom(zoom);79807981ED_SHORTCUT("animation_editor/auto_fit", TTRC("Fit to panel"), KeyModifierMask::ALT | Key::F);79827983auto_fit = memnew(Button);7984auto_fit->set_flat(true);7985auto_fit->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_auto_fit));7986auto_fit->set_shortcut(ED_GET_SHORTCUT("animation_editor/auto_fit"));7987auto_fit->set_accessibility_name(TTRC("Auto Fit"));7988bottom_hf->add_child(auto_fit);79897990auto_fit_bezier = memnew(Button);7991auto_fit_bezier->set_flat(true);7992auto_fit_bezier->set_visible(false);7993auto_fit_bezier->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_auto_fit_bezier));7994auto_fit_bezier->set_shortcut(ED_GET_SHORTCUT("animation_editor/auto_fit"));7995auto_fit_bezier->set_accessibility_name(TTRC("Auto Fit Bezier"));7996bottom_hf->add_child(auto_fit_bezier);79977998edit = memnew(MenuButton);7999edit->set_shortcut_context(this);8000edit->set_text(TTR("Edit"));8001edit->set_flat(false);8002edit->set_disabled(true);8003edit->set_tooltip_text(TTR("Animation properties."));8004edit->set_accessibility_name(TTRC("Animation properties."));8005edit->get_popup()->add_item(TTR("Copy Tracks..."), EDIT_COPY_TRACKS);8006edit->get_popup()->add_item(TTR("Paste Tracks"), EDIT_PASTE_TRACKS);8007edit->get_popup()->add_separator();8008edit->get_popup()->add_item(TTR("Scale Selection..."), EDIT_SCALE_SELECTION);8009edit->get_popup()->add_item(TTR("Scale From Cursor..."), EDIT_SCALE_FROM_CURSOR);8010edit->get_popup()->add_separator();8011edit->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);8012edit->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);8013edit->get_popup()->add_separator();8014edit->get_popup()->add_item(TTR("Make Easing Selection..."), EDIT_EASE_SELECTION);8015edit->get_popup()->add_separator();8016edit->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);8017edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/cut_selected_keys", TTRC("Cut Selected Keys"), KeyModifierMask::CMD_OR_CTRL | Key::X), EDIT_CUT_KEYS);8018edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/copy_selected_keys", TTRC("Copy Selected Keys"), KeyModifierMask::CMD_OR_CTRL | Key::C), EDIT_COPY_KEYS);8019edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/paste_keys", TTRC("Paste Keys"), KeyModifierMask::CMD_OR_CTRL | Key::V), EDIT_PASTE_KEYS);8020edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/add_reset_value", TTRC("Add RESET Value(s)")));8021edit->get_popup()->add_separator();8022edit->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);8023edit->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);8024edit->get_popup()->add_separator();8025edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/delete_selection", TTRC("Delete Selection"), Key::KEY_DELETE), EDIT_DELETE_SELECTION);80268027edit->get_popup()->add_separator();8028edit->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);8029edit->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);8030edit->get_popup()->add_separator();8031edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/apply_reset", TTRC("Apply Reset")), EDIT_APPLY_RESET);8032edit->get_popup()->add_separator();8033edit->get_popup()->add_item(TTR("Bake Animation..."), EDIT_BAKE_ANIMATION);8034edit->get_popup()->add_item(TTR("Optimize Animation (no undo)..."), EDIT_OPTIMIZE_ANIMATION);8035edit->get_popup()->add_item(TTR("Clean-Up Animation (no undo)..."), EDIT_CLEAN_UP_ANIMATION);80368037edit->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed));8038edit->get_popup()->connect("about_to_popup", callable_mp(this, &AnimationTrackEditor::_edit_menu_about_to_popup));80398040pick_track = memnew(SceneTreeDialog);8041add_child(pick_track);8042pick_track->set_title(TTR("Pick a node to animate:"));8043pick_track->connect("selected", callable_mp(this, &AnimationTrackEditor::_new_track_node_selected));8044pick_track->get_filter_line_edit()->connect(SceneStringName(text_changed), callable_mp(this, &AnimationTrackEditor::_pick_track_filter_text_changed));80458046prop_selector = memnew(PropertySelector);8047add_child(prop_selector);8048prop_selector->connect("selected", callable_mp(this, &AnimationTrackEditor::_new_track_property_selected));8049prop_selector->set_accessibility_name(TTRC("Track Property"));80508051method_selector = memnew(PropertySelector);8052add_child(method_selector);8053method_selector->connect("selected", callable_mp(this, &AnimationTrackEditor::_add_method_key));8054method_selector->set_accessibility_name(TTRC("Method Key"));80558056insert_confirm = memnew(ConfirmationDialog);8057add_child(insert_confirm);8058insert_confirm->connect(SceneStringName(confirmed), callable_mp(this, &AnimationTrackEditor::_confirm_insert_list));8059VBoxContainer *icvb = memnew(VBoxContainer);8060insert_confirm->add_child(icvb);8061insert_confirm_text = memnew(Label);8062insert_confirm_text->set_focus_mode(FOCUS_ACCESSIBILITY);8063icvb->add_child(insert_confirm_text);8064HBoxContainer *ichb = memnew(HBoxContainer);8065icvb->add_child(ichb);8066insert_confirm_bezier = memnew(CheckBox);8067insert_confirm_bezier->set_text(TTR("Use Bezier Curves"));8068insert_confirm_bezier->set_pressed(EDITOR_GET("editors/animation/default_create_bezier_tracks"));8069ichb->add_child(insert_confirm_bezier);8070insert_confirm_reset = memnew(CheckBox);8071insert_confirm_reset->set_text(TTR("Create RESET Track(s)", ""));8072insert_confirm_reset->set_pressed(EDITOR_GET("editors/animation/default_create_reset_tracks"));8073ichb->add_child(insert_confirm_reset);80748075box_selection = memnew(Control);8076box_selection_container->add_child(box_selection);8077box_selection->set_mouse_filter(MOUSE_FILTER_IGNORE);8078box_selection->hide();8079box_selection->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_box_selection_draw));80808081// Default Plugins.80828083Ref<AnimationTrackEditDefaultPlugin> def_plugin;8084def_plugin.instantiate();8085add_track_edit_plugin(def_plugin);80868087// Dialogs.80888089optimize_dialog = memnew(ConfirmationDialog);8090add_child(optimize_dialog);8091optimize_dialog->set_title(TTR("Animation Optimizer"));8092VBoxContainer *optimize_vb = memnew(VBoxContainer);8093optimize_dialog->add_child(optimize_vb);80948095optimize_velocity_error = memnew(SpinBox);8096optimize_velocity_error->set_max(1.0);8097optimize_velocity_error->set_min(0.001);8098optimize_velocity_error->set_step(0.001);8099optimize_velocity_error->set_value(0.01);8100optimize_velocity_error->set_accessibility_name(TTRC("Max Velocity Error:"));8101optimize_vb->add_margin_child(TTR("Max Velocity Error:"), optimize_velocity_error);8102optimize_angular_error = memnew(SpinBox);8103optimize_angular_error->set_max(1.0);8104optimize_angular_error->set_min(0.001);8105optimize_angular_error->set_step(0.001);8106optimize_angular_error->set_value(0.01);8107optimize_angular_error->set_accessibility_name(TTRC("Max Angular Error:"));8108optimize_vb->add_margin_child(TTR("Max Angular Error:"), optimize_angular_error);8109optimize_precision_error = memnew(SpinBox);8110optimize_precision_error->set_max(6);8111optimize_precision_error->set_min(1);8112optimize_precision_error->set_step(1);8113optimize_precision_error->set_value(3);8114optimize_precision_error->set_accessibility_name(TTRC("Max Precision Error:"));8115optimize_vb->add_margin_child(TTR("Max Precision Error:"), optimize_precision_error);81168117optimize_dialog->set_ok_button_text(TTR("Optimize"));8118optimize_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_OPTIMIZE_ANIMATION_CONFIRM));81198120//8121cleanup_dialog = memnew(ConfirmationDialog);8122add_child(cleanup_dialog);8123VBoxContainer *cleanup_vb = memnew(VBoxContainer);8124cleanup_dialog->add_child(cleanup_vb);81258126cleanup_keys_with_trimming_head = memnew(CheckBox);8127cleanup_keys_with_trimming_head->set_text(TTR("Trim keys placed in negative time"));8128cleanup_keys_with_trimming_head->set_pressed(true);8129cleanup_vb->add_child(cleanup_keys_with_trimming_head);81308131cleanup_keys_with_trimming_end = memnew(CheckBox);8132cleanup_keys_with_trimming_end->set_text(TTR("Trim keys placed exceed the animation length"));8133cleanup_keys_with_trimming_end->set_pressed(true);8134cleanup_vb->add_child(cleanup_keys_with_trimming_end);81358136cleanup_keys = memnew(CheckBox);8137cleanup_keys->set_text(TTR("Remove invalid keys"));8138cleanup_keys->set_pressed(true);8139cleanup_vb->add_child(cleanup_keys);81408141cleanup_tracks = memnew(CheckBox);8142cleanup_tracks->set_text(TTR("Remove unresolved and empty tracks"));8143cleanup_tracks->set_pressed(true);8144cleanup_vb->add_child(cleanup_tracks);81458146cleanup_all = memnew(CheckBox);8147cleanup_all->set_text(TTR("Clean-up all animations"));8148cleanup_vb->add_child(cleanup_all);81498150cleanup_dialog->set_title(TTR("Clean-Up Animation(s) (NO UNDO!)"));8151cleanup_dialog->set_ok_button_text(TTR("Clean-Up"));81528153cleanup_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_CLEAN_UP_ANIMATION_CONFIRM));81548155//8156scale_dialog = memnew(ConfirmationDialog);8157VBoxContainer *vbc = memnew(VBoxContainer);8158scale_dialog->add_child(vbc);81598160scale = memnew(SpinBox);8161scale->set_min(-99999);8162scale->set_max(99999);8163scale->set_step(0.001);8164scale->set_select_all_on_focus(true);8165scale->set_accessibility_name(TTRC("Scale Ratio"));8166vbc->add_margin_child(TTR("Scale Ratio:"), scale);8167scale_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_SCALE_CONFIRM), CONNECT_DEFERRED);8168add_child(scale_dialog);81698170scale_dialog->register_text_enter(scale->get_line_edit());81718172//8173ease_dialog = memnew(ConfirmationDialog);8174ease_dialog->set_title(TTR("Select Transition and Easing"));8175ease_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_EASE_CONFIRM));8176add_child(ease_dialog);8177GridContainer *ease_grid = memnew(GridContainer);8178ease_grid->set_columns(2);8179ease_dialog->add_child(ease_grid);8180transition_selection = memnew(OptionButton);8181transition_selection->set_accessibility_name(TTRC("Transition Type:"));8182transition_selection->add_item(TTR("Linear", "Transition Type"), Tween::TRANS_LINEAR);8183transition_selection->add_item(TTR("Sine", "Transition Type"), Tween::TRANS_SINE);8184transition_selection->add_item(TTR("Quint", "Transition Type"), Tween::TRANS_QUINT);8185transition_selection->add_item(TTR("Quart", "Transition Type"), Tween::TRANS_QUART);8186transition_selection->add_item(TTR("Quad", "Transition Type"), Tween::TRANS_QUAD);8187transition_selection->add_item(TTR("Expo", "Transition Type"), Tween::TRANS_EXPO);8188transition_selection->add_item(TTR("Elastic", "Transition Type"), Tween::TRANS_ELASTIC);8189transition_selection->add_item(TTR("Cubic", "Transition Type"), Tween::TRANS_CUBIC);8190transition_selection->add_item(TTR("Circ", "Transition Type"), Tween::TRANS_CIRC);8191transition_selection->add_item(TTR("Bounce", "Transition Type"), Tween::TRANS_BOUNCE);8192transition_selection->add_item(TTR("Back", "Transition Type"), Tween::TRANS_BACK);8193transition_selection->add_item(TTR("Spring", "Transition Type"), Tween::TRANS_SPRING);8194transition_selection->select(Tween::TRANS_LINEAR); // Default8195transition_selection->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); // Translation context is needed.8196ease_selection = memnew(OptionButton);8197ease_selection->set_accessibility_name(TTRC("Ease Type:"));8198ease_selection->add_item(TTR("In", "Ease Type"), Tween::EASE_IN);8199ease_selection->add_item(TTR("Out", "Ease Type"), Tween::EASE_OUT);8200ease_selection->add_item(TTR("InOut", "Ease Type"), Tween::EASE_IN_OUT);8201ease_selection->add_item(TTR("OutIn", "Ease Type"), Tween::EASE_OUT_IN);8202ease_selection->select(Tween::EASE_IN_OUT); // Default8203ease_selection->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); // Translation context is needed.8204ease_fps = memnew(SpinBox);8205ease_fps->set_min(FPS_DECIMAL);8206ease_fps->set_max(999);8207ease_fps->set_step(FPS_DECIMAL);8208ease_fps->set_value(30); // Default8209ease_fps->set_accessibility_name(TTRC("FPS:"));8210ease_grid->add_child(memnew(Label(TTR("Transition Type:"))));8211ease_grid->add_child(transition_selection);8212ease_grid->add_child(memnew(Label(TTR("Ease Type:"))));8213ease_grid->add_child(ease_selection);8214ease_grid->add_child(memnew(Label(TTR("FPS:"))));8215ease_grid->add_child(ease_fps);82168217//8218bake_dialog = memnew(ConfirmationDialog);8219bake_dialog->set_title(TTR("Animation Baker"));8220bake_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_BAKE_ANIMATION_CONFIRM));8221add_child(bake_dialog);8222GridContainer *bake_grid = memnew(GridContainer);8223bake_grid->set_columns(2);8224bake_dialog->add_child(bake_grid);8225bake_trs = memnew(CheckBox);8226bake_trs->set_accessibility_name(TTRC("3D Pos/Rot/Scl Track:"));8227bake_trs->set_pressed(true);8228bake_blendshape = memnew(CheckBox);8229bake_blendshape->set_accessibility_name(TTRC("Blendshape Track:"));8230bake_blendshape->set_pressed(true);8231bake_value = memnew(CheckBox);8232bake_value->set_accessibility_name(TTRC("Value Track:"));8233bake_value->set_pressed(true);8234bake_fps = memnew(SpinBox);8235bake_fps->set_accessibility_name(TTRC("FPS:"));8236bake_fps->set_min(FPS_DECIMAL);8237bake_fps->set_max(999);8238bake_fps->set_step(FPS_DECIMAL);8239bake_fps->set_value(30); // Default8240bake_grid->add_child(memnew(Label(TTR("3D Pos/Rot/Scl Track:"))));8241bake_grid->add_child(bake_trs);8242bake_grid->add_child(memnew(Label(TTR("Blendshape Track:"))));8243bake_grid->add_child(bake_blendshape);8244bake_grid->add_child(memnew(Label(TTR("Value Track:"))));8245bake_grid->add_child(bake_value);8246bake_grid->add_child(memnew(Label(TTR("FPS:"))));8247bake_grid->add_child(bake_fps);82488249track_copy_dialog = memnew(ConfirmationDialog);8250add_child(track_copy_dialog);8251track_copy_dialog->set_title(TTR("Select Tracks to Copy"));8252track_copy_dialog->set_ok_button_text(TTR("Copy"));82538254VBoxContainer *track_copy_vbox = memnew(VBoxContainer);8255track_copy_dialog->add_child(track_copy_vbox);82568257Button *select_all_button = memnew(Button);8258select_all_button->set_text(TTR("Select All/None"));8259select_all_button->connect(SceneStringName(pressed), callable_mp(this, &AnimationTrackEditor::_select_all_tracks_for_copy));8260track_copy_vbox->add_child(select_all_button);82618262track_copy_select = memnew(Tree);8263track_copy_select->set_accessibility_name(TTRC("Copy Selection"));8264track_copy_select->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);8265track_copy_select->set_h_size_flags(SIZE_EXPAND_FILL);8266track_copy_select->set_v_size_flags(SIZE_EXPAND_FILL);8267track_copy_select->set_hide_root(true);8268track_copy_vbox->add_child(track_copy_select);8269track_copy_dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_COPY_TRACKS_CONFIRM));8270}82718272AnimationTrackEditor::~AnimationTrackEditor() {8273if (key_edit) {8274memdelete(key_edit);8275}8276if (multi_key_edit) {8277memdelete(multi_key_edit);8278}8279}82808281// AnimationTrackKeyEditEditorPlugin.82828283void AnimationTrackKeyEditEditor::_time_edit_spun() {8284_time_edit_entered();8285_time_edit_exited();8286}82878288void AnimationTrackKeyEditEditor::_time_edit_entered() {8289int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX);8290if (key == -1) {8291return;8292}8293key_data_cache.time = animation->track_get_key_time(track, key);8294key_data_cache.transition = animation->track_get_key_transition(track, key);8295key_data_cache.value = animation->track_get_key_value(track, key);8296}82978298void AnimationTrackKeyEditEditor::_time_edit_exited() {8299real_t new_time = spinner->get_value();83008301if (use_fps) {8302real_t fps = animation->get_step();8303if (fps > 0) {8304fps = 1.0 / fps;8305}8306new_time /= fps;8307}83088309if (Math::is_equal_approx(new_time, key_data_cache.time)) {8310return; // No change.8311}83128313int existing = animation->track_find_key(track, new_time, Animation::FIND_MODE_APPROX);8314EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();8315undo_redo->create_action(TTR("Animation Change Keyframe Time"));83168317if (existing != -1) {8318undo_redo->add_do_method(animation.ptr(), "track_remove_key_at_time", track, animation->track_get_key_time(track, existing));8319}8320undo_redo->add_do_method(animation.ptr(), "track_remove_key_at_time", track, key_data_cache.time);8321undo_redo->add_do_method(animation.ptr(), "track_insert_key", track, new_time, key_data_cache.value, key_data_cache.transition);8322undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", track, new_time);8323undo_redo->add_undo_method(animation.ptr(), "track_insert_key", track, key_data_cache.time, key_data_cache.value, key_data_cache.transition);8324if (existing != -1) {8325undo_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));8326}83278328// Reselect key.8329AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();8330if (ape) {8331AnimationTrackEditor *ate = ape->get_track_editor();8332if (ate) {8333undo_redo->add_do_method(ate, "_clear_selection_for_anim", animation);8334undo_redo->add_undo_method(ate, "_clear_selection_for_anim", animation);8335undo_redo->add_do_method(ate, "_select_at_anim", animation, track, new_time);8336undo_redo->add_undo_method(ate, "_select_at_anim", animation, track, key_data_cache.time);8337}8338undo_redo->add_do_method(ape, "_animation_update_key_frame");8339undo_redo->add_undo_method(ape, "_animation_update_key_frame");8340}83418342undo_redo->commit_action();8343}83448345AnimationTrackKeyEditEditor::AnimationTrackKeyEditEditor(Ref<Animation> p_animation, int p_track, real_t p_key_ofs, bool p_use_fps) {8346if (p_animation.is_null()) {8347return;8348}83498350animation = p_animation;8351track = p_track;8352key_ofs = p_key_ofs;8353use_fps = p_use_fps;83548355set_label("Time");83568357spinner = memnew(EditorSpinSlider);8358spinner->set_focus_mode(Control::FOCUS_CLICK);8359spinner->set_min(0);8360spinner->set_allow_greater(true);8361spinner->set_allow_lesser(true);8362add_child(spinner);83638364if (use_fps) {8365spinner->set_step(FPS_DECIMAL);8366real_t fps = animation->get_step();8367if (fps > 0) {8368fps = 1.0 / fps;8369}8370spinner->set_value(key_ofs * fps);8371spinner->connect("updown_pressed", callable_mp(this, &AnimationTrackKeyEditEditor::_time_edit_spun), CONNECT_DEFERRED);8372} else {8373spinner->set_step(SECOND_DECIMAL);8374spinner->set_value(key_ofs);8375spinner->set_max(animation->get_length());8376}83778378spinner->connect("grabbed", callable_mp(this, &AnimationTrackKeyEditEditor::_time_edit_entered), CONNECT_DEFERRED);8379spinner->connect("ungrabbed", callable_mp(this, &AnimationTrackKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED);8380spinner->connect("value_focus_entered", callable_mp(this, &AnimationTrackKeyEditEditor::_time_edit_entered), CONNECT_DEFERRED);8381spinner->connect("value_focus_exited", callable_mp(this, &AnimationTrackKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED);8382}83838384void AnimationMarkerEdit::_zoom_changed() {8385queue_redraw();8386play_position->queue_redraw();8387}83888389void AnimationMarkerEdit::_menu_selected(int p_index) {8390switch (p_index) {8391case MENU_KEY_INSERT: {8392_insert_marker(insert_at_pos);8393} break;8394case MENU_KEY_RENAME: {8395if (selection.size() > 0) {8396_rename_marker(*selection.last());8397}8398} break;8399case MENU_KEY_DELETE: {8400_delete_selected_markers();8401} break;8402case MENU_KEY_TOGGLE_MARKER_NAMES: {8403should_show_all_marker_names = !should_show_all_marker_names;8404queue_redraw();8405} break;8406}8407}84088409void AnimationMarkerEdit::_play_position_draw() {8410if (animation.is_null() || play_position_pos < 0) {8411return;8412}84138414float scale = timeline->get_zoom_scale();8415int h = get_size().height;84168417int px = (play_position_pos - timeline->get_value()) * scale + timeline->get_name_limit();84188419if (px >= timeline->get_name_limit() && px < (get_size().width - timeline->get_buttons_width())) {8420Color color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));8421play_position->draw_line(Point2(px, 0), Point2(px, h), color, Math::round(2 * EDSCALE));8422}8423}84248425bool AnimationMarkerEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable) {8426int limit = timeline->get_name_limit();8427int limit_end = get_size().width - timeline->get_buttons_width();8428// Left Border including space occupied by keyframes on t=0.8429int limit_start_hitbox = limit - type_icon->get_width();84308431if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) {8432int key_idx = -1;8433float key_distance = 1e20;8434PackedStringArray names = animation->get_marker_names();8435for (int i = 0; i < names.size(); i++) {8436Rect2 rect = const_cast<AnimationMarkerEdit *>(this)->get_key_rect(timeline->get_zoom_scale());8437float offset = animation->get_marker_time(names[i]) - timeline->get_value();8438offset = offset * timeline->get_zoom_scale() + limit;8439rect.position.x += offset;8440if (rect.has_point(p_pos)) {8441if (const_cast<AnimationMarkerEdit *>(this)->is_key_selectable_by_distance()) {8442float distance = Math::abs(offset - p_pos.x);8443if (key_idx == -1 || distance < key_distance) {8444key_idx = i;8445key_distance = distance;8446}8447} else {8448// First one does it.8449break;8450}8451}8452}84538454if (key_idx != -1) {8455if (p_aggregate) {8456StringName name = names[key_idx];8457if (selection.has(name)) {8458if (p_deselectable) {8459call_deferred("_deselect_key", name);8460moving_selection_pivot = 0.0f;8461moving_selection_mouse_begin_x = 0.0f;8462}8463} else {8464call_deferred("_select_key", name, false);8465moving_selection_attempt = true;8466moving_selection_effective = false;8467select_single_attempt = StringName();8468moving_selection_pivot = animation->get_marker_time(name);8469moving_selection_mouse_begin_x = p_pos.x;8470}84718472} else {8473StringName name = names[key_idx];8474if (!selection.has(name)) {8475call_deferred("_select_key", name, true);8476select_single_attempt = StringName();8477} else {8478select_single_attempt = name;8479}84808481moving_selection_attempt = true;8482moving_selection_effective = false;8483moving_selection_pivot = animation->get_marker_time(name);8484moving_selection_mouse_begin_x = p_pos.x;8485}84868487if (read_only) {8488moving_selection_attempt = false;8489moving_selection_pivot = 0.0f;8490moving_selection_mouse_begin_x = 0.0f;8491}8492return true;8493}8494}84958496return false;8497}84988499bool AnimationMarkerEdit::_is_ui_pos_in_current_section(const Point2 &p_pos) {8500int limit = timeline->get_name_limit();8501int limit_end = get_size().width - timeline->get_buttons_width();85028503if (p_pos.x >= limit && p_pos.x <= limit_end) {8504PackedStringArray section = get_selected_section();8505if (!section.is_empty()) {8506StringName start_marker = section[0];8507StringName end_marker = section[1];8508float start_offset = (animation->get_marker_time(start_marker) - timeline->get_value()) * timeline->get_zoom_scale() + limit;8509float end_offset = (animation->get_marker_time(end_marker) - timeline->get_value()) * timeline->get_zoom_scale() + limit;8510return p_pos.x >= start_offset && p_pos.x <= end_offset;8511}8512}85138514return false;8515}85168517HBoxContainer *AnimationMarkerEdit::_create_hbox_labeled_control(const String &p_text, Control *p_control) const {8518HBoxContainer *hbox = memnew(HBoxContainer);8519Label *label = memnew(Label);8520label->set_text(p_text);8521hbox->add_child(label);8522hbox->add_child(p_control);8523hbox->set_h_size_flags(SIZE_EXPAND_FILL);8524label->set_h_size_flags(SIZE_EXPAND_FILL);8525label->set_stretch_ratio(1.0);8526p_control->set_h_size_flags(SIZE_EXPAND_FILL);8527p_control->set_stretch_ratio(1.0);8528return hbox;8529}85308531void AnimationMarkerEdit::_update_key_edit() {8532_clear_key_edit();8533if (animation.is_null()) {8534return;8535}85368537if (selection.size() == 1) {8538key_edit = memnew(AnimationMarkerKeyEdit);8539key_edit->animation = animation;8540key_edit->animation_read_only = read_only;8541key_edit->marker_name = *selection.begin();8542key_edit->use_fps = timeline->is_using_fps();8543key_edit->marker_edit = this;85448545EditorNode::get_singleton()->push_item(key_edit);85468547InspectorDock::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);8548} else if (selection.size() > 1) {8549multi_key_edit = memnew(AnimationMultiMarkerKeyEdit);8550multi_key_edit->animation = animation;8551multi_key_edit->animation_read_only = read_only;8552multi_key_edit->marker_edit = this;8553for (const StringName &name : selection) {8554multi_key_edit->marker_names.push_back(name);8555}85568557EditorNode::get_singleton()->push_item(multi_key_edit);8558}8559}85608561void AnimationMarkerEdit::_clear_key_edit() {8562if (key_edit) {8563// If key edit is the object being inspected, remove it first.8564if (InspectorDock::get_inspector_singleton()->get_edited_object() == key_edit) {8565EditorNode::get_singleton()->push_item(nullptr);8566}85678568// Then actually delete it.8569memdelete(key_edit);8570key_edit = nullptr;8571}85728573if (multi_key_edit) {8574if (InspectorDock::get_inspector_singleton()->get_edited_object() == multi_key_edit) {8575EditorNode::get_singleton()->push_item(nullptr);8576}85778578memdelete(multi_key_edit);8579multi_key_edit = nullptr;8580}8581}85828583void AnimationMarkerEdit::_bind_methods() {8584ClassDB::bind_method("_clear_selection_for_anim", &AnimationMarkerEdit::_clear_selection_for_anim);8585ClassDB::bind_method("_select_key", &AnimationMarkerEdit::_select_key);8586ClassDB::bind_method("_deselect_key", &AnimationMarkerEdit::_deselect_key);8587}85888589void AnimationMarkerEdit::_notification(int p_what) {8590switch (p_what) {8591case NOTIFICATION_THEME_CHANGED: {8592if (animation.is_null()) {8593return;8594}85958596type_icon = get_editor_theme_icon(SNAME("Marker"));8597selected_icon = get_editor_theme_icon(SNAME("MarkerSelected"));8598} break;85998600case NOTIFICATION_ACCESSIBILITY_UPDATE: {8601RID ae = get_accessibility_element();8602ERR_FAIL_COND(ae.is_null());86038604//TODO8605DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_STATIC_TEXT);8606DisplayServer::get_singleton()->accessibility_update_set_value(ae, TTR(vformat("The %s is not accessible at this time.", "Animation marker editor")));8607} break;86088609case NOTIFICATION_DRAW: {8610if (animation.is_null()) {8611return;8612}86138614int limit = timeline->get_name_limit();86158616Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));8617Color color = get_theme_color(SceneStringName(font_color), SNAME("Label"));86188619// SECTION PREVIEW //86208621{8622float scale = timeline->get_zoom_scale();8623int limit_end = get_size().width - timeline->get_buttons_width();86248625PackedStringArray section = get_selected_section();8626if (section.size() == 2) {8627StringName start_marker = section[0];8628StringName end_marker = section[1];8629double start_time = animation->get_marker_time(start_marker);8630double end_time = animation->get_marker_time(end_marker);86318632// When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section.8633AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();8634if (moving_selection && !(player && player->is_playing())) {8635start_time += moving_selection_offset;8636end_time += moving_selection_offset;8637}86388639if (start_time < animation->get_length() && end_time >= 0) {8640float start_ofs = MAX(0, start_time) - timeline->get_value();8641float end_ofs = MIN(animation->get_length(), end_time) - timeline->get_value();8642start_ofs = start_ofs * scale + limit;8643end_ofs = end_ofs * scale + limit;8644start_ofs = MAX(start_ofs, limit);8645end_ofs = MIN(end_ofs, limit_end);8646Rect2 rect;8647rect.set_position(Vector2(start_ofs, 0));8648rect.set_size(Vector2(end_ofs - start_ofs, get_size().height));86498650draw_rect(rect, Color(1, 0.1, 0.1, 0.2));8651}8652}8653}86548655// KEYFRAMES //86568657draw_bg(limit, get_size().width - timeline->get_buttons_width());86588659{8660float scale = timeline->get_zoom_scale();8661int limit_end = get_size().width - timeline->get_buttons_width();86628663PackedStringArray names = animation->get_marker_names();8664for (int i = 0; i < names.size(); i++) {8665StringName name = names[i];8666bool is_selected = selection.has(name);8667float offset = animation->get_marker_time(name) - timeline->get_value();8668if (is_selected && moving_selection) {8669offset += moving_selection_offset;8670}86718672offset = offset * scale + limit;86738674draw_key(name, scale, int(offset), is_selected, limit, limit_end);86758676const int font_size = 12 * EDSCALE;8677Size2 string_size = font->get_string_size(name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size);8678if (int(offset) <= limit_end && int(offset) >= limit && should_show_all_marker_names) {8679float bottom = get_size().height + string_size.y - font->get_descent(font_size);8680float extrusion = MAX(0, offset + string_size.x - limit_end); // How much the string would extrude outside limit_end if unadjusted.8681Color marker_color = animation->get_marker_color(name);8682float margin = 4 * EDSCALE;8683Point2 pos = Point2(offset - extrusion + margin, bottom + margin);8684draw_string(font, pos, name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size, marker_color);8685draw_string_outline(font, pos, name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size, 1, color);8686}8687}8688}86898690draw_fg(limit, get_size().width - timeline->get_buttons_width());8691} break;86928693case NOTIFICATION_MOUSE_ENTER:8694hovered = true;8695queue_redraw();8696break;8697case NOTIFICATION_MOUSE_EXIT:8698hovered = false;8699// When the mouse cursor exits the track, we're no longer hovering any keyframe.8700hovering_marker = StringName();8701queue_redraw();8702break;8703}8704}87058706void AnimationMarkerEdit::gui_input(const Ref<InputEvent> &p_event) {8707ERR_FAIL_COND(p_event.is_null());87088709if (animation.is_null()) {8710return;8711}87128713if (p_event->is_pressed()) {8714if (ED_IS_SHORTCUT("animation_marker_edit/rename_marker", p_event)) {8715if (!read_only) {8716_menu_selected(MENU_KEY_RENAME);8717}8718}87198720if (ED_IS_SHORTCUT("animation_marker_edit/delete_selection", p_event)) {8721if (!read_only) {8722_menu_selected(MENU_KEY_DELETE);8723}8724}87258726if (ED_IS_SHORTCUT("animation_marker_edit/toggle_marker_names", p_event)) {8727if (!read_only) {8728_menu_selected(MENU_KEY_TOGGLE_MARKER_NAMES);8729}8730}8731}87328733Ref<InputEventMouseButton> mb = p_event;87348735if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {8736Point2 pos = mb->get_position();8737if (_try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), true)) {8738accept_event();8739} else if (!_is_ui_pos_in_current_section(pos)) {8740_clear_selection_for_anim(animation);8741}8742}87438744if (mb.is_valid() && moving_selection_attempt) {8745if (!mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {8746moving_selection_attempt = false;8747if (moving_selection && moving_selection_effective) {8748if (Math::abs(moving_selection_offset) > CMP_EPSILON) {8749_move_selection_commit();8750accept_event(); // So play position doesn't snap to the end of move selection.8751}8752} else if (select_single_attempt) {8753call_deferred("_select_key", select_single_attempt, true);87548755// First select click should not affect play position.8756if (!selection.has(select_single_attempt)) {8757accept_event();8758} else {8759// Second click and onwards should snap to marker time.8760double ofs = animation->get_marker_time(select_single_attempt);8761timeline->set_play_position(ofs);8762timeline->emit_signal(SNAME("timeline_changed"), ofs, mb->is_alt_pressed());8763accept_event();8764}8765} else {8766// First select click should not affect play position.8767if (!selection.has(select_single_attempt)) {8768accept_event();8769}8770}87718772moving_selection = false;8773select_single_attempt = StringName();8774}87758776if (moving_selection && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {8777moving_selection_attempt = false;8778moving_selection = false;8779_move_selection_cancel();8780}8781}87828783if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {8784Point2 pos = mb->get_position();8785if (pos.x >= timeline->get_name_limit() && pos.x <= get_size().width - timeline->get_buttons_width()) {8786// Can do something with menu too! show insert key.8787float offset = (pos.x - timeline->get_name_limit()) / timeline->get_zoom_scale();8788if (!read_only) {8789bool selected = _try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), false);87908791menu->clear();8792menu->add_icon_item(get_editor_theme_icon(SNAME("Key")), TTR("Insert Marker..."), MENU_KEY_INSERT);87938794if (selected || selection.size() > 0) {8795menu->add_icon_item(get_editor_theme_icon(SNAME("Edit")), TTR("Rename Marker"), MENU_KEY_RENAME);8796menu->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Delete Marker(s)"), MENU_KEY_DELETE);8797}87988799menu->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);8800menu->reset_size();88018802moving_selection_attempt = false;8803moving_selection = false;88048805menu->set_position(get_screen_position() + get_local_mouse_position());8806menu->popup();88078808insert_at_pos = offset + timeline->get_value();8809accept_event();8810}8811}8812}88138814Ref<InputEventMouseMotion> mm = p_event;88158816if (mm.is_valid()) {8817const StringName previous_hovering_marker = hovering_marker;88188819// Hovering compressed keyframes for editing is not possible.8820const float scale = timeline->get_zoom_scale();8821const int limit = timeline->get_name_limit();8822const int limit_end = get_size().width - timeline->get_buttons_width();8823// Left Border including space occupied by keyframes on t=0.8824const int limit_start_hitbox = limit - type_icon->get_width();8825const Point2 pos = mm->get_position();88268827if (pos.x >= limit_start_hitbox && pos.x <= limit_end) {8828// Use the same logic as key selection to ensure that hovering accurately represents8829// which key will be selected when clicking.8830int key_idx = -1;8831float key_distance = 1e20;88328833hovering_marker = StringName();88348835PackedStringArray names = animation->get_marker_names();88368837// Hovering should happen in the opposite order of drawing for more accurate overlap hovering.8838for (int i = names.size() - 1; i >= 0; i--) {8839StringName name = names[i];8840Rect2 rect = get_key_rect(scale);8841float offset = animation->get_marker_time(name) - timeline->get_value();8842offset = offset * scale + limit;8843rect.position.x += offset;88448845if (rect.has_point(pos)) {8846if (is_key_selectable_by_distance()) {8847const float distance = Math::abs(offset - pos.x);8848if (key_idx == -1 || distance < key_distance) {8849key_idx = i;8850key_distance = distance;8851hovering_marker = name;8852}8853} else {8854// First one does it.8855hovering_marker = name;8856break;8857}8858}8859}88608861if (hovering_marker != previous_hovering_marker) {8862// Required to draw keyframe hover feedback on the correct keyframe.8863queue_redraw();8864}8865}8866}88678868if (mm.is_valid() && mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && moving_selection_attempt) {8869if (!moving_selection) {8870moving_selection = true;8871_move_selection_begin();8872}88738874float moving_begin_time = ((moving_selection_mouse_begin_x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value();8875float new_time = ((mm->get_position().x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value();8876float delta = new_time - moving_begin_time;8877float snapped_time = editor->snap_time(moving_selection_pivot + delta);88788879float offset = 0.0;8880if (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)) {8881offset = snapped_time - moving_selection_pivot;8882moving_selection_effective = true;8883}88848885_move_selection(offset);8886}8887}88888889String AnimationMarkerEdit::get_tooltip(const Point2 &p_pos) const {8890if (animation.is_null()) {8891return Control::get_tooltip(p_pos);8892}88938894int limit = timeline->get_name_limit();8895int limit_end = get_size().width - timeline->get_buttons_width();8896// Left Border including space occupied by keyframes on t=0.8897int limit_start_hitbox = limit - type_icon->get_width();88988899if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) {8900int key_idx = -1;8901float key_distance = 1e20;89028903PackedStringArray names = animation->get_marker_names();89048905// Select should happen in the opposite order of drawing for more accurate overlap select.8906for (int i = names.size() - 1; i >= 0; i--) {8907StringName name = names[i];8908Rect2 rect = const_cast<AnimationMarkerEdit *>(this)->get_key_rect(timeline->get_zoom_scale());8909float offset = animation->get_marker_time(name) - timeline->get_value();8910offset = offset * timeline->get_zoom_scale() + limit;8911rect.position.x += offset;89128913if (rect.has_point(p_pos)) {8914if (const_cast<AnimationMarkerEdit *>(this)->is_key_selectable_by_distance()) {8915float distance = Math::abs(offset - p_pos.x);8916if (key_idx == -1 || distance < key_distance) {8917key_idx = i;8918key_distance = distance;8919}8920} else {8921// First one does it.8922break;8923}8924}8925}89268927if (key_idx != -1) {8928String name = names[key_idx];8929String text = TTR("Time (s):") + " " + TS->format_number(rtos(Math::snapped(animation->get_marker_time(name), 0.0001))) + "\n";8930text += TTR("Marker:") + " " + name + "\n";8931return text;8932}8933}89348935return Control::get_tooltip(p_pos);8936}89378938int AnimationMarkerEdit::get_key_height() const {8939if (animation.is_null()) {8940return 0;8941}89428943return type_icon->get_height();8944}89458946Rect2 AnimationMarkerEdit::get_key_rect(float p_pixels_sec) const {8947if (animation.is_null()) {8948return Rect2();8949}89508951Rect2 rect = Rect2(-type_icon->get_width() / 2, get_size().height - type_icon->get_size().height, type_icon->get_width(), type_icon->get_size().height);89528953// Make it a big easier to click.8954rect.position.x -= rect.size.x * 0.5;8955rect.size.x *= 2;8956return rect;8957}89588959PackedStringArray AnimationMarkerEdit::get_selected_section() const {8960if (selection.size() >= 2) {8961PackedStringArray arr;8962arr.push_back(""); // Marker with smallest time.8963arr.push_back(""); // Marker with largest time.8964double min_time = Math::INF;8965double max_time = -Math::INF;8966for (const StringName &marker_name : selection) {8967double time = animation->get_marker_time(marker_name);8968if (time < min_time) {8969arr.set(0, marker_name);8970min_time = time;8971}8972if (time > max_time) {8973arr.set(1, marker_name);8974max_time = time;8975}8976}8977return arr;8978}89798980return PackedStringArray();8981}89828983bool AnimationMarkerEdit::is_marker_selected(const StringName &p_marker) const {8984return selection.has(p_marker);8985}89868987bool AnimationMarkerEdit::is_key_selectable_by_distance() const {8988return true;8989}89908991void 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) {8992if (animation.is_null()) {8993return;8994}89958996if (p_x < p_clip_left || p_x > p_clip_right) {8997return;8998}89999000Ref<Texture2D> icon_to_draw = p_selected ? selected_icon : type_icon;90019002Vector2 ofs(p_x - icon_to_draw->get_width() / 2, int(get_size().height - icon_to_draw->get_height()));90039004// Don't apply custom marker color when the key is selected.9005Color marker_color = p_selected ? Color(1, 1, 1) : animation->get_marker_color(p_name);90069007// Use a different color for the currently hovered key.9008// The color multiplier is chosen to work with both dark and light editor themes,9009// and on both unselected and selected key icons.9010draw_texture(9011icon_to_draw,9012ofs,9013p_name == hovering_marker ? get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")) : marker_color);9014}90159016void AnimationMarkerEdit::draw_bg(int p_clip_left, int p_clip_right) {9017}90189019void AnimationMarkerEdit::draw_fg(int p_clip_left, int p_clip_right) {9020}90219022Ref<Animation> AnimationMarkerEdit::get_animation() const {9023return animation;9024}90259026void AnimationMarkerEdit::set_animation(const Ref<Animation> &p_animation, bool p_read_only) {9027if (animation.is_valid()) {9028_clear_selection_for_anim(animation);9029}9030animation = p_animation;9031read_only = p_read_only;9032type_icon = get_editor_theme_icon(SNAME("Marker"));9033selected_icon = get_editor_theme_icon(SNAME("MarkerSelected"));90349035queue_redraw();9036}90379038Size2 AnimationMarkerEdit::get_minimum_size() const {9039Ref<Texture2D> texture = get_editor_theme_icon(SNAME("Object"));9040Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));9041int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));9042int separation = get_theme_constant(SNAME("v_separation"), SNAME("ItemList"));90439044int max_h = MAX(texture->get_height(), font->get_height(font_size));9045max_h = MAX(max_h, get_key_height());90469047return Vector2(1, max_h + separation);9048}90499050void AnimationMarkerEdit::set_timeline(AnimationTimelineEdit *p_timeline) {9051timeline = p_timeline;9052timeline->connect("zoom_changed", callable_mp(this, &AnimationMarkerEdit::_zoom_changed));9053timeline->connect("name_limit_changed", callable_mp(this, &AnimationMarkerEdit::_zoom_changed));9054}90559056void AnimationMarkerEdit::set_editor(AnimationTrackEditor *p_editor) {9057editor = p_editor;9058}90599060void AnimationMarkerEdit::set_play_position(float p_pos) {9061play_position_pos = p_pos;9062play_position->queue_redraw();9063}90649065void AnimationMarkerEdit::update_play_position() {9066play_position->queue_redraw();9067}90689069void AnimationMarkerEdit::set_use_fps(bool p_use_fps) {9070if (key_edit) {9071key_edit->use_fps = p_use_fps;9072key_edit->notify_property_list_changed();9073}9074}90759076void AnimationMarkerEdit::_move_selection_begin() {9077moving_selection = true;9078moving_selection_offset = 0;9079}90809081void AnimationMarkerEdit::_move_selection(float p_offset) {9082moving_selection_offset = p_offset;9083queue_redraw();9084}90859086void AnimationMarkerEdit::_move_selection_commit() {9087EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();9088undo_redo->create_action(TTR("Animation Move Markers"));90899090for (HashSet<StringName>::Iterator E = selection.last(); E; --E) {9091StringName name = *E;9092double time = animation->get_marker_time(name);9093float newpos = time + moving_selection_offset;9094undo_redo->add_do_method(animation.ptr(), "remove_marker", name);9095undo_redo->add_do_method(animation.ptr(), "add_marker", name, newpos);9096undo_redo->add_do_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name));9097undo_redo->add_undo_method(animation.ptr(), "remove_marker", name);9098undo_redo->add_undo_method(animation.ptr(), "add_marker", name, time);9099undo_redo->add_undo_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name));91009101// add_marker will overwrite the overlapped key on the redo pass, so we add it back on the undo pass.9102if (StringName overlap = animation->get_marker_at_time(newpos)) {9103if (select_single_attempt == overlap) {9104select_single_attempt = "";9105}9106undo_redo->add_undo_method(animation.ptr(), "add_marker", overlap, newpos);9107undo_redo->add_undo_method(animation.ptr(), "set_marker_color", overlap, animation->get_marker_color(overlap));9108}9109}91109111moving_selection = false;9112AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();9113if (player) {9114PackedStringArray selected_section = get_selected_section();9115if (selected_section.size() >= 2) {9116undo_redo->add_do_method(player, "set_section_with_markers", selected_section[0], selected_section[1]);9117undo_redo->add_undo_method(player, "set_section_with_markers", selected_section[0], selected_section[1]);9118}9119}9120undo_redo->add_do_method(timeline, "queue_redraw");9121undo_redo->add_undo_method(timeline, "queue_redraw");9122undo_redo->add_do_method(this, "queue_redraw");9123undo_redo->add_undo_method(this, "queue_redraw");9124undo_redo->commit_action();9125_update_key_edit();9126}91279128void AnimationMarkerEdit::_delete_selected_markers() {9129if (selection.size()) {9130EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();9131undo_redo->create_action(TTR("Animation Delete Markers"));9132for (const StringName &name : selection) {9133double time = animation->get_marker_time(name);9134undo_redo->add_do_method(animation.ptr(), "remove_marker", name);9135undo_redo->add_undo_method(animation.ptr(), "add_marker", name, time);9136undo_redo->add_undo_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name));9137}9138_clear_selection_for_anim(animation);91399140undo_redo->add_do_method(this, "queue_redraw");9141undo_redo->add_undo_method(this, "queue_redraw");9142undo_redo->commit_action();9143_update_key_edit();9144}9145}91469147void AnimationMarkerEdit::_move_selection_cancel() {9148moving_selection = false;9149queue_redraw();9150}91519152void AnimationMarkerEdit::_clear_selection(bool p_update) {9153AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();9154if (player) {9155player->reset_section();9156}91579158selection.clear();91599160if (p_update) {9161queue_redraw();9162}91639164_clear_key_edit();9165}91669167void AnimationMarkerEdit::_clear_selection_for_anim(const Ref<Animation> &p_anim) {9168if (animation != p_anim) {9169return;9170}91719172_clear_selection(true);9173}91749175void AnimationMarkerEdit::_select_key(const StringName &p_name, bool is_single) {9176if (is_single) {9177_clear_selection(false);9178}91799180selection.insert(p_name);91819182AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();9183if (player) {9184if (selection.size() >= 2) {9185PackedStringArray selected_section = get_selected_section();9186double start_time = animation->get_marker_time(selected_section[0]);9187double end_time = animation->get_marker_time(selected_section[1]);9188player->set_section(start_time, end_time);9189} else {9190player->reset_section();9191}9192}91939194queue_redraw();9195_update_key_edit();91969197editor->_clear_selection(editor->is_selection_active());9198}91999200void AnimationMarkerEdit::_deselect_key(const StringName &p_name) {9201selection.erase(p_name);92029203AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();9204if (player) {9205if (selection.size() >= 2) {9206PackedStringArray selected_section = get_selected_section();9207double start_time = animation->get_marker_time(selected_section[0]);9208double end_time = animation->get_marker_time(selected_section[1]);9209player->set_section(start_time, end_time);9210} else {9211player->reset_section();9212}9213}92149215queue_redraw();9216_update_key_edit();9217}92189219void AnimationMarkerEdit::_insert_marker(float p_ofs) {9220if (editor->is_snap_timeline_enabled()) {9221p_ofs = editor->snap_time(p_ofs);9222}92239224marker_insert_confirm->popup_centered(Size2(200, 100) * EDSCALE);9225marker_insert_color->set_pick_color(Color(1, 1, 1));92269227String base = "new_marker";9228int count = 1;9229while (true) {9230String attempt = base;9231if (count > 1) {9232attempt += vformat("_%d", count);9233}9234if (animation->has_marker(attempt)) {9235count++;9236continue;9237}9238base = attempt;9239break;9240}92419242marker_insert_new_name->set_text(base);9243_marker_insert_new_name_changed(base);9244marker_insert_ofs = p_ofs;9245}92469247void AnimationMarkerEdit::_rename_marker(const StringName &p_name) {9248marker_rename_confirm->popup_centered(Size2i(200, 0) * EDSCALE);9249marker_rename_prev_name = p_name;9250marker_rename_new_name->set_text(p_name);9251}92529253void AnimationMarkerEdit::_marker_insert_confirmed() {9254StringName name = marker_insert_new_name->get_text();92559256if (animation->has_marker(name)) {9257marker_insert_error_dialog->set_text(vformat(TTR("Marker '%s' already exists!"), name));9258marker_insert_error_dialog->popup_centered();9259return;9260}92619262EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();92639264undo_redo->create_action(TTR("Add Marker Key"));9265undo_redo->add_do_method(animation.ptr(), "add_marker", name, marker_insert_ofs);9266undo_redo->add_undo_method(animation.ptr(), "remove_marker", name);9267StringName existing_marker = animation->get_marker_at_time(marker_insert_ofs);9268if (existing_marker) {9269undo_redo->add_undo_method(animation.ptr(), "add_marker", existing_marker, marker_insert_ofs);9270undo_redo->add_undo_method(animation.ptr(), "set_marker_color", existing_marker, animation->get_marker_color(existing_marker));9271}9272undo_redo->add_do_method(animation.ptr(), "set_marker_color", name, marker_insert_color->get_pick_color());92739274undo_redo->add_do_method(this, "queue_redraw");9275undo_redo->add_undo_method(this, "queue_redraw");92769277undo_redo->commit_action();92789279marker_insert_confirm->hide();9280}92819282void AnimationMarkerEdit::_marker_insert_new_name_changed(const String &p_text) {9283marker_insert_confirm->get_ok_button()->set_disabled(p_text.is_empty());9284}92859286void AnimationMarkerEdit::_marker_rename_confirmed() {9287StringName new_name = marker_rename_new_name->get_text();9288StringName prev_name = marker_rename_prev_name;92899290if (new_name == StringName()) {9291marker_rename_error_dialog->set_text(TTR("Empty marker names are not allowed."));9292marker_rename_error_dialog->popup_centered();9293return;9294}92959296if (new_name != prev_name && animation->has_marker(new_name)) {9297marker_rename_error_dialog->set_text(vformat(TTR("Marker '%s' already exists!"), new_name));9298marker_rename_error_dialog->popup_centered();9299return;9300}93019302if (prev_name != new_name) {9303EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();9304undo_redo->create_action(TTR("Rename Marker"));9305undo_redo->add_do_method(animation.ptr(), "remove_marker", prev_name);9306undo_redo->add_do_method(animation.ptr(), "add_marker", new_name, animation->get_marker_time(prev_name));9307undo_redo->add_do_method(animation.ptr(), "set_marker_color", new_name, animation->get_marker_color(prev_name));9308undo_redo->add_undo_method(animation.ptr(), "remove_marker", new_name);9309undo_redo->add_undo_method(animation.ptr(), "add_marker", prev_name, animation->get_marker_time(prev_name));9310undo_redo->add_undo_method(animation.ptr(), "set_marker_color", prev_name, animation->get_marker_color(prev_name));9311undo_redo->add_do_method(this, "_select_key", new_name, true);9312undo_redo->add_undo_method(this, "_select_key", prev_name, true);9313undo_redo->commit_action();9314select_single_attempt = StringName();9315}9316marker_rename_confirm->hide();9317}93189319void AnimationMarkerEdit::_marker_rename_new_name_changed(const String &p_text) {9320marker_rename_confirm->get_ok_button()->set_disabled(p_text.is_empty());9321}93229323AnimationMarkerEdit::AnimationMarkerEdit() {9324play_position = memnew(Control);9325play_position->set_mouse_filter(MOUSE_FILTER_PASS);9326add_child(play_position);9327play_position->connect(SceneStringName(draw), callable_mp(this, &AnimationMarkerEdit::_play_position_draw));9328set_focus_mode(FOCUS_CLICK);9329set_mouse_filter(MOUSE_FILTER_PASS); // Scroll has to work too for selection.93309331menu = memnew(PopupMenu);9332add_child(menu);9333menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationMarkerEdit::_menu_selected));9334menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/rename_marker", TTRC("Rename Marker"), Key::R), MENU_KEY_RENAME);9335menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/delete_selection", TTRC("Delete Marker(s)"), Key::KEY_DELETE), MENU_KEY_DELETE);9336menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/toggle_marker_names", TTRC("Show All Marker Names"), Key::M), MENU_KEY_TOGGLE_MARKER_NAMES);93379338marker_insert_confirm = memnew(ConfirmationDialog);9339marker_insert_confirm->set_title(TTR("Insert Marker"));9340marker_insert_confirm->set_hide_on_ok(false);9341marker_insert_confirm->connect(SceneStringName(confirmed), callable_mp(this, &AnimationMarkerEdit::_marker_insert_confirmed));9342add_child(marker_insert_confirm);9343VBoxContainer *marker_insert_vbox = memnew(VBoxContainer);9344marker_insert_vbox->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT);9345marker_insert_confirm->add_child(marker_insert_vbox);9346marker_insert_new_name = memnew(LineEdit);9347marker_insert_new_name->connect(SceneStringName(text_changed), callable_mp(this, &AnimationMarkerEdit::_marker_insert_new_name_changed));9348marker_insert_confirm->register_text_enter(marker_insert_new_name);9349marker_insert_vbox->add_child(_create_hbox_labeled_control(TTR("Marker Name"), marker_insert_new_name));9350marker_insert_color = memnew(ColorPickerButton);9351marker_insert_color->set_edit_alpha(false);9352marker_insert_color->get_popup()->connect("about_to_popup", callable_mp(EditorNode::get_singleton(), &EditorNode::setup_color_picker).bind(marker_insert_color->get_picker()));9353marker_insert_vbox->add_child(_create_hbox_labeled_control(TTR("Marker Color"), marker_insert_color));9354marker_insert_error_dialog = memnew(AcceptDialog);9355marker_insert_error_dialog->set_ok_button_text(TTR("Close"));9356marker_insert_error_dialog->set_title(TTR("Error!"));9357marker_insert_confirm->add_child(marker_insert_error_dialog);93589359marker_rename_confirm = memnew(ConfirmationDialog);9360marker_rename_confirm->set_title(TTR("Rename Marker"));9361marker_rename_confirm->set_hide_on_ok(false);9362marker_rename_confirm->connect(SceneStringName(confirmed), callable_mp(this, &AnimationMarkerEdit::_marker_rename_confirmed));9363add_child(marker_rename_confirm);9364VBoxContainer *marker_rename_vbox = memnew(VBoxContainer);9365marker_rename_vbox->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT);9366marker_rename_confirm->add_child(marker_rename_vbox);9367Label *marker_rename_new_name_label = memnew(Label);9368marker_rename_new_name_label->set_text(TTR("Change Marker Name:"));9369marker_rename_vbox->add_child(marker_rename_new_name_label);9370marker_rename_new_name = memnew(LineEdit);9371marker_rename_new_name->set_accessibility_name(TTRC("Change Marker Name:"));9372marker_rename_new_name->connect(SceneStringName(text_changed), callable_mp(this, &AnimationMarkerEdit::_marker_rename_new_name_changed));9373marker_rename_confirm->register_text_enter(marker_rename_new_name);9374marker_rename_vbox->add_child(marker_rename_new_name);93759376marker_rename_error_dialog = memnew(AcceptDialog);9377marker_rename_error_dialog->set_ok_button_text(TTR("Close"));9378marker_rename_error_dialog->set_title(TTR("Error!"));9379marker_rename_confirm->add_child(marker_rename_error_dialog);9380}93819382float AnimationMarkerKeyEdit::get_time() const {9383return animation->get_marker_time(marker_name);9384}93859386void AnimationMarkerKeyEdit::_bind_methods() {9387ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMarkerKeyEdit::_hide_script_from_inspector);9388ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMarkerKeyEdit::_hide_metadata_from_inspector);9389ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMarkerKeyEdit::_dont_undo_redo);9390ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMarkerKeyEdit::_is_read_only);9391ClassDB::bind_method(D_METHOD("_set_marker_name"), &AnimationMarkerKeyEdit::_set_marker_name);9392}93939394void AnimationMarkerKeyEdit::_set_marker_name(const StringName &p_name) {9395marker_name = p_name;9396}93979398bool AnimationMarkerKeyEdit::_set(const StringName &p_name, const Variant &p_value) {9399EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();94009401if (p_name == "color") {9402Color color = p_value;9403Color prev_color = animation->get_marker_color(marker_name);9404if (color != prev_color) {9405undo_redo->create_action(TTR("Edit Marker Color"), UndoRedo::MERGE_ENDS);9406undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color);9407undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, prev_color);9408undo_redo->add_do_method(marker_edit, "queue_redraw");9409undo_redo->add_undo_method(marker_edit, "queue_redraw");9410undo_redo->commit_action();9411}9412return true;9413}94149415return false;9416}94179418bool AnimationMarkerKeyEdit::_get(const StringName &p_name, Variant &r_ret) const {9419if (p_name == "name") {9420r_ret = marker_name;9421return true;9422}94239424if (p_name == "color") {9425r_ret = animation->get_marker_color(marker_name);9426return true;9427}94289429return false;9430}94319432void AnimationMarkerKeyEdit::_get_property_list(List<PropertyInfo> *p_list) const {9433if (animation.is_null()) {9434return;9435}94369437p_list->push_back(PropertyInfo(Variant::STRING_NAME, "name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY | PROPERTY_USAGE_EDITOR));9438p_list->push_back(PropertyInfo(Variant::COLOR, "color", PROPERTY_HINT_COLOR_NO_ALPHA));9439}94409441void AnimationMultiMarkerKeyEdit::_bind_methods() {9442ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMultiMarkerKeyEdit::_hide_script_from_inspector);9443ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMultiMarkerKeyEdit::_hide_metadata_from_inspector);9444ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMultiMarkerKeyEdit::_dont_undo_redo);9445ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMultiMarkerKeyEdit::_is_read_only);9446}94479448bool AnimationMultiMarkerKeyEdit::_set(const StringName &p_name, const Variant &p_value) {9449EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();9450if (p_name == "color") {9451Color color = p_value;94529453undo_redo->create_action(TTR("Multi Edit Marker Color"), UndoRedo::MERGE_ENDS);94549455for (const StringName &marker_name : marker_names) {9456undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color);9457undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, animation->get_marker_color(marker_name));9458}94599460undo_redo->add_do_method(marker_edit, "queue_redraw");9461undo_redo->add_undo_method(marker_edit, "queue_redraw");9462undo_redo->commit_action();94639464return true;9465}94669467return false;9468}94699470bool AnimationMultiMarkerKeyEdit::_get(const StringName &p_name, Variant &r_ret) const {9471if (p_name == "color") {9472r_ret = animation->get_marker_color(marker_names[0]);9473return true;9474}94759476return false;9477}94789479void AnimationMultiMarkerKeyEdit::_get_property_list(List<PropertyInfo> *p_list) const {9480if (animation.is_null()) {9481return;9482}94839484p_list->push_back(PropertyInfo(Variant::COLOR, "color", PROPERTY_HINT_COLOR_NO_ALPHA));9485}94869487// AnimationMarkerKeyEditEditorPlugin94889489void AnimationMarkerKeyEditEditor::_time_edit_exited() {9490real_t new_time = spinner->get_value();94919492if (use_fps) {9493real_t fps = animation->get_step();9494if (fps > 0) {9495fps = 1.0 / fps;9496}9497new_time /= fps;9498}94999500real_t prev_time = animation->get_marker_time(marker_name);95019502if (Math::is_equal_approx(new_time, prev_time)) {9503return; // No change.9504}95059506EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();9507undo_redo->create_action(TTR("Animation Change Marker Time"));95089509Color color = animation->get_marker_color(marker_name);9510undo_redo->add_do_method(animation.ptr(), "add_marker", marker_name, new_time);9511undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color);9512undo_redo->add_undo_method(animation.ptr(), "remove_marker", marker_name);9513undo_redo->add_undo_method(animation.ptr(), "add_marker", marker_name, prev_time);9514undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, color);9515StringName existing_marker = animation->get_marker_at_time(new_time);9516if (existing_marker) {9517undo_redo->add_undo_method(animation.ptr(), "add_marker", existing_marker, animation->get_marker_time(existing_marker));9518undo_redo->add_undo_method(animation.ptr(), "set_marker_color", existing_marker, animation->get_marker_color(existing_marker));9519}9520AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();9521if (ape) {9522AnimationTrackEditor *ate = ape->get_track_editor();9523if (ate) {9524AnimationMarkerEdit *ame = ate->marker_edit;9525undo_redo->add_do_method(ame, "queue_redraw");9526undo_redo->add_undo_method(ame, "queue_redraw");9527}9528}9529undo_redo->commit_action();9530}95319532AnimationMarkerKeyEditEditor::AnimationMarkerKeyEditEditor(Ref<Animation> p_animation, const StringName &p_name, bool p_use_fps) {9533if (p_animation.is_null()) {9534return;9535}95369537animation = p_animation;9538use_fps = p_use_fps;9539marker_name = p_name;95409541set_label("Time");95429543spinner = memnew(EditorSpinSlider);9544spinner->set_focus_mode(Control::FOCUS_CLICK);9545spinner->set_min(0);9546spinner->set_allow_greater(true);9547spinner->set_allow_lesser(true);9548add_child(spinner);95499550float time = animation->get_marker_time(marker_name);95519552if (use_fps) {9553spinner->set_step(FPS_DECIMAL);9554real_t fps = animation->get_step();9555if (fps > 0) {9556fps = 1.0 / fps;9557}9558spinner->set_value(time * fps);9559spinner->connect("updown_pressed", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED);9560} else {9561spinner->set_step(SECOND_DECIMAL);9562spinner->set_value(time);9563spinner->set_max(animation->get_length());9564}95659566spinner->connect("ungrabbed", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED);9567spinner->connect("value_focus_exited", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED);9568}956995709571