/**************************************************************************/ /* animation_track_editor.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /**************************************************************************/ /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ /* "Software"), to deal in the Software without restriction, including */ /* without limitation the rights to use, copy, modify, merge, publish, */ /* distribute, sublicense, and/or sell copies of the Software, and to */ /* permit persons to whom the Software is furnished to do so, subject to */ /* the following conditions: */ /* */ /* The above copyright notice and this permission notice shall be */ /* included in all copies or substantial portions of the Software. */ /* */ /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ #include "animation_track_editor.h" #include "animation_track_editor_plugins.h" #include "core/error/error_macros.h" #include "core/input/input.h" #include "editor/animation/animation_bezier_editor.h" #include "editor/animation/animation_player_editor_plugin.h" #include "editor/docks/inspector_dock.h" #include "editor/editor_node.h" #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" #include "editor/gui/editor_spin_slider.h" #include "editor/gui/editor_validation_panel.h" #include "editor/inspector/multi_node_edit.h" #include "editor/scene/scene_tree_editor.h" #include "editor/script/script_editor_plugin.h" #include "editor/settings/editor_settings.h" #include "editor/themes/editor_scale.h" #include "scene/3d/mesh_instance_3d.h" #include "scene/animation/animation_player.h" #include "scene/animation/tween.h" #include "scene/gui/check_box.h" #include "scene/gui/color_picker.h" #include "scene/gui/flow_container.h" #include "scene/gui/grid_container.h" #include "scene/gui/option_button.h" #include "scene/gui/panel_container.h" #include "scene/gui/separator.h" #include "scene/gui/slider.h" #include "scene/gui/spin_box.h" #include "scene/gui/texture_rect.h" #include "scene/gui/view_panner.h" #include "scene/main/window.h" #include "servers/audio/audio_stream.h" constexpr double FPS_DECIMAL = 1.0; constexpr double SECOND_DECIMAL = 0.0001; void AnimationTrackKeyEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("_update_obj"), &AnimationTrackKeyEdit::_update_obj); ClassDB::bind_method(D_METHOD("_key_ofs_changed"), &AnimationTrackKeyEdit::_key_ofs_changed); ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationTrackKeyEdit::_hide_script_from_inspector); ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationTrackKeyEdit::_hide_metadata_from_inspector); ClassDB::bind_method(D_METHOD("get_root_path"), &AnimationTrackKeyEdit::get_root_path); ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationTrackKeyEdit::_dont_undo_redo); ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationTrackKeyEdit::_is_read_only); } void AnimationTrackKeyEdit::_fix_node_path(Variant &value) { NodePath np = value; if (np == NodePath()) { return; } Node *root = EditorNode::get_singleton()->get_tree()->get_root(); Node *np_node = root->get_node_or_null(np); ERR_FAIL_NULL(np_node); Node *edited_node = root->get_node_or_null(base); ERR_FAIL_NULL(edited_node); value = edited_node->get_path_to(np_node); } void AnimationTrackKeyEdit::_update_obj(const Ref &p_anim) { if (setting || animation != p_anim) { return; } notify_change(); } void AnimationTrackKeyEdit::_key_ofs_changed(const Ref &p_anim, float from, float to) { if (animation != p_anim || from != key_ofs) { return; } key_ofs = to; if (setting) { return; } notify_change(); } bool AnimationTrackKeyEdit::_set(const StringName &p_name, const Variant &p_value) { int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX); ERR_FAIL_COND_V(key == -1, false); String name = p_name; if (name == "easing") { float val = p_value; float prev_val = animation->track_get_key_transition(track, key); setting = true; EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Animation Change Transition"), UndoRedo::MERGE_ENDS); undo_redo->add_do_method(animation.ptr(), "track_set_key_transition", track, key, val); undo_redo->add_undo_method(animation.ptr(), "track_set_key_transition", track, key, prev_val); undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(this, "_update_obj", animation); AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); if (ape) { undo_redo->add_do_method(ape, "_animation_update_key_frame"); undo_redo->add_undo_method(ape, "_animation_update_key_frame"); } undo_redo->commit_action(); setting = false; return true; } EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); switch (animation->track_get_type(track)) { case Animation::TYPE_POSITION_3D: case Animation::TYPE_ROTATION_3D: case Animation::TYPE_SCALE_3D: { if (name == "position" || name == "rotation" || name == "scale") { Variant old = animation->track_get_key_value(track, key); setting = true; String action_name; switch (animation->track_get_type(track)) { case Animation::TYPE_POSITION_3D: action_name = TTR("Animation Change Position3D"); break; case Animation::TYPE_ROTATION_3D: action_name = TTR("Animation Change Rotation3D"); break; case Animation::TYPE_SCALE_3D: action_name = TTR("Animation Change Scale3D"); break; default: { } } undo_redo->create_action(action_name); undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, p_value); undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, old); undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(this, "_update_obj", animation); undo_redo->commit_action(); setting = false; return true; } } break; case Animation::TYPE_BLEND_SHAPE: case Animation::TYPE_VALUE: { if (name == "value") { Variant value = p_value; if (value.get_type() == Variant::NODE_PATH) { _fix_node_path(value); } setting = true; undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS); Variant prev = animation->track_get_key_value(track, key); undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, value); undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, prev); undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(this, "_update_obj", animation); AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); if (ape) { undo_redo->add_do_method(ape, "_animation_update_key_frame"); undo_redo->add_undo_method(ape, "_animation_update_key_frame"); } undo_redo->commit_action(); setting = false; return true; } } break; case Animation::TYPE_METHOD: { Dictionary d_old = animation->track_get_key_value(track, key); Dictionary d_new = d_old.duplicate(); bool change_notify_deserved = false; bool mergeable = false; if (name == "name") { d_new["method"] = p_value; } else if (name == "arg_count") { Vector args = d_old["args"]; args.resize(p_value); d_new["args"] = args; change_notify_deserved = true; } else if (name.begins_with("args/")) { Vector args = d_old["args"]; int idx = name.get_slicec('/', 1).to_int(); ERR_FAIL_INDEX_V(idx, args.size(), false); String what = name.get_slicec('/', 2); if (what == "type") { Variant::Type t = Variant::Type(int(p_value)); if (t != args[idx].get_type()) { Callable::CallError err; if (Variant::can_convert_strict(args[idx].get_type(), t)) { Variant old = args[idx]; Variant *ptrs[1] = { &old }; Variant::construct(t, args.write[idx], (const Variant **)ptrs, 1, err); } else { Variant::construct(t, args.write[idx], nullptr, 0, err); } change_notify_deserved = true; d_new["args"] = args; } } else if (what == "value") { Variant value = p_value; if (value.get_type() == Variant::NODE_PATH) { _fix_node_path(value); } args.write[idx] = value; d_new["args"] = args; mergeable = true; } } if (mergeable) { undo_redo->create_action(TTR("Animation Change Call"), UndoRedo::MERGE_ENDS); } else { undo_redo->create_action(TTR("Animation Change Call")); } setting = true; undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, d_new); undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, d_old); undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(this, "_update_obj", animation); undo_redo->commit_action(); setting = false; if (change_notify_deserved) { notify_change(); } return true; } break; case Animation::TYPE_BEZIER: { if (name == "value") { const Variant &value = p_value; setting = true; undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS); float prev = animation->bezier_track_get_key_value(track, key); undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_value", track, key, value); undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_value", track, key, prev); undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(this, "_update_obj", animation); AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); if (ape) { undo_redo->add_do_method(ape, "_animation_update_key_frame"); undo_redo->add_undo_method(ape, "_animation_update_key_frame"); } undo_redo->commit_action(); setting = false; return true; } if (name == "in_handle") { const Variant &value = p_value; setting = true; undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS); Vector2 prev = animation->bezier_track_get_key_in_handle(track, key); undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, value); undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, prev); undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(this, "_update_obj", animation); AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); if (ape) { undo_redo->add_do_method(ape, "_animation_update_key_frame"); undo_redo->add_undo_method(ape, "_animation_update_key_frame"); } undo_redo->commit_action(); setting = false; return true; } if (name == "out_handle") { const Variant &value = p_value; setting = true; undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS); Vector2 prev = animation->bezier_track_get_key_out_handle(track, key); undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, value); undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, prev); undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(this, "_update_obj", animation); AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); if (ape) { undo_redo->add_do_method(ape, "_animation_update_key_frame"); undo_redo->add_undo_method(ape, "_animation_update_key_frame"); } undo_redo->commit_action(); setting = false; return true; } if (name == "handle_mode") { const Variant &value = p_value; setting = true; undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS, animation.ptr()); int prev_mode = animation->bezier_track_get_key_handle_mode(track, key); Vector2 prev_in_handle = animation->bezier_track_get_key_in_handle(track, key); Vector2 prev_out_handle = animation->bezier_track_get_key_out_handle(track, key); undo_redo->add_do_method(editor, "_bezier_track_set_key_handle_mode", animation.ptr(), track, key, value); undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(editor, "_bezier_track_set_key_handle_mode", animation.ptr(), track, key, prev_mode); undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, prev_in_handle); undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, prev_out_handle); undo_redo->add_undo_method(this, "_update_obj", animation); AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); if (ape) { undo_redo->add_do_method(ape, "_animation_update_key_frame"); undo_redo->add_undo_method(ape, "_animation_update_key_frame"); } undo_redo->commit_action(); setting = false; return true; } } break; case Animation::TYPE_AUDIO: { if (name == "stream") { Ref stream = p_value; setting = true; undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS); Ref prev = animation->audio_track_get_key_stream(track, key); undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_stream", track, key, stream); undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_stream", track, key, prev); undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(this, "_update_obj", animation); undo_redo->commit_action(); setting = false; notify_change(); // To update limits for `start_offset`/`end_offset` sliders (they depend on the stream length). return true; } if (name == "start_offset") { float value = p_value; setting = true; undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS); float prev = animation->audio_track_get_key_start_offset(track, key); undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_start_offset", track, key, value); undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_start_offset", track, key, prev); undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(this, "_update_obj", animation); undo_redo->commit_action(); setting = false; return true; } if (name == "end_offset") { float value = p_value; setting = true; undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS); float prev = animation->audio_track_get_key_end_offset(track, key); undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_end_offset", track, key, value); undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_end_offset", track, key, prev); undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(this, "_update_obj", animation); undo_redo->commit_action(); setting = false; return true; } } break; case Animation::TYPE_ANIMATION: { if (name == "animation") { StringName anim_name = p_value; setting = true; undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS); StringName prev = animation->animation_track_get_key_animation(track, key); undo_redo->add_do_method(animation.ptr(), "animation_track_set_key_animation", track, key, anim_name); undo_redo->add_undo_method(animation.ptr(), "animation_track_set_key_animation", track, key, prev); undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(this, "_update_obj", animation); undo_redo->commit_action(); setting = false; return true; } } break; } return false; } bool AnimationTrackKeyEdit::_get(const StringName &p_name, Variant &r_ret) const { int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX); ERR_FAIL_COND_V(key == -1, false); String name = p_name; if (name == "easing") { r_ret = animation->track_get_key_transition(track, key); return true; } switch (animation->track_get_type(track)) { case Animation::TYPE_POSITION_3D: case Animation::TYPE_ROTATION_3D: case Animation::TYPE_SCALE_3D: { if (name == "position" || name == "rotation" || name == "scale") { r_ret = animation->track_get_key_value(track, key); return true; } } break; case Animation::TYPE_BLEND_SHAPE: case Animation::TYPE_VALUE: { if (name == "value") { r_ret = animation->track_get_key_value(track, key); return true; } } break; case Animation::TYPE_METHOD: { Dictionary d = animation->track_get_key_value(track, key); if (name == "name") { ERR_FAIL_COND_V(!d.has("method"), false); r_ret = d["method"]; return true; } ERR_FAIL_COND_V(!d.has("args"), false); Vector args = d["args"]; if (name == "arg_count") { r_ret = args.size(); return true; } if (name.begins_with("args/")) { int idx = name.get_slicec('/', 1).to_int(); ERR_FAIL_INDEX_V(idx, args.size(), false); String what = name.get_slicec('/', 2); if (what == "type") { r_ret = args[idx].get_type(); return true; } if (what == "value") { r_ret = args[idx]; return true; } } } break; case Animation::TYPE_BEZIER: { if (name == "value") { r_ret = animation->bezier_track_get_key_value(track, key); return true; } if (name == "in_handle") { r_ret = animation->bezier_track_get_key_in_handle(track, key); return true; } if (name == "out_handle") { r_ret = animation->bezier_track_get_key_out_handle(track, key); return true; } if (name == "handle_mode") { r_ret = animation->bezier_track_get_key_handle_mode(track, key); return true; } } break; case Animation::TYPE_AUDIO: { if (name == "stream") { r_ret = animation->audio_track_get_key_stream(track, key); return true; } if (name == "start_offset") { r_ret = animation->audio_track_get_key_start_offset(track, key); return true; } if (name == "end_offset") { r_ret = animation->audio_track_get_key_end_offset(track, key); return true; } } break; case Animation::TYPE_ANIMATION: { if (name == "animation") { r_ret = animation->animation_track_get_key_animation(track, key); return true; } } break; } return false; } void AnimationTrackKeyEdit::_get_property_list(List *p_list) const { if (animation.is_null()) { return; } ERR_FAIL_INDEX(track, animation->get_track_count()); int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX); ERR_FAIL_COND(key == -1); switch (animation->track_get_type(track)) { case Animation::TYPE_POSITION_3D: { p_list->push_back(PropertyInfo(Variant::VECTOR3, PNAME("position"))); } break; case Animation::TYPE_ROTATION_3D: { p_list->push_back(PropertyInfo(Variant::QUATERNION, PNAME("rotation"))); } break; case Animation::TYPE_SCALE_3D: { p_list->push_back(PropertyInfo(Variant::VECTOR3, PNAME("scale"))); } break; case Animation::TYPE_BLEND_SHAPE: { p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("value"))); } break; case Animation::TYPE_VALUE: { Variant v = animation->track_get_key_value(track, key); if (hint.type != Variant::NIL) { PropertyInfo pi = hint; pi.name = PNAME("value"); p_list->push_back(pi); } else { PropertyHint val_hint = PROPERTY_HINT_NONE; String val_hint_string; if (v.get_type() == Variant::OBJECT) { // Could actually check the object property if exists..? Yes I will! Ref res = v; if (res.is_valid()) { val_hint = PROPERTY_HINT_RESOURCE_TYPE; val_hint_string = res->get_class(); } } if (v.get_type() != Variant::NIL) { p_list->push_back(PropertyInfo(v.get_type(), PNAME("value"), val_hint, val_hint_string)); } } } break; case Animation::TYPE_METHOD: { p_list->push_back(PropertyInfo(Variant::STRING_NAME, PNAME("name"))); p_list->push_back(PropertyInfo(Variant::INT, PNAME("arg_count"), PROPERTY_HINT_RANGE, "0,32,1,or_greater")); Dictionary d = animation->track_get_key_value(track, key); ERR_FAIL_COND(!d.has("args")); Vector args = d["args"]; String vtypes; for (int i = 0; i < Variant::VARIANT_MAX; i++) { if (i > 0) { vtypes += ","; } vtypes += Variant::get_type_name(Variant::Type(i)); } for (int i = 0; i < args.size(); i++) { p_list->push_back(PropertyInfo(Variant::INT, vformat("%s/%d/%s", PNAME("args"), i, PNAME("type")), PROPERTY_HINT_ENUM, vtypes)); if (args[i].get_type() != Variant::NIL) { p_list->push_back(PropertyInfo(args[i].get_type(), vformat("%s/%d/%s", PNAME("args"), i, PNAME("value")))); } } } break; case Animation::TYPE_BEZIER: { Animation::HandleMode hm = animation->bezier_track_get_key_handle_mode(track, key); p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("value"))); if (hm == Animation::HANDLE_MODE_LINEAR) { p_list->push_back(PropertyInfo(Variant::VECTOR2, PNAME("in_handle"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY)); p_list->push_back(PropertyInfo(Variant::VECTOR2, PNAME("out_handle"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY)); } else { p_list->push_back(PropertyInfo(Variant::VECTOR2, PNAME("in_handle"))); p_list->push_back(PropertyInfo(Variant::VECTOR2, PNAME("out_handle"))); } p_list->push_back(PropertyInfo(Variant::INT, PNAME("handle_mode"), PROPERTY_HINT_ENUM, "Free,Linear,Balanced,Mirrored")); } break; case Animation::TYPE_AUDIO: { p_list->push_back(PropertyInfo(Variant::OBJECT, PNAME("stream"), PROPERTY_HINT_RESOURCE_TYPE, "AudioStream")); Ref audio_stream = animation->audio_track_get_key_stream(track, key); String hint_string = vformat("0,%.4f,0.0001,or_greater", audio_stream.is_valid() ? audio_stream->get_length() : 3600.0); p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("start_offset"), PROPERTY_HINT_RANGE, hint_string)); p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("end_offset"), PROPERTY_HINT_RANGE, hint_string)); } break; case Animation::TYPE_ANIMATION: { String animations; if (root_path) { AnimationPlayer *ap = Object::cast_to(root_path->get_node_or_null(animation->track_get_path(track))); if (ap) { List anims; ap->get_animation_list(&anims); for (const StringName &E : anims) { if (!animations.is_empty()) { animations += ","; } animations += String(E); } } } if (!animations.is_empty()) { animations += ","; } animations += "[stop]"; p_list->push_back(PropertyInfo(Variant::STRING_NAME, PNAME("animation"), PROPERTY_HINT_ENUM, animations)); } break; } if (animation->track_get_type(track) == Animation::TYPE_VALUE) { p_list->push_back(PropertyInfo(Variant::FLOAT, PNAME("easing"), PROPERTY_HINT_EXP_EASING)); } } void AnimationTrackKeyEdit::notify_change() { notify_property_list_changed(); } Node *AnimationTrackKeyEdit::get_root_path() { return root_path; } void AnimationTrackKeyEdit::set_use_fps(bool p_enable) { use_fps = p_enable; notify_property_list_changed(); } void AnimationMultiTrackKeyEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("_update_obj"), &AnimationMultiTrackKeyEdit::_update_obj); ClassDB::bind_method(D_METHOD("_key_ofs_changed"), &AnimationMultiTrackKeyEdit::_key_ofs_changed); ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMultiTrackKeyEdit::_hide_script_from_inspector); ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMultiTrackKeyEdit::_hide_metadata_from_inspector); ClassDB::bind_method(D_METHOD("get_root_path"), &AnimationMultiTrackKeyEdit::get_root_path); ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMultiTrackKeyEdit::_dont_undo_redo); ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMultiTrackKeyEdit::_is_read_only); } void AnimationMultiTrackKeyEdit::_fix_node_path(Variant &value, NodePath &base) { NodePath np = value; if (np == NodePath()) { return; } Node *root = EditorNode::get_singleton()->get_tree()->get_root(); Node *np_node = root->get_node_or_null(np); ERR_FAIL_NULL(np_node); Node *edited_node = root->get_node_or_null(base); ERR_FAIL_NULL(edited_node); value = edited_node->get_path_to(np_node); } void AnimationMultiTrackKeyEdit::_update_obj(const Ref &p_anim) { if (setting || animation != p_anim) { return; } notify_change(); } void AnimationMultiTrackKeyEdit::_key_ofs_changed(const Ref &p_anim, float from, float to) { if (animation != p_anim) { return; } for (const KeyValue> &E : key_ofs_map) { int key = 0; for (const float &key_ofs : E.value) { if (from != key_ofs) { key++; continue; } int track = E.key; key_ofs_map[track].get(key) = to; if (setting) { return; } notify_change(); return; } } } bool AnimationMultiTrackKeyEdit::_set(const StringName &p_name, const Variant &p_value) { bool update_obj = false; bool change_notify_deserved = false; for (const KeyValue> &E : key_ofs_map) { int track = E.key; for (const float &key_ofs : E.value) { int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX); ERR_FAIL_COND_V(key == -1, false); String name = p_name; if (name == "easing") { float val = p_value; float prev_val = animation->track_get_key_transition(track, key); EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); if (!setting) { setting = true; undo_redo->create_action(TTR("Animation Multi Change Transition"), UndoRedo::MERGE_ENDS); } undo_redo->add_do_method(animation.ptr(), "track_set_key_transition", track, key, val); undo_redo->add_undo_method(animation.ptr(), "track_set_key_transition", track, key, prev_val); update_obj = true; } EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); switch (animation->track_get_type(track)) { case Animation::TYPE_POSITION_3D: case Animation::TYPE_ROTATION_3D: case Animation::TYPE_SCALE_3D: { Variant old = animation->track_get_key_value(track, key); if (!setting) { String action_name; switch (animation->track_get_type(track)) { case Animation::TYPE_POSITION_3D: action_name = TTR("Animation Multi Change Position3D"); break; case Animation::TYPE_ROTATION_3D: action_name = TTR("Animation Multi Change Rotation3D"); break; case Animation::TYPE_SCALE_3D: action_name = TTR("Animation Multi Change Scale3D"); break; default: { } } setting = true; undo_redo->create_action(action_name); } undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, p_value); undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, old); update_obj = true; } break; case Animation::TYPE_BLEND_SHAPE: case Animation::TYPE_VALUE: { if (name == "value") { Variant value = p_value; if (value.get_type() == Variant::NODE_PATH) { _fix_node_path(value, base_map[track]); } if (!setting) { setting = true; undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS); } Variant prev = animation->track_get_key_value(track, key); undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, value); undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, prev); update_obj = true; } } break; case Animation::TYPE_METHOD: { Dictionary d_old = animation->track_get_key_value(track, key); Dictionary d_new = d_old.duplicate(); bool mergeable = false; if (name == "name") { d_new["method"] = p_value; } else if (name == "arg_count") { Vector args = d_old["args"]; args.resize(p_value); d_new["args"] = args; change_notify_deserved = true; } else if (name.begins_with("args/")) { Vector args = d_old["args"]; int idx = name.get_slicec('/', 1).to_int(); ERR_FAIL_INDEX_V(idx, args.size(), false); String what = name.get_slicec('/', 2); if (what == "type") { Variant::Type t = Variant::Type(int(p_value)); if (t != args[idx].get_type()) { Callable::CallError err; if (Variant::can_convert_strict(args[idx].get_type(), t)) { Variant old = args[idx]; Variant *ptrs[1] = { &old }; Variant::construct(t, args.write[idx], (const Variant **)ptrs, 1, err); } else { Variant::construct(t, args.write[idx], nullptr, 0, err); } change_notify_deserved = true; d_new["args"] = args; } } else if (what == "value") { Variant value = p_value; if (value.get_type() == Variant::NODE_PATH) { _fix_node_path(value, base_map[track]); } args.write[idx] = value; d_new["args"] = args; mergeable = true; } } Variant prev = animation->track_get_key_value(track, key); if (!setting) { if (mergeable) { undo_redo->create_action(TTR("Animation Multi Change Call"), UndoRedo::MERGE_ENDS); } else { undo_redo->create_action(TTR("Animation Multi Change Call")); } setting = true; } undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, d_new); undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, d_old); update_obj = true; } break; case Animation::TYPE_BEZIER: { if (name == "value") { const Variant &value = p_value; if (!setting) { setting = true; undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS); } float prev = animation->bezier_track_get_key_value(track, key); undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_value", track, key, value); undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_value", track, key, prev); update_obj = true; } else if (name == "in_handle") { const Variant &value = p_value; if (!setting) { setting = true; undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS); } Vector2 prev = animation->bezier_track_get_key_in_handle(track, key); undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, value); undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, prev); update_obj = true; } else if (name == "out_handle") { const Variant &value = p_value; if (!setting) { setting = true; undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS); } Vector2 prev = animation->bezier_track_get_key_out_handle(track, key); undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, value); undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, prev); update_obj = true; } else if (name == "handle_mode") { const Variant &value = p_value; if (!setting) { setting = true; undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS, animation.ptr()); } int prev_mode = animation->bezier_track_get_key_handle_mode(track, key); Vector2 prev_in_handle = animation->bezier_track_get_key_in_handle(track, key); Vector2 prev_out_handle = animation->bezier_track_get_key_out_handle(track, key); undo_redo->add_do_method(editor, "_bezier_track_set_key_handle_mode", animation.ptr(), track, key, value); undo_redo->add_undo_method(editor, "_bezier_track_set_key_handle_mode", animation.ptr(), track, key, prev_mode); undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_in_handle", track, key, prev_in_handle); undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, prev_out_handle); update_obj = true; } } break; case Animation::TYPE_AUDIO: { if (name == "stream") { Ref stream = p_value; if (!setting) { setting = true; undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS); } Ref prev = animation->audio_track_get_key_stream(track, key); undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_stream", track, key, stream); undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_stream", track, key, prev); update_obj = true; } else if (name == "start_offset") { float value = p_value; if (!setting) { setting = true; undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS); } float prev = animation->audio_track_get_key_start_offset(track, key); undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_start_offset", track, key, value); undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_start_offset", track, key, prev); update_obj = true; } else if (name == "end_offset") { float value = p_value; if (!setting) { setting = true; undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS); } float prev = animation->audio_track_get_key_end_offset(track, key); undo_redo->add_do_method(animation.ptr(), "audio_track_set_key_end_offset", track, key, value); undo_redo->add_undo_method(animation.ptr(), "audio_track_set_key_end_offset", track, key, prev); update_obj = true; } } break; case Animation::TYPE_ANIMATION: { if (name == "animation") { StringName anim_name = p_value; if (!setting) { setting = true; undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS); } StringName prev = animation->animation_track_get_key_animation(track, key); undo_redo->add_do_method(animation.ptr(), "animation_track_set_key_animation", track, key, anim_name); undo_redo->add_undo_method(animation.ptr(), "animation_track_set_key_animation", track, key, prev); update_obj = true; } } break; } } } EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); if (setting) { if (update_obj) { undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(this, "_update_obj", animation); } undo_redo->commit_action(); setting = false; if (change_notify_deserved) { notify_change(); } return true; } return false; } bool AnimationMultiTrackKeyEdit::_get(const StringName &p_name, Variant &r_ret) const { for (const KeyValue> &E : key_ofs_map) { int track = E.key; for (const float &key_ofs : E.value) { int key = animation->track_find_key(track, key_ofs, Animation::FIND_MODE_APPROX); ERR_CONTINUE(key == -1); String name = p_name; if (name == "easing") { r_ret = animation->track_get_key_transition(track, key); return true; } switch (animation->track_get_type(track)) { case Animation::TYPE_POSITION_3D: case Animation::TYPE_ROTATION_3D: case Animation::TYPE_SCALE_3D: { if (name == "position" || name == "rotation" || name == "scale") { r_ret = animation->track_get_key_value(track, key); return true; } } break; case Animation::TYPE_BLEND_SHAPE: case Animation::TYPE_VALUE: { if (name == "value") { r_ret = animation->track_get_key_value(track, key); return true; } } break; case Animation::TYPE_METHOD: { Dictionary d = animation->track_get_key_value(track, key); if (name == "name") { ERR_FAIL_COND_V(!d.has("method"), false); r_ret = d["method"]; return true; } ERR_FAIL_COND_V(!d.has("args"), false); Vector args = d["args"]; if (name == "arg_count") { r_ret = args.size(); return true; } if (name.begins_with("args/")) { int idx = name.get_slicec('/', 1).to_int(); ERR_FAIL_INDEX_V(idx, args.size(), false); String what = name.get_slicec('/', 2); if (what == "type") { r_ret = args[idx].get_type(); return true; } if (what == "value") { r_ret = args[idx]; return true; } } } break; case Animation::TYPE_BEZIER: { if (name == "value") { r_ret = animation->bezier_track_get_key_value(track, key); return true; } if (name == "in_handle") { r_ret = animation->bezier_track_get_key_in_handle(track, key); return true; } if (name == "out_handle") { r_ret = animation->bezier_track_get_key_out_handle(track, key); return true; } if (name == "handle_mode") { r_ret = animation->bezier_track_get_key_handle_mode(track, key); return true; } } break; case Animation::TYPE_AUDIO: { if (name == "stream") { r_ret = animation->audio_track_get_key_stream(track, key); return true; } if (name == "start_offset") { r_ret = animation->audio_track_get_key_start_offset(track, key); return true; } if (name == "end_offset") { r_ret = animation->audio_track_get_key_end_offset(track, key); return true; } } break; case Animation::TYPE_ANIMATION: { if (name == "animation") { r_ret = animation->animation_track_get_key_animation(track, key); return true; } } break; } } } return false; } void AnimationMultiTrackKeyEdit::_get_property_list(List *p_list) const { if (animation.is_null()) { return; } int first_track = -1; float first_key = -1.0; bool same_track_type = true; bool same_key_type = true; for (const KeyValue> &E : key_ofs_map) { int track = E.key; ERR_FAIL_INDEX(track, animation->get_track_count()); if (first_track < 0) { first_track = track; } if (same_track_type) { if (animation->track_get_type(first_track) != animation->track_get_type(track)) { same_track_type = false; same_key_type = false; } for (const float &F : E.value) { int key = animation->track_find_key(track, F, Animation::FIND_MODE_APPROX); ERR_FAIL_COND(key == -1); if (first_key < 0) { first_key = key; } if (animation->track_get_key_value(first_track, first_key).get_type() != animation->track_get_key_value(track, key).get_type()) { same_key_type = false; } } } } if (same_track_type) { switch (animation->track_get_type(first_track)) { case Animation::TYPE_POSITION_3D: { p_list->push_back(PropertyInfo(Variant::VECTOR3, "position")); } break; case Animation::TYPE_ROTATION_3D: { p_list->push_back(PropertyInfo(Variant::QUATERNION, "rotation")); } break; case Animation::TYPE_SCALE_3D: { p_list->push_back(PropertyInfo(Variant::VECTOR3, "scale")); } break; case Animation::TYPE_BLEND_SHAPE: { p_list->push_back(PropertyInfo(Variant::FLOAT, "value")); } break; case Animation::TYPE_VALUE: { if (same_key_type) { Variant v = animation->track_get_key_value(first_track, first_key); if (hint.type != Variant::NIL) { PropertyInfo pi = hint; pi.name = "value"; p_list->push_back(pi); } else { PropertyHint val_hint = PROPERTY_HINT_NONE; String val_hint_string; if (v.get_type() == Variant::OBJECT) { // Could actually check the object property if exists..? Yes I will! Ref res = v; if (res.is_valid()) { val_hint = PROPERTY_HINT_RESOURCE_TYPE; val_hint_string = res->get_class(); } } if (v.get_type() != Variant::NIL) { p_list->push_back(PropertyInfo(v.get_type(), "value", val_hint, val_hint_string)); } } } p_list->push_back(PropertyInfo(Variant::FLOAT, "easing", PROPERTY_HINT_EXP_EASING)); } break; case Animation::TYPE_METHOD: { p_list->push_back(PropertyInfo(Variant::STRING_NAME, "name")); p_list->push_back(PropertyInfo(Variant::INT, "arg_count", PROPERTY_HINT_RANGE, "0,32,1,or_greater")); Dictionary d = animation->track_get_key_value(first_track, first_key); ERR_FAIL_COND(!d.has("args")); Vector args = d["args"]; String vtypes; for (int i = 0; i < Variant::VARIANT_MAX; i++) { if (i > 0) { vtypes += ","; } vtypes += Variant::get_type_name(Variant::Type(i)); } for (int i = 0; i < args.size(); i++) { p_list->push_back(PropertyInfo(Variant::INT, "args/" + itos(i) + "/type", PROPERTY_HINT_ENUM, vtypes)); if (args[i].get_type() != Variant::NIL) { p_list->push_back(PropertyInfo(args[i].get_type(), "args/" + itos(i) + "/value")); } } } break; case Animation::TYPE_BEZIER: { p_list->push_back(PropertyInfo(Variant::FLOAT, "value")); p_list->push_back(PropertyInfo(Variant::VECTOR2, "in_handle")); p_list->push_back(PropertyInfo(Variant::VECTOR2, "out_handle")); p_list->push_back(PropertyInfo(Variant::INT, "handle_mode", PROPERTY_HINT_ENUM, "Free,Linear,Balanced,Mirrored")); } break; case Animation::TYPE_AUDIO: { p_list->push_back(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream")); p_list->push_back(PropertyInfo(Variant::FLOAT, "start_offset", PROPERTY_HINT_RANGE, "0,3600,0.0001,or_greater")); p_list->push_back(PropertyInfo(Variant::FLOAT, "end_offset", PROPERTY_HINT_RANGE, "0,3600,0.0001,or_greater")); } break; case Animation::TYPE_ANIMATION: { if (key_ofs_map.size() > 1) { break; } String animations; if (root_path) { AnimationPlayer *ap = Object::cast_to(root_path->get_node_or_null(animation->track_get_path(first_track))); if (ap) { List anims; ap->get_animation_list(&anims); for (const StringName &anim : anims) { if (!animations.is_empty()) { animations += ","; } animations += String(anim); } } } if (!animations.is_empty()) { animations += ","; } animations += "[stop]"; p_list->push_back(PropertyInfo(Variant::STRING_NAME, "animation", PROPERTY_HINT_ENUM, animations)); } break; } } } void AnimationMultiTrackKeyEdit::notify_change() { notify_property_list_changed(); } Node *AnimationMultiTrackKeyEdit::get_root_path() { return root_path; } void AnimationMultiTrackKeyEdit::set_use_fps(bool p_enable) { use_fps = p_enable; notify_property_list_changed(); } void AnimationTimelineEdit::_zoom_changed(double) { double zoom_pivot = 0; // Point on timeline to stay fixed. double zoom_pivot_delta = 0; // Delta seconds from left-most point on timeline to zoom pivot. int timeline_width_pixels = get_size().width - get_buttons_width() - get_name_limit(); double timeline_width_seconds = timeline_width_pixels / last_zoom_scale; // Length (in seconds) of visible part of timeline before zoom. double updated_timeline_width_seconds = timeline_width_pixels / get_zoom_scale(); // Length after zoom. double updated_timeline_half_width = updated_timeline_width_seconds / 2.0; bool zooming = updated_timeline_width_seconds < timeline_width_seconds; double timeline_left = get_value(); double timeline_right = timeline_left + timeline_width_seconds; double timeline_center = timeline_left + timeline_width_seconds / 2.0; if (zoom_callback_occurred) { // Zooming with scroll wheel will focus on the position of the mouse. double zoom_scroll_origin_norm = (zoom_scroll_origin.x - get_name_limit()) / timeline_width_pixels; zoom_scroll_origin_norm = MAX(zoom_scroll_origin_norm, 0); zoom_pivot = timeline_left + timeline_width_seconds * zoom_scroll_origin_norm; zoom_pivot_delta = updated_timeline_width_seconds * zoom_scroll_origin_norm; zoom_callback_occurred = false; } else { // Zooming with slider will depend on the current play position. // If the play position is not in range, or exactly in the center, zoom in on the center. if (get_play_position() < timeline_left || get_play_position() > timeline_left + timeline_width_seconds || get_play_position() == timeline_center) { zoom_pivot = timeline_center; zoom_pivot_delta = updated_timeline_half_width; } // Zoom from right if play position is right of center, // and shrink from right if play position is left of center. else if ((get_play_position() > timeline_center) == zooming) { // If play position crosses to other side of center, center it. bool center_passed = (get_play_position() < timeline_right - updated_timeline_half_width) == zooming; zoom_pivot = center_passed ? get_play_position() : timeline_right; double center_offset = CMP_EPSILON * (zooming ? 1 : -1); // Small offset to prevent crossover. zoom_pivot_delta = center_passed ? updated_timeline_half_width + center_offset : updated_timeline_width_seconds; } // Zoom from left if play position is left of center, // and shrink from left if play position is right of center. else if ((get_play_position() <= timeline_center) == zooming) { // If play position crosses to other side of center, center it. bool center_passed = (get_play_position() > timeline_left + updated_timeline_half_width) == zooming; zoom_pivot = center_passed ? get_play_position() : timeline_left; double center_offset = CMP_EPSILON * (zooming ? -1 : 1); // Small offset to prevent crossover. zoom_pivot_delta = center_passed ? updated_timeline_half_width + center_offset : 0; } } double hscroll_pos = zoom_pivot - zoom_pivot_delta; hscroll_pos = CLAMP(hscroll_pos, hscroll->get_min(), hscroll->get_max()); hscroll->set_value(hscroll_pos); hscroll_on_zoom_buffer = hscroll_pos; // In case of page update. last_zoom_scale = get_zoom_scale(); queue_redraw(); play_position->queue_redraw(); emit_signal(SNAME("zoom_changed")); } float AnimationTimelineEdit::get_zoom_scale() const { return _get_zoom_scale(zoom->get_value()); } float AnimationTimelineEdit::_get_zoom_scale(double p_zoom_value) const { float zv = zoom->get_max() - p_zoom_value; if (zv < 1) { zv = 1.0 - zv; return Math::pow(1.0f + zv, 8.0f) * 100; } else { return 1.0 / Math::pow(zv, 8.0f) * 100; } } void AnimationTimelineEdit::_anim_length_changed(double p_new_len) { if (editing) { return; } p_new_len = MAX(SECOND_DECIMAL, p_new_len); if (use_fps && animation->get_step() > 0) { p_new_len *= animation->get_step(); } editing = true; EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Change Animation Length"), UndoRedo::MERGE_ENDS); undo_redo->add_do_method(animation.ptr(), "set_length", p_new_len); undo_redo->add_undo_method(animation.ptr(), "set_length", animation->get_length()); undo_redo->commit_action(); editing = false; queue_redraw(); emit_signal(SNAME("length_changed"), p_new_len); } void AnimationTimelineEdit::_anim_loop_pressed() { if (!read_only) { EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Change Animation Loop")); switch (animation->get_loop_mode()) { case Animation::LOOP_NONE: { undo_redo->add_do_method(animation.ptr(), "set_loop_mode", Animation::LOOP_LINEAR); } break; case Animation::LOOP_LINEAR: { undo_redo->add_do_method(animation.ptr(), "set_loop_mode", Animation::LOOP_PINGPONG); } break; case Animation::LOOP_PINGPONG: { undo_redo->add_do_method(animation.ptr(), "set_loop_mode", Animation::LOOP_NONE); } break; default: break; } undo_redo->add_do_method(this, "update_values"); undo_redo->add_undo_method(animation.ptr(), "set_loop_mode", animation->get_loop_mode()); undo_redo->add_undo_method(this, "update_values"); undo_redo->commit_action(); } else { String base = animation->get_path(); int srpos = base.find("::"); if (srpos != -1) { base = animation->get_path().substr(0, srpos); } if (FileAccess::exists(base + ".import")) { if (ResourceLoader::get_resource_type(base) == "PackedScene") { EditorNode::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.")); } else { EditorNode::get_singleton()->show_warning(TTR("Can't change loop mode on animation instanced from an imported resource.")); } } else { if (ResourceLoader::get_resource_type(base) == "PackedScene") { EditorNode::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.")); } else { EditorNode::get_singleton()->show_warning(TTR("Can't change loop mode on animation embedded in another resource.")); } } update_values(); } } int AnimationTimelineEdit::get_buttons_width() const { const Ref interp_mode = get_editor_theme_icon(SNAME("TrackContinuous")); const Ref interp_type = get_editor_theme_icon(SNAME("InterpRaw")); const Ref loop_type = get_editor_theme_icon(SNAME("InterpWrapClamp")); const Ref remove_icon = get_editor_theme_icon(SNAME("Remove")); const Ref down_icon = get_theme_icon(SNAME("select_arrow"), SNAME("Tree")); const int h_separation = get_theme_constant(SNAME("h_separation"), SNAME("AnimationTrackEdit")); int total_w = interp_mode->get_width() + interp_type->get_width() + loop_type->get_width() + remove_icon->get_width(); total_w += (down_icon->get_width() + h_separation) * 4; return total_w; } int AnimationTimelineEdit::get_name_limit() const { Ref hsize_icon = get_editor_theme_icon(SNAME("Hsize")); int filter_track_width = filter_track->is_visible() ? filter_track->get_custom_minimum_size().width : 0; int limit = MAX(name_limit, add_track->get_minimum_size().width + hsize_icon->get_width() + filter_track_width + 16 * EDSCALE); limit = MIN(limit, get_size().width - get_buttons_width() - 1); return limit; } void AnimationTimelineEdit::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: { add_track->set_button_icon(get_editor_theme_icon(SNAME("Add"))); loop->set_button_icon(get_editor_theme_icon(SNAME("Loop"))); time_icon->set_texture(get_editor_theme_icon(SNAME("Time"))); filter_track->set_right_icon(get_editor_theme_icon(SNAME("Search"))); add_track->get_popup()->clear(); add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyValue")), TTR("Property Track...")); add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyXPosition")), TTR("3D Position Track...")); add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyXRotation")), TTR("3D Rotation Track...")); add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyXScale")), TTR("3D Scale Track...")); add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyBlendShape")), TTR("Blend Shape Track...")); add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyCall")), TTR("Call Method Track...")); add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyBezier")), TTR("Bezier Curve Track...")); add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyAudio")), TTR("Audio Playback Track...")); add_track->get_popup()->add_icon_item(get_editor_theme_icon(SNAME("KeyAnimation")), TTR("Animation Playback Track...")); } break; case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { if (!EditorSettings::get_singleton()->check_changed_settings_in_group("editors/panning")) { break; } [[fallthrough]]; } case NOTIFICATION_ENTER_TREE: { panner->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"))); panner->setup_warped_panning(get_viewport(), EDITOR_GET("editors/panning/warped_mouse_panning")); } break; case NOTIFICATION_RESIZED: { len_hb->set_position(Vector2(get_size().width - get_buttons_width(), 0)); len_hb->set_size(Size2(get_buttons_width(), get_size().height)); int hsize_icon_width = get_editor_theme_icon(SNAME("Hsize"))->get_width(); add_track_hb->set_size(Size2(name_limit - ((hsize_icon_width + 16) * EDSCALE), 0)); } break; case NOTIFICATION_ACCESSIBILITY_UPDATE: { RID ae = get_accessibility_element(); ERR_FAIL_COND(ae.is_null()); //TODO DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_STATIC_TEXT); DisplayServer::get_singleton()->accessibility_update_set_value(ae, TTR(vformat("The %s is not accessible at this time.", "Animation timeline editor"))); } break; case NOTIFICATION_DRAW: { int key_range = get_size().width - get_buttons_width() - get_name_limit(); if (animation.is_null()) { return; } const Ref font = get_theme_font(SceneStringName(font), SNAME("Label")); const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label")); const Ref &stylebox_time_unavailable = get_theme_stylebox(SNAME("time_unavailable"), SNAME("AnimationTimelineEdit")); const Ref &stylebox_time_available = get_theme_stylebox(SNAME("time_available"), SNAME("AnimationTimelineEdit")); const Color v_line_primary_color = get_theme_color(SNAME("v_line_primary_color"), SNAME("AnimationTimelineEdit")); const Color v_line_secondary_color = get_theme_color(SNAME("v_line_secondary_color"), SNAME("AnimationTimelineEdit")); const Color h_line_color = get_theme_color(SNAME("h_line_color"), SNAME("AnimationTimelineEdit")); const Color font_primary_color = get_theme_color(SNAME("font_primary_color"), SNAME("AnimationTimelineEdit")); const Color font_secondary_color = get_theme_color(SNAME("font_secondary_color"), SNAME("AnimationTimelineEdit")); const int v_line_primary_margin = get_theme_constant(SNAME("v_line_primary_margin"), SNAME("AnimationTimelineEdit")); const int v_line_secondary_margin = get_theme_constant(SNAME("v_line_secondary_margin"), SNAME("AnimationTimelineEdit")); const int v_line_primary_width = get_theme_constant(SNAME("v_line_primary_width"), SNAME("AnimationTimelineEdit")); const int v_line_secondary_width = get_theme_constant(SNAME("v_line_secondary_width"), SNAME("AnimationTimelineEdit")); const int text_primary_margin = get_theme_constant(SNAME("text_primary_margin"), SNAME("AnimationTimelineEdit")); const int text_secondary_margin = get_theme_constant(SNAME("text_secondary_margin"), SNAME("AnimationTimelineEdit")); int zoomw = key_range; float scale = get_zoom_scale(); int h = get_size().height; float l = animation->get_length(); if (l <= 0) { l = SECOND_DECIMAL; // Avoid crashor. } Ref hsize_icon = get_editor_theme_icon(SNAME("Hsize")); hsize_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()); draw_texture(hsize_icon, hsize_rect.position); { float time_min = 0; float time_max = animation->get_length(); for (int i = 0; i < animation->get_track_count(); i++) { if (animation->track_get_key_count(i) > 0) { float beg = animation->track_get_key_time(i, 0); if (beg < time_min) { time_min = beg; } float end = animation->track_get_key_time(i, animation->track_get_key_count(i) - 1); if (end > time_max) { time_max = end; } } } PackedStringArray markers = animation->get_marker_names(); if (markers.size() > 0) { float min_marker = animation->get_marker_time(markers[0]); float max_marker = animation->get_marker_time(markers[markers.size() - 1]); if (min_marker < time_min) { time_min = min_marker; } if (max_marker > time_max) { time_max = max_marker; } } float extra = (zoomw / scale) * 0.5; time_max += extra; set_min(time_min); set_max(time_max); if (zoomw / scale < (time_max - time_min)) { hscroll->show(); } else { hscroll->hide(); } } set_page(zoomw / scale); if (hscroll->is_visible() && hscroll_on_zoom_buffer >= 0) { hscroll->set_value(hscroll_on_zoom_buffer); hscroll_on_zoom_buffer = -1.0; } int end_px = (l - get_value()) * scale; int begin_px = -get_value() * scale; { draw_style_box(stylebox_time_unavailable, Rect2(Point2(get_name_limit(), 0), Point2(zoomw - 1, h))); if (begin_px < zoomw && end_px > 0) { if (begin_px < 0) { begin_px = 0; } if (end_px > zoomw) { end_px = zoomw; } draw_style_box(stylebox_time_available, Rect2(Point2(get_name_limit() + begin_px, 0), Point2(end_px - begin_px, h))); } } #define SC_ADJ 100 int dec = 1; int step = 1; int decimals = 2; bool step_found = false; const float period_width = font->get_char_size('.', font_size).width; float max_digit_width = font->get_char_size('0', font_size).width; for (int i = 1; i <= 9; i++) { const float digit_width = font->get_char_size('0' + i, font_size).width; max_digit_width = MAX(digit_width, max_digit_width); } const int max_sc = int(Math::ceil(zoomw / scale)); const int max_sc_width = String::num(max_sc).length() * Math::ceil(max_digit_width); const int min_margin = MAX(text_secondary_margin, text_primary_margin); while (!step_found) { int min = max_sc_width; if (decimals > 0) { min += Math::ceil(period_width + max_digit_width * decimals); } min += (min_margin * 2); static const int _multp[3] = { 1, 2, 5 }; for (int i = 0; i < 3; i++) { step = (_multp[i] * dec); if (step * scale / SC_ADJ > min) { step_found = true; break; } } if (step_found) { break; } dec *= 10; decimals--; if (decimals < 0) { decimals = 0; } } if (use_fps) { float step_size = animation->get_step(); if (step_size > 0) { int prev_frame_ofs = -10000000; for (int i = 0; i < zoomw; i++) { float pos = get_value() + double(i) / scale; float prev = get_value() + (double(i) - 1.0) / scale; int frame = pos / step_size; int prev_frame = prev / step_size; bool sub = Math::floor(prev) == Math::floor(pos); if (frame != prev_frame && i >= prev_frame_ofs) { int line_margin = sub ? v_line_secondary_margin : v_line_primary_margin; int line_width = sub ? v_line_secondary_width : v_line_primary_width; Color line_color = sub ? v_line_secondary_color : v_line_primary_color; draw_line(Point2(get_name_limit() + i, 0 + line_margin), Point2(get_name_limit() + i, h - line_margin), line_color, line_width); int text_margin = sub ? text_secondary_margin : text_primary_margin; draw_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); prev_frame_ofs = i + font->get_string_size(itos(frame), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x + text_margin; } } } } else { for (int i = 0; i < zoomw; i++) { float pos = get_value() + double(i) / scale; float prev = get_value() + (double(i) - 1.0) / scale; int sc = int(Math::floor(pos * SC_ADJ)); int prev_sc = int(Math::floor(prev * SC_ADJ)); if ((sc / step) != (prev_sc / step) || (prev_sc < 0 && sc >= 0)) { int scd = sc < 0 ? prev_sc : sc; bool sub = (((scd - (scd % step)) % (dec * 10)) != 0); int line_margin = sub ? v_line_secondary_margin : v_line_primary_margin; int line_width = sub ? v_line_secondary_width : v_line_primary_width; Color line_color = sub ? v_line_secondary_color : v_line_primary_color; draw_line(Point2(get_name_limit() + i, 0 + line_margin), Point2(get_name_limit() + i, h - line_margin), line_color, line_width); int text_margin = sub ? text_secondary_margin : text_primary_margin; draw_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); } } } draw_line(Vector2(0, get_size().height), get_size(), h_line_color, Math::round(EDSCALE)); update_values(); } break; } } void AnimationTimelineEdit::set_animation(const Ref &p_animation, bool p_read_only) { animation = p_animation; read_only = p_read_only; length->set_read_only(read_only); if (animation.is_valid()) { len_hb->show(); filter_track->show(); if (read_only) { add_track->hide(); } else { add_track->show(); } play_position->show(); } else { len_hb->hide(); filter_track->hide(); add_track->hide(); play_position->hide(); } queue_redraw(); } Size2 AnimationTimelineEdit::get_minimum_size() const { Size2 ms = filter_track->get_minimum_size(); const Ref font = get_theme_font(SceneStringName(font), SNAME("Label")); const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label")); ms.height = MAX(ms.height, font->get_height(font_size)); ms.width = get_buttons_width() + add_track->get_minimum_size().width + get_editor_theme_icon(SNAME("Hsize"))->get_width() + 2 + 8 * EDSCALE; return ms; } void AnimationTimelineEdit::set_zoom(Range *p_zoom) { zoom = p_zoom; zoom->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTimelineEdit::_zoom_changed)); } void AnimationTimelineEdit::auto_fit() { if (animation.is_null()) { return; } float anim_end = animation->get_length(); float anim_start = 0; // Search for keyframe outside animation boundaries to include keyframes before animation start and after animation length. int track_count = animation->get_track_count(); for (int track = 0; track < track_count; ++track) { for (int i = 0; i < animation->track_get_key_count(track); i++) { float key_time = animation->track_get_key_time(track, i); if (key_time > anim_end) { anim_end = key_time; } if (key_time < anim_start) { anim_start = key_time; } } } float anim_length = anim_end - anim_start; int timeline_width_pixels = get_size().width - get_buttons_width() - get_name_limit(); // I want a little buffer at the end... (5% looks nice and we should keep some space for the bezier handles) timeline_width_pixels *= 0.95; // The technique is to reuse the _get_zoom_scale function directly to be sure that the auto_fit is always calculated // the same way as the zoom slider. It's a little bit more calculation then doing the inverse of get_zoom_scale but // it's really easier to understand and should always be accurate. float new_zoom = zoom->get_max(); while (true) { double test_zoom_scale = _get_zoom_scale(new_zoom); if (anim_length * test_zoom_scale <= timeline_width_pixels) { // It fits... break; } new_zoom -= zoom->get_step(); if (new_zoom <= zoom->get_min()) { new_zoom = zoom->get_min(); break; } } // Horizontal scroll to get_min which should include keyframes that are before the animation start. hscroll->set_value(hscroll->get_min()); // Set the zoom value... the signal value_changed will be emitted and the timeline will be refreshed correctly! zoom->set_value(new_zoom); // The new zoom value must be applied correctly so the scrollbar are updated before we move the scrollbar to // the beginning of the animation, hence the call deferred. callable_mp(this, &AnimationTimelineEdit::_scroll_to_start).call_deferred(); } void AnimationTimelineEdit::_scroll_to_start() { // Horizontal scroll to get_min which should include keyframes that are before the animation start. hscroll->set_value(hscroll->get_min()); } void AnimationTimelineEdit::set_track_edit(AnimationTrackEdit *p_track_edit) { track_edit = p_track_edit; } void AnimationTimelineEdit::set_play_position(float p_pos) { play_position_pos = p_pos; play_position->queue_redraw(); } float AnimationTimelineEdit::get_play_position() const { return play_position_pos; } void AnimationTimelineEdit::update_play_position() { play_position->queue_redraw(); } void AnimationTimelineEdit::update_values() { if (animation.is_null() || editing) { return; } editing = true; if (use_fps && animation->get_step() > 0.0) { length->set_value(animation->get_length() / animation->get_step()); length->set_step(FPS_DECIMAL); length->set_tooltip_text(TTR("Animation length (frames)")); time_icon->set_tooltip_text(TTR("Animation length (frames)")); if (track_edit) { track_edit->editor->_update_key_edit(); track_edit->editor->marker_edit->_update_key_edit(); } } else { length->set_value(animation->get_length()); length->set_step(SECOND_DECIMAL); length->set_tooltip_text(TTR("Animation length (seconds)")); time_icon->set_tooltip_text(TTR("Animation length (seconds)")); } switch (animation->get_loop_mode()) { case Animation::LOOP_NONE: { loop->set_button_icon(get_editor_theme_icon(SNAME("Loop"))); loop->set_pressed(false); } break; case Animation::LOOP_LINEAR: { loop->set_button_icon(get_editor_theme_icon(SNAME("Loop"))); loop->set_pressed(true); } break; case Animation::LOOP_PINGPONG: { loop->set_button_icon(get_editor_theme_icon(SNAME("PingPongLoop"))); loop->set_pressed(true); } break; default: break; } editing = false; } void AnimationTimelineEdit::_play_position_draw() { if (animation.is_null() || play_position_pos < 0) { return; } float scale = get_zoom_scale(); int h = play_position->get_size().height; int px = (-get_value() + play_position_pos) * scale + get_name_limit(); if (px >= get_name_limit() && px < (play_position->get_size().width - get_buttons_width())) { Color color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); play_position->draw_line(Point2(px, 0), Point2(px, h), color, Math::round(2 * EDSCALE)); play_position->draw_texture( get_editor_theme_icon(SNAME("TimelineIndicator")), Point2(px - get_editor_theme_icon(SNAME("TimelineIndicator"))->get_width() * 0.5, 0), color); } } void AnimationTimelineEdit::gui_input(const Ref &p_event) { ERR_FAIL_COND(p_event.is_null()); if (panner->gui_input(p_event, get_global_rect())) { accept_event(); return; } const Ref mb = p_event; if (mb.is_valid() && mb->is_pressed() && mb->is_alt_pressed() && mb->get_button_index() == MouseButton::WHEEL_UP) { if (track_edit) { track_edit->get_editor()->goto_prev_step(true); } accept_event(); } if (mb.is_valid() && mb->is_pressed() && mb->is_alt_pressed() && mb->get_button_index() == MouseButton::WHEEL_DOWN) { if (track_edit) { track_edit->get_editor()->goto_next_step(true); } accept_event(); } if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && hsize_rect.has_point(mb->get_position())) { dragging_hsize = true; dragging_hsize_from = mb->get_position().x; dragging_hsize_at = name_limit; } if (mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && dragging_hsize) { dragging_hsize = false; } if (mb.is_valid() && mb->get_position().x > get_name_limit() && mb->get_position().x < (get_size().width - get_buttons_width())) { if (!panner->is_panning() && mb->get_button_index() == MouseButton::LEFT) { int x = mb->get_position().x - get_name_limit(); float ofs = x / get_zoom_scale() + get_value(); emit_signal(SNAME("timeline_changed"), ofs, mb->is_alt_pressed()); dragging_timeline = true; } } if (dragging_timeline && mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && !mb->is_pressed()) { dragging_timeline = false; } Ref mm = p_event; if (mm.is_valid()) { if (dragging_hsize) { int ofs = mm->get_position().x - dragging_hsize_from; name_limit = dragging_hsize_at + ofs; // Make sure name_limit is clamped to the range that UI allows. name_limit = get_name_limit(); int hsize_icon_width = get_editor_theme_icon(SNAME("Hsize"))->get_width(); add_track_hb->set_size(Size2(name_limit - ((hsize_icon_width + 16) * EDSCALE), 0)); queue_redraw(); emit_signal(SNAME("name_limit_changed")); play_position->queue_redraw(); } if (dragging_timeline) { int x = mm->get_position().x - get_name_limit(); float ofs = x / get_zoom_scale() + get_value(); emit_signal(SNAME("timeline_changed"), ofs, mm->is_alt_pressed()); } } } Control::CursorShape AnimationTimelineEdit::get_cursor_shape(const Point2 &p_pos) const { if (dragging_hsize || hsize_rect.has_point(p_pos)) { // Indicate that the track name column's width can be adjusted return Control::CURSOR_HSIZE; } else { return get_default_cursor_shape(); } } void AnimationTimelineEdit::_pan_callback(Vector2 p_scroll_vec, Ref p_event) { set_value(get_value() - p_scroll_vec.x / get_zoom_scale()); } void AnimationTimelineEdit::_zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref p_event) { double current_zoom_value = get_zoom()->get_value(); zoom_scroll_origin = p_origin; zoom_callback_occurred = true; get_zoom()->set_value(MAX(0.01, current_zoom_value - (1.0 - p_zoom_factor))); } void AnimationTimelineEdit::set_use_fps(bool p_use_fps) { use_fps = p_use_fps; queue_redraw(); } bool AnimationTimelineEdit::is_using_fps() const { return use_fps; } void AnimationTimelineEdit::set_hscroll(HScrollBar *p_hscroll) { hscroll = p_hscroll; } void AnimationTimelineEdit::_track_added(int p_track) { emit_signal(SNAME("track_added"), p_track); } void AnimationTimelineEdit::_bind_methods() { ADD_SIGNAL(MethodInfo("zoom_changed")); ADD_SIGNAL(MethodInfo("name_limit_changed")); ADD_SIGNAL(MethodInfo("timeline_changed", PropertyInfo(Variant::FLOAT, "position"), PropertyInfo(Variant::BOOL, "timeline_only"))); ADD_SIGNAL(MethodInfo("track_added", PropertyInfo(Variant::INT, "track"))); ADD_SIGNAL(MethodInfo("length_changed", PropertyInfo(Variant::FLOAT, "size"))); ADD_SIGNAL(MethodInfo("filter_changed")); ClassDB::bind_method(D_METHOD("update_values"), &AnimationTimelineEdit::update_values); } AnimationTimelineEdit::AnimationTimelineEdit() { name_limit = 150 * EDSCALE; play_position = memnew(Control); play_position->set_mouse_filter(MOUSE_FILTER_PASS); add_child(play_position); play_position->set_anchors_and_offsets_preset(PRESET_FULL_RECT); play_position->connect(SceneStringName(draw), callable_mp(this, &AnimationTimelineEdit::_play_position_draw)); add_track_hb = memnew(HBoxContainer); add_child(add_track_hb); add_track = memnew(MenuButton); add_track->set_tooltip_text(TTR("Select a new track by type to add to this animation.")); add_track->set_position(Vector2(0, 0)); add_track_hb->add_child(add_track); filter_track = memnew(LineEdit); filter_track->set_h_size_flags(SIZE_EXPAND_FILL); filter_track->set_custom_minimum_size(Vector2(120 * EDSCALE, 0)); filter_track->set_placeholder(TTR("Filter Tracks")); filter_track->set_tooltip_text(TTR("Filter tracks by entering part of their node name or property.")); filter_track->connect(SceneStringName(text_changed), callable_mp((AnimationTrackEditor *)this, &AnimationTrackEditor::_on_filter_updated)); filter_track->set_clear_button_enabled(true); filter_track->hide(); add_track_hb->add_child(filter_track); len_hb = memnew(HBoxContainer); Control *expander = memnew(Control); expander->set_h_size_flags(SIZE_EXPAND_FILL); expander->set_mouse_filter(MOUSE_FILTER_IGNORE); len_hb->add_child(expander); time_icon = memnew(TextureRect); time_icon->set_v_size_flags(SIZE_SHRINK_CENTER); time_icon->set_tooltip_text(TTR("Animation length (seconds)")); len_hb->add_child(time_icon); length = memnew(EditorSpinSlider); length->set_min(SECOND_DECIMAL); length->set_max(36000); length->set_step(SECOND_DECIMAL); length->set_allow_greater(true); length->set_custom_minimum_size(Vector2(70 * EDSCALE, 0)); length->set_hide_slider(true); length->set_tooltip_text(TTR("Animation length (seconds)")); length->set_accessibility_name(TTRC("Animation length (seconds)")); length->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTimelineEdit::_anim_length_changed)); len_hb->add_child(length); loop = memnew(Button); loop->set_flat(true); loop->set_tooltip_text(TTR("Animation Looping")); loop->connect(SceneStringName(pressed), callable_mp(this, &AnimationTimelineEdit::_anim_loop_pressed)); loop->set_toggle_mode(true); len_hb->add_child(loop); add_child(len_hb); add_track->hide(); add_track->get_popup()->connect("index_pressed", callable_mp(this, &AnimationTimelineEdit::_track_added)); len_hb->hide(); panner.instantiate(); panner->set_scroll_zoom_factor(SCROLL_ZOOM_FACTOR_IN); panner->set_callbacks(callable_mp(this, &AnimationTimelineEdit::_pan_callback), callable_mp(this, &AnimationTimelineEdit::_zoom_callback)); panner->set_pan_axis(ViewPanner::PAN_AXIS_HORIZONTAL); set_layout_direction(Control::LAYOUT_DIRECTION_LTR); } //////////////////////////////////// void AnimationTrackEdit::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: { if (animation.is_null()) { return; } ERR_FAIL_INDEX(track, animation->get_track_count()); type_icon = _get_key_type_icon(); selected_icon = get_editor_theme_icon(SNAME("KeySelected")); } break; case NOTIFICATION_ACCESSIBILITY_UPDATE: { RID ae = get_accessibility_element(); ERR_FAIL_COND(ae.is_null()); //TODO DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_STATIC_TEXT); DisplayServer::get_singleton()->accessibility_update_set_value(ae, TTR(vformat("The %s is not accessible at this time.", "Animation track editor"))); } break; case NOTIFICATION_DRAW: { if (animation.is_null()) { return; } ERR_FAIL_INDEX(track, animation->get_track_count()); int limit = timeline->get_name_limit(); const Ref &stylebox_odd = get_theme_stylebox(SNAME("odd"), SNAME("AnimationTrackEdit")); const Ref &stylebox_focus = get_theme_stylebox(SNAME("focus"), SNAME("AnimationTrackEdit")); const Ref &stylebox_hover = get_theme_stylebox(SceneStringName(hover), SNAME("AnimationTrackEdit")); const Color h_line_color = get_theme_color(SNAME("h_line_color"), SNAME("AnimationTrackEdit")); const int h_separation = get_theme_constant(SNAME("h_separation"), SNAME("AnimationTrackEdit")); const int outer_margin = get_theme_constant(SNAME("outer_margin"), SNAME("AnimationTrackEdit")); if (track % 2 == 1) { // Draw a background over odd lines to make long lists of tracks easier to read. draw_style_box(stylebox_odd, Rect2(Point2(1 * EDSCALE, 0), get_size() - Size2(1 * EDSCALE, 0))); } if (hovered) { // Draw hover feedback. draw_style_box(stylebox_hover, Rect2(Point2(1 * EDSCALE, 0), get_size() - Size2(1 * EDSCALE, 0))); } if (has_focus()) { // Offside so the horizontal sides aren't cutoff. draw_style_box(stylebox_focus, Rect2(Point2(1 * EDSCALE, 0), get_size() - Size2(1 * EDSCALE, 0))); } const Ref font = get_theme_font(SceneStringName(font), SNAME("Label")); const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label")); const Color color = get_theme_color(SceneStringName(font_color), SNAME("Label")); const Color dc = get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)); // Names and icons. { Ref check = animation->track_is_enabled(track) ? get_theme_icon(SNAME("checked"), SNAME("CheckBox")) : get_theme_icon(SNAME("unchecked"), SNAME("CheckBox")); int ofs = in_group ? outer_margin : 0; check_rect = Rect2(Point2(ofs, (get_size().height - check->get_height()) / 2).round(), check->get_size()); draw_texture(check, check_rect.position); ofs += check->get_width() + h_separation; Ref key_type_icon = _get_key_type_icon(); draw_texture(key_type_icon, Point2(ofs, (get_size().height - key_type_icon->get_height()) / 2).round()); ofs += key_type_icon->get_width() + h_separation; NodePath anim_path = animation->track_get_path(track); Node *node = nullptr; if (root) { node = root->get_node_or_null(anim_path); } String text; Color text_color = color; if (node && EditorNode::get_singleton()->get_editor_selection()->is_selected(node)) { text_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); } if (in_group) { if (animation->track_get_type(track) == Animation::TYPE_METHOD) { text = TTR("Functions:"); } else if (animation->track_get_type(track) == Animation::TYPE_AUDIO) { text = TTR("Audio Clips:"); } else if (animation->track_get_type(track) == Animation::TYPE_ANIMATION) { text = TTR("Animation Clips:"); } else { text += anim_path.get_concatenated_subnames(); } text_color.a *= 0.7; } else if (node) { Ref icon = EditorNode::get_singleton()->get_object_icon(node, "Node"); const Vector2 icon_size = Vector2(1, 1) * get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor)); icon_rect = Rect2(Point2(ofs, (get_size().height - check->get_height()) / 2).round(), icon->get_size()); draw_texture_rect(icon, icon_rect); icon_cache = icon; text = String() + node->get_name() + ":" + anim_path.get_concatenated_subnames(); ofs += h_separation; ofs += icon_size.x; } else { icon_cache = key_type_icon; text = String(anim_path); } path_cache = text; path_rect = Rect2(ofs, 0, limit - ofs - h_separation, get_size().height); Vector2 string_pos = Point2(ofs, (get_size().height - font->get_height(font_size)) / 2 + font->get_ascent(font_size)); string_pos = string_pos.floor(); draw_string(font, string_pos, text, HORIZONTAL_ALIGNMENT_LEFT, limit - ofs - h_separation, font_size, text_color); draw_line(Point2(limit, 0), Point2(limit, get_size().height), h_line_color, Math::round(EDSCALE)); } // Marker sections. { float scale = timeline->get_zoom_scale(); int limit_end = get_size().width - timeline->get_buttons_width(); PackedStringArray section = editor->get_selected_section(); if (section.size() == 2) { StringName start_marker = section[0]; StringName end_marker = section[1]; double start_time = animation->get_marker_time(start_marker); double end_time = animation->get_marker_time(end_marker); // When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section. AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); if (editor->is_marker_moving_selection() && !(player && player->is_playing())) { start_time += editor->get_marker_moving_selection_offset(); end_time += editor->get_marker_moving_selection_offset(); } if (start_time < animation->get_length() && end_time >= 0) { float start_ofs = MAX(0, start_time) - timeline->get_value(); float end_ofs = MIN(animation->get_length(), end_time) - timeline->get_value(); start_ofs = start_ofs * scale + limit; end_ofs = end_ofs * scale + limit; start_ofs = MAX(start_ofs, limit); end_ofs = MIN(end_ofs, limit_end); Rect2 rect; rect.set_position(Vector2(start_ofs, 0)); rect.set_size(Vector2(end_ofs - start_ofs, get_size().height)); draw_rect(rect, Color(1, 0.1, 0.1, 0.2)); } } } // Marker overlays. { float scale = timeline->get_zoom_scale(); PackedStringArray markers = animation->get_marker_names(); for (const StringName marker : markers) { double time = animation->get_marker_time(marker); if (editor->is_marker_selected(marker) && editor->is_marker_moving_selection()) { time += editor->get_marker_moving_selection_offset(); } if (time >= 0) { float offset = time - timeline->get_value(); offset = offset * scale + limit; Color marker_color = animation->get_marker_color(marker); marker_color.a = 0.2; draw_line(Point2(offset, 0), Point2(offset, get_size().height), marker_color, Math::round(EDSCALE)); } } } // Keyframes. draw_bg(limit, get_size().width - timeline->get_buttons_width() - outer_margin); { float scale = timeline->get_zoom_scale(); int limit_end = get_size().width - timeline->get_buttons_width() - outer_margin; for (int i = 0; i < animation->track_get_key_count(track); i++) { float offset = animation->track_get_key_time(track, i) - timeline->get_value(); if (editor->is_key_selected(track, i) && editor->is_moving_selection()) { offset = offset + editor->get_moving_selection_offset(); } offset = offset * scale + limit; if (i < animation->track_get_key_count(track) - 1) { float offset_n = animation->track_get_key_time(track, i + 1) - timeline->get_value(); if (editor->is_key_selected(track, i + 1) && editor->is_moving_selection()) { offset_n = offset_n + editor->get_moving_selection_offset(); } offset_n = offset_n * scale + limit; float offset_last = limit_end; if (i < animation->track_get_key_count(track) - 2) { offset_last = animation->track_get_key_time(track, i + 2) - timeline->get_value(); if (editor->is_key_selected(track, i + 2) && editor->is_moving_selection()) { offset_last = offset_last + editor->get_moving_selection_offset(); } offset_last = offset_last * scale + limit; } int limit_string = (editor->is_key_selected(track, i + 1) && editor->is_moving_selection()) ? int(offset_last) : int(offset_n); if (editor->is_key_selected(track, i) && editor->is_moving_selection()) { limit_string = int(MAX(limit_end, offset_last)); } draw_key_link(i, scale, int(offset), int(offset_n), limit, limit_end); draw_key(i, scale, int(offset), editor->is_key_selected(track, i), limit, limit_string); continue; } draw_key(i, scale, int(offset), editor->is_key_selected(track, i), limit, limit_end); } } draw_fg(limit, get_size().width - timeline->get_buttons_width() - outer_margin); // Buttons. { Ref wrap_icon[2] = { get_editor_theme_icon(SNAME("InterpWrapClamp")), get_editor_theme_icon(SNAME("InterpWrapLoop")), }; Ref interp_icon[5] = { get_editor_theme_icon(SNAME("InterpRaw")), get_editor_theme_icon(SNAME("InterpLinear")), get_editor_theme_icon(SNAME("InterpCubic")), get_editor_theme_icon(SNAME("InterpLinearAngle")), get_editor_theme_icon(SNAME("InterpCubicAngle")), }; Ref cont_icon[3] = { get_editor_theme_icon(SNAME("TrackContinuous")), get_editor_theme_icon(SNAME("TrackDiscrete")), get_editor_theme_icon(SNAME("TrackCapture")) }; Ref blend_icon[2] = { get_editor_theme_icon(SNAME("UseBlendEnable")), get_editor_theme_icon(SNAME("UseBlendDisable")), }; int ofs = get_size().width - timeline->get_buttons_width() - outer_margin; const Ref down_icon = get_theme_icon(SNAME("select_arrow"), SNAME("Tree")); draw_line(Point2(ofs, 0), Point2(ofs, get_size().height), h_line_color, Math::round(EDSCALE)); ofs += h_separation; { // Callmode. Animation::UpdateMode update_mode; if (animation->track_get_type(track) == Animation::TYPE_VALUE) { update_mode = animation->value_track_get_update_mode(track); } else { update_mode = Animation::UPDATE_CONTINUOUS; } Ref update_icon = cont_icon[update_mode]; update_mode_rect.position.x = ofs; update_mode_rect.position.y = Math::round((get_size().height - update_icon->get_height()) / 2); update_mode_rect.size = update_icon->get_size(); if (!animation->track_is_compressed(track) && animation->track_get_type(track) == Animation::TYPE_VALUE) { draw_texture(update_icon, update_mode_rect.position); } if (animation->track_get_type(track) == Animation::TYPE_AUDIO) { Ref use_blend_icon = blend_icon[animation->audio_track_is_use_blend(track) ? 0 : 1]; Vector2 use_blend_icon_pos = update_mode_rect.position + (update_mode_rect.size - use_blend_icon->get_size()) / 2; draw_texture(use_blend_icon, use_blend_icon_pos); } // Make it easier to click. update_mode_rect.position.y = 0; update_mode_rect.size.y = get_size().height; ofs += update_icon->get_width() + h_separation / 2; update_mode_rect.size.x += h_separation / 2; if (!read_only) { if (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_AUDIO) { draw_texture(down_icon, Vector2(ofs, (get_size().height - down_icon->get_height()) / 2).round()); update_mode_rect.size.x += down_icon->get_width(); } else if (animation->track_get_type(track) == Animation::TYPE_BEZIER) { update_mode_rect.size.x += down_icon->get_width(); update_mode_rect = Rect2(); } else { update_mode_rect = Rect2(); } } else { update_mode_rect = Rect2(); } ofs += down_icon->get_width(); draw_line(Point2(ofs + h_separation * 0.5, 0), Point2(ofs + h_separation * 0.5, get_size().height), h_line_color, Math::round(EDSCALE)); ofs += h_separation; } { // Interp. Animation::InterpolationType interp_mode = animation->track_get_interpolation_type(track); Ref icon = interp_icon[interp_mode]; interp_mode_rect.position.x = ofs; interp_mode_rect.position.y = Math::round((get_size().height - icon->get_height()) / 2); interp_mode_rect.size = icon->get_size(); if (!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)) { draw_texture(icon, interp_mode_rect.position); } // Make it easier to click. interp_mode_rect.position.y = 0; interp_mode_rect.size.y = get_size().height; ofs += icon->get_width() + h_separation / 2; interp_mode_rect.size.x += h_separation / 2; if (!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)) { draw_texture(down_icon, Vector2(ofs, (get_size().height - down_icon->get_height()) / 2).round()); interp_mode_rect.size.x += down_icon->get_width(); } else { interp_mode_rect = Rect2(); } ofs += down_icon->get_width(); draw_line(Point2(ofs + h_separation * 0.5, 0), Point2(ofs + h_separation * 0.5, get_size().height), h_line_color, Math::round(EDSCALE)); ofs += h_separation; } { // Loop. bool loop_wrap = animation->track_get_interpolation_loop_wrap(track); Ref icon = wrap_icon[loop_wrap ? 1 : 0]; loop_wrap_rect.position.x = ofs; loop_wrap_rect.position.y = Math::round((get_size().height - icon->get_height()) / 2); loop_wrap_rect.size = icon->get_size(); if (!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)) { draw_texture(icon, loop_wrap_rect.position); } loop_wrap_rect.position.y = 0; loop_wrap_rect.size.y = get_size().height; ofs += icon->get_width() + h_separation / 2; loop_wrap_rect.size.x += h_separation / 2; if (!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)) { draw_texture(down_icon, Vector2(ofs, (get_size().height - down_icon->get_height()) / 2).round()); loop_wrap_rect.size.x += down_icon->get_width(); } else { loop_wrap_rect = Rect2(); } ofs += down_icon->get_width(); draw_line(Point2(ofs + h_separation * 0.5, 0), Point2(ofs + h_separation * 0.5, get_size().height), h_line_color, Math::round(EDSCALE)); ofs += h_separation; } { // Erase. Ref icon = get_editor_theme_icon(animation->track_is_compressed(track) ? SNAME("Lock") : SNAME("Remove")); remove_rect.position.x = ofs + ((get_size().width - ofs) - icon->get_width()) - outer_margin; remove_rect.position.y = Math::round((get_size().height - icon->get_height()) / 2); remove_rect.size = icon->get_size(); if (read_only) { draw_texture(icon, remove_rect.position, dc); } else { draw_texture(icon, remove_rect.position); } } } if (in_group) { draw_line(Vector2(timeline->get_name_limit(), get_size().height), get_size(), h_line_color, Math::round(EDSCALE)); } else { draw_line(Vector2(0, get_size().height), get_size(), h_line_color, Math::round(EDSCALE)); } if (dropping_at != 0) { Color drop_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); if (dropping_at < 0) { draw_line(Vector2(0, 0), Vector2(get_size().width, 0), drop_color, Math::round(EDSCALE)); } else { draw_line(Vector2(0, get_size().height), get_size(), drop_color, Math::round(EDSCALE)); } } } break; case NOTIFICATION_MOUSE_ENTER: hovered = true; queue_redraw(); break; case NOTIFICATION_MOUSE_EXIT: hovered = false; // When the mouse cursor exits the track, we're no longer hovering any keyframe. hovering_key_idx = -1; queue_redraw(); [[fallthrough]]; case NOTIFICATION_DRAG_END: { cancel_drop(); } break; } } int AnimationTrackEdit::get_key_height() const { if (animation.is_null()) { return 0; } return type_icon->get_height(); } Rect2 AnimationTrackEdit::get_key_rect(int p_index, float p_pixels_sec) { if (animation.is_null()) { return Rect2(); } Rect2 rect = Rect2(-type_icon->get_width() / 2, 0, type_icon->get_width(), get_size().height); // Make it a big easier to click. rect.position.x -= rect.size.x * 0.5; rect.size.x *= 2; return rect; } bool AnimationTrackEdit::is_key_selectable_by_distance() const { return true; } void 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) { if (p_next_x < p_clip_left) { return; } if (p_x > p_clip_right) { return; } Variant current = animation->track_get_key_value(get_track(), p_index); Variant next = animation->track_get_key_value(get_track(), p_index + 1); if (current != next || animation->track_get_type(get_track()) == Animation::TrackType::TYPE_METHOD) { return; } Color color = get_theme_color(SceneStringName(font_color), SNAME("Label")); color.a = 0.5; int from_x = MAX(p_x, p_clip_left); int to_x = MIN(p_next_x, p_clip_right); draw_line(Point2(from_x + 1, get_size().height / 2), Point2(to_x, get_size().height / 2), color, Math::round(2 * EDSCALE)); } void AnimationTrackEdit::draw_key(int p_index, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right) { if (animation.is_null()) { return; } if (p_x < p_clip_left || p_x > p_clip_right) { return; } Ref icon_to_draw = p_selected ? selected_icon : type_icon; if (animation->track_get_type(track) == Animation::TYPE_VALUE && !Math::is_equal_approx(animation->track_get_key_transition(track, p_index), real_t(1.0))) { // Use a different icon for keys with non-linear easing. icon_to_draw = get_editor_theme_icon(p_selected ? SNAME("KeyEasedSelected") : SNAME("KeyValueEased")); } // Override type icon for invalid value keys, unless selected. if (!p_selected && animation->track_get_type(track) == Animation::TYPE_VALUE) { const Variant &v = animation->track_get_key_value(track, p_index); Variant::Type valid_type = Variant::NIL; if (!_is_value_key_valid(v, valid_type)) { icon_to_draw = get_editor_theme_icon(SNAME("KeyInvalid")); } } Vector2 ofs(p_x - icon_to_draw->get_width() / 2, (get_size().height - icon_to_draw->get_height()) / 2); if (animation->track_get_type(track) == Animation::TYPE_METHOD) { const Ref font = get_theme_font(SceneStringName(font), SNAME("Label")); const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label")); Color color = get_theme_color(SceneStringName(font_color), SNAME("Label")); color.a = 0.5; Dictionary d = animation->track_get_key_value(track, p_index); String text; if (d.has("method")) { text += String(d["method"]); } text += "("; Vector args; if (d.has("args")) { args = d["args"]; } for (int i = 0; i < args.size(); i++) { if (i > 0) { text += ", "; } text += args[i].get_construct_string(); } text += ")"; int 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); if (limit > 0) { draw_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); } } // Use a different color for the currently hovered key. // The color multiplier is chosen to work with both dark and light editor themes, // and on both unselected and selected key icons. draw_texture( icon_to_draw, ofs, p_index == hovering_key_idx ? get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")) : Color(1, 1, 1)); } // Helper. void AnimationTrackEdit::draw_rect_clipped(const Rect2 &p_rect, const Color &p_color, bool p_filled) { int clip_left = timeline->get_name_limit(); int clip_right = get_size().width - timeline->get_buttons_width(); if (p_rect.position.x > clip_right) { return; } if (p_rect.position.x + p_rect.size.x < clip_left) { return; } Rect2 clip = Rect2(clip_left, 0, clip_right - clip_left, get_size().height); draw_rect(clip.intersection(p_rect), p_color, p_filled); } void AnimationTrackEdit::draw_bg(int p_clip_left, int p_clip_right) { } void AnimationTrackEdit::draw_fg(int p_clip_left, int p_clip_right) { } void AnimationTrackEdit::draw_texture_region_clipped(const Ref &p_texture, const Rect2 &p_rect, const Rect2 &p_region) { int clip_left = timeline->get_name_limit(); int clip_right = get_size().width - timeline->get_buttons_width(); // Clip left and right. if (clip_left > p_rect.position.x + p_rect.size.x) { return; } if (clip_right < p_rect.position.x) { return; } Rect2 rect = p_rect; Rect2 region = p_region; if (clip_left > rect.position.x) { int rect_pixels = (clip_left - rect.position.x); int region_pixels = rect_pixels * region.size.x / rect.size.x; rect.position.x += rect_pixels; rect.size.x -= rect_pixels; region.position.x += region_pixels; region.size.x -= region_pixels; } if (clip_right < rect.position.x + rect.size.x) { int rect_pixels = rect.position.x + rect.size.x - clip_right; int region_pixels = rect_pixels * region.size.x / rect.size.x; rect.size.x -= rect_pixels; region.size.x -= region_pixels; } draw_texture_rect_region(p_texture, rect, region); } int AnimationTrackEdit::get_track() const { return track; } Ref AnimationTrackEdit::get_animation() const { return animation; } void AnimationTrackEdit::set_animation_and_track(const Ref &p_animation, int p_track, bool p_read_only) { animation = p_animation; read_only = p_read_only; track = p_track; queue_redraw(); ERR_FAIL_INDEX(track, animation->get_track_count()); node_path = animation->track_get_path(p_track); type_icon = _get_key_type_icon(); selected_icon = get_editor_theme_icon(SNAME("KeySelected")); } NodePath AnimationTrackEdit::get_path() const { return node_path; } Size2 AnimationTrackEdit::get_minimum_size() const { Ref texture = get_editor_theme_icon(SNAME("Object")); const Ref font = get_theme_font(SceneStringName(font), SNAME("Label")); const int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label")); const int separation = get_theme_constant(SNAME("v_separation"), SNAME("ItemList")); int max_h = MAX(texture->get_height(), font->get_height(font_size)); max_h = MAX(max_h, get_key_height()); return Vector2(1, max_h + separation); } void AnimationTrackEdit::set_timeline(AnimationTimelineEdit *p_timeline) { timeline = p_timeline; timeline->set_track_edit(this); timeline->connect("zoom_changed", callable_mp(this, &AnimationTrackEdit::_zoom_changed)); timeline->connect("name_limit_changed", callable_mp(this, &AnimationTrackEdit::_zoom_changed)); } void AnimationTrackEdit::set_editor(AnimationTrackEditor *p_editor) { editor = p_editor; } void AnimationTrackEdit::_play_position_draw() { if (animation.is_null() || play_position_pos < 0) { return; } float scale = timeline->get_zoom_scale(); int h = get_size().height; int px = (-timeline->get_value() + play_position_pos) * scale + timeline->get_name_limit(); if (px >= timeline->get_name_limit() && px < (get_size().width - timeline->get_buttons_width())) { Color color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); play_position->draw_line(Point2(px, 0), Point2(px, h), color, Math::round(2 * EDSCALE)); } } void AnimationTrackEdit::set_play_position(float p_pos) { play_position_pos = p_pos; play_position->queue_redraw(); } void AnimationTrackEdit::update_play_position() { play_position->queue_redraw(); } void AnimationTrackEdit::set_root(Node *p_root) { root = p_root; } void AnimationTrackEdit::_zoom_changed() { queue_redraw(); play_position->queue_redraw(); } void AnimationTrackEdit::_path_submitted(const String &p_text) { EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Change Track Path")); undo_redo->add_do_method(animation.ptr(), "track_set_path", track, p_text); undo_redo->add_undo_method(animation.ptr(), "track_set_path", track, animation->track_get_path(track)); undo_redo->commit_action(); path_popup->hide(); } bool AnimationTrackEdit::_is_value_key_valid(const Variant &p_key_value, Variant::Type &r_valid_type) const { if (root == nullptr || !root->has_node_and_resource(animation->track_get_path(track))) { return false; } Ref res; Vector leftover_path; Node *node = root->get_node_and_resource(animation->track_get_path(track), res, leftover_path); Object *obj = nullptr; if (res.is_valid()) { obj = res.ptr(); } else if (node) { obj = node; } bool prop_exists = false; if (obj) { r_valid_type = obj->get_static_property_type_indexed(leftover_path, &prop_exists); } return (!prop_exists || Variant::can_convert(p_key_value.get_type(), r_valid_type)); } Ref AnimationTrackEdit::_get_key_type_icon() const { const Ref type_icons[9] = { get_editor_theme_icon(SNAME("KeyValue")), get_editor_theme_icon(SNAME("KeyTrackPosition")), get_editor_theme_icon(SNAME("KeyTrackRotation")), get_editor_theme_icon(SNAME("KeyTrackScale")), get_editor_theme_icon(SNAME("KeyTrackBlendShape")), get_editor_theme_icon(SNAME("KeyCall")), get_editor_theme_icon(SNAME("KeyBezier")), get_editor_theme_icon(SNAME("KeyAudio")), get_editor_theme_icon(SNAME("KeyAnimation")) }; return type_icons[animation->track_get_type(track)]; } Control::CursorShape AnimationTrackEdit::get_cursor_shape(const Point2 &p_pos) const { if (command_or_control_pressed && animation->track_get_type(track) == Animation::TYPE_METHOD && hovering_key_idx != -1) { return Control::CURSOR_POINTING_HAND; } return get_default_cursor_shape(); } String AnimationTrackEdit::get_tooltip(const Point2 &p_pos) const { if (check_rect.has_point(p_pos)) { return TTR("Toggle this track on/off."); } if (icon_rect.has_point(p_pos)) { return TTR("Select node in scene."); } // Don't overlap track keys if they start at 0. if (path_rect.has_point(p_pos + Size2(type_icon->get_width(), 0))) { return String(animation->track_get_path(track)); } if (update_mode_rect.has_point(p_pos)) { if (animation->track_get_type(track) == Animation::TYPE_AUDIO) { return TTR("Use Blend"); } else { return TTR("Update Mode (How this property is set)"); } } if (interp_mode_rect.has_point(p_pos)) { return TTR("Interpolation Mode"); } if (loop_wrap_rect.has_point(p_pos)) { return TTR("Loop Wrap Mode (Interpolate end with beginning on loop)"); } if (remove_rect.has_point(p_pos)) { return TTR("Remove this track."); } int limit = timeline->get_name_limit(); int limit_end = get_size().width - timeline->get_buttons_width(); // Left Border including space occupied by keyframes on t=0. int limit_start_hitbox = limit - type_icon->get_width(); if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) { int key_idx = -1; float key_distance = 1e20; // Select should happen in the opposite order of drawing for more accurate overlap select. for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) { Rect2 rect = const_cast(this)->get_key_rect(i, timeline->get_zoom_scale()); float offset = animation->track_get_key_time(track, i) - timeline->get_value(); offset = offset * timeline->get_zoom_scale() + limit; rect.position.x += offset; if (rect.has_point(p_pos)) { if (const_cast(this)->is_key_selectable_by_distance()) { float distance = Math::abs(offset - p_pos.x); if (key_idx == -1 || distance < key_distance) { key_idx = i; key_distance = distance; } } else { // First one does it. break; } } } if (key_idx != -1) { String text = TTR("Time (s):") + " " + TS->format_number(rtos(Math::snapped(animation->track_get_key_time(track, key_idx), SECOND_DECIMAL))) + "\n"; switch (animation->track_get_type(track)) { case Animation::TYPE_POSITION_3D: { Vector3 t = animation->track_get_key_value(track, key_idx); text += TTR("Position:") + " " + String(t) + "\n"; } break; case Animation::TYPE_ROTATION_3D: { Quaternion t = animation->track_get_key_value(track, key_idx); text += TTR("Rotation:") + " " + String(t) + "\n"; } break; case Animation::TYPE_SCALE_3D: { Vector3 t = animation->track_get_key_value(track, key_idx); text += TTR("Scale:") + " " + String(t) + "\n"; } break; case Animation::TYPE_BLEND_SHAPE: { float t = animation->track_get_key_value(track, key_idx); text += TTR("Blend Shape:") + " " + itos(t) + "\n"; } break; case Animation::TYPE_VALUE: { const Variant &v = animation->track_get_key_value(track, key_idx); text += TTR("Type:") + " " + Variant::get_type_name(v.get_type()) + "\n"; Variant::Type valid_type = Variant::NIL; text += TTR("Value:") + " " + String(v); if (!_is_value_key_valid(v, valid_type)) { text += " " + vformat(TTR("(Invalid, expected type: %s)"), Variant::get_type_name(valid_type)); } text += "\n" + TTR("Easing:") + " " + rtos(animation->track_get_key_transition(track, key_idx)); } break; case Animation::TYPE_METHOD: { Dictionary d = animation->track_get_key_value(track, key_idx); if (d.has("method")) { text += String(d["method"]); } text += "("; Vector args; if (d.has("args")) { args = d["args"]; } for (int i = 0; i < args.size(); i++) { if (i > 0) { text += ", "; } text += args[i].get_construct_string(); } text += ")\n"; } break; case Animation::TYPE_BEZIER: { float h = animation->bezier_track_get_key_value(track, key_idx); text += TTR("Value:") + " " + rtos(h) + "\n"; Vector2 ih = animation->bezier_track_get_key_in_handle(track, key_idx); text += TTR("In-Handle:") + " " + String(ih) + "\n"; Vector2 oh = animation->bezier_track_get_key_out_handle(track, key_idx); text += TTR("Out-Handle:") + " " + String(oh) + "\n"; int hm = animation->bezier_track_get_key_handle_mode(track, key_idx); switch (hm) { case Animation::HANDLE_MODE_FREE: { text += TTR("Handle mode: Free\n"); } break; case Animation::HANDLE_MODE_LINEAR: { text += TTR("Handle mode: Linear\n"); } break; case Animation::HANDLE_MODE_BALANCED: { text += TTR("Handle mode: Balanced\n"); } break; case Animation::HANDLE_MODE_MIRRORED: { text += TTR("Handle mode: Mirrored\n"); } break; } } break; case Animation::TYPE_AUDIO: { String stream_name = "null"; Ref stream = animation->audio_track_get_key_stream(track, key_idx); if (stream.is_valid()) { if (stream->get_path().is_resource_file()) { stream_name = stream->get_path().get_file(); } else if (!stream->get_name().is_empty()) { stream_name = stream->get_name(); } else { stream_name = stream->get_class(); } } text += TTR("Stream:") + " " + stream_name + "\n"; float so = animation->audio_track_get_key_start_offset(track, key_idx); text += TTR("Start (s):") + " " + rtos(so) + "\n"; float eo = animation->audio_track_get_key_end_offset(track, key_idx); text += TTR("End (s):") + " " + rtos(eo) + "\n"; } break; case Animation::TYPE_ANIMATION: { String name = animation->animation_track_get_key_animation(track, key_idx); text += TTR("Animation Clip:") + " " + name + "\n"; } break; } return text; } } return Control::get_tooltip(p_pos); } void AnimationTrackEdit::gui_input(const Ref &p_event) { ERR_FAIL_COND(p_event.is_null()); if (p_event->is_pressed()) { if (ED_IS_SHORTCUT("animation_editor/duplicate_selected_keys", p_event)) { if (!read_only) { emit_signal(SNAME("duplicate_request"), -1.0, false); } accept_event(); } if (ED_IS_SHORTCUT("animation_editor/cut_selected_keys", p_event)) { if (!read_only) { emit_signal(SNAME("cut_request")); } accept_event(); } if (ED_IS_SHORTCUT("animation_editor/copy_selected_keys", p_event)) { if (!read_only) { emit_signal(SNAME("copy_request")); } accept_event(); } if (ED_IS_SHORTCUT("animation_editor/paste_keys", p_event)) { if (!read_only) { emit_signal(SNAME("paste_request"), -1.0, false); } accept_event(); } if (ED_IS_SHORTCUT("animation_editor/delete_selection", p_event)) { if (!read_only) { emit_signal(SNAME("delete_request")); } accept_event(); } } Ref mb = p_event; if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { Point2 pos = mb->get_position(); bool no_mod_key_pressed = !mb->is_alt_pressed() && !mb->is_shift_pressed() && !mb->is_command_or_control_pressed(); if (mb->is_double_click() && !moving_selection && no_mod_key_pressed) { int x = pos.x - timeline->get_name_limit(); float ofs = x / timeline->get_zoom_scale() + timeline->get_value(); emit_signal(SNAME("timeline_changed"), ofs, false); } if (!read_only) { if (check_rect.has_point(pos)) { EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Toggle Track Enabled")); undo_redo->add_do_method(animation.ptr(), "track_set_enabled", track, !animation->track_is_enabled(track)); undo_redo->add_undo_method(animation.ptr(), "track_set_enabled", track, animation->track_is_enabled(track)); undo_redo->commit_action(); queue_redraw(); accept_event(); } if (icon_rect.has_point(pos)) { EditorSelection *editor_selection = EditorNode::get_singleton()->get_editor_selection(); editor_selection->clear(); Node *n = root->get_node_or_null(node_path); if (n) { editor_selection->add_node(n); } } // Don't overlap track keys if they start at 0. if (path_rect.has_point(pos + Size2(type_icon->get_width(), 0))) { clicking_on_name = true; accept_event(); } if (update_mode_rect.has_point(pos)) { if (!menu) { menu = memnew(PopupMenu); add_child(menu); menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationTrackEdit::_menu_selected)); } menu->clear(); if (animation->track_get_type(track) == Animation::TYPE_AUDIO) { menu->add_icon_item(get_editor_theme_icon(SNAME("UseBlendEnable")), TTR("Use Blend"), MENU_USE_BLEND_ENABLED); menu->add_icon_item(get_editor_theme_icon(SNAME("UseBlendDisable")), TTR("Don't Use Blend"), MENU_USE_BLEND_DISABLED); } else { menu->add_icon_item(get_editor_theme_icon(SNAME("TrackContinuous")), TTR("Continuous"), MENU_CALL_MODE_CONTINUOUS); menu->add_icon_item(get_editor_theme_icon(SNAME("TrackDiscrete")), TTR("Discrete"), MENU_CALL_MODE_DISCRETE); menu->add_icon_item(get_editor_theme_icon(SNAME("TrackCapture")), TTR("Capture"), MENU_CALL_MODE_CAPTURE); } menu->reset_size(); moving_selection_attempt = false; moving_selection = false; Vector2 popup_pos = get_screen_position() + update_mode_rect.position + Vector2(0, update_mode_rect.size.height); menu->set_position(popup_pos); menu->popup(); accept_event(); } if (interp_mode_rect.has_point(pos)) { if (!menu) { menu = memnew(PopupMenu); add_child(menu); menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationTrackEdit::_menu_selected)); } menu->clear(); menu->add_icon_item(get_editor_theme_icon(SNAME("InterpRaw")), TTR("Nearest"), MENU_INTERPOLATION_NEAREST); menu->add_icon_item(get_editor_theme_icon(SNAME("InterpLinear")), TTR("Linear"), MENU_INTERPOLATION_LINEAR); menu->add_icon_item(get_editor_theme_icon(SNAME("InterpCubic")), TTR("Cubic"), MENU_INTERPOLATION_CUBIC); // Check whether it is angle property. AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); if (ape) { AnimationPlayer *ap = ape->get_player(); if (ap) { NodePath npath = animation->track_get_path(track); Node *a_ap_root_node = ap->get_node_or_null(ap->get_root_node()); Node *nd = nullptr; // We must test that we have a valid a_ap_root_node before trying to access its content to init the nd Node. if (a_ap_root_node) { nd = a_ap_root_node->get_node_or_null(NodePath(npath.get_concatenated_names())); } if (nd) { StringName prop = npath.get_concatenated_subnames(); PropertyInfo prop_info; ClassDB::get_property_info(nd->get_class(), prop, &prop_info); #ifdef DISABLE_DEPRECATED bool is_angle = prop_info.type == Variant::FLOAT && prop_info.hint_string.contains("radians_as_degrees"); #else bool is_angle = prop_info.type == Variant::FLOAT && prop_info.hint_string.contains("radians"); #endif // DISABLE_DEPRECATED if (is_angle) { menu->add_icon_item(get_editor_theme_icon(SNAME("InterpLinearAngle")), TTR("Linear Angle"), MENU_INTERPOLATION_LINEAR_ANGLE); menu->add_icon_item(get_editor_theme_icon(SNAME("InterpCubicAngle")), TTR("Cubic Angle"), MENU_INTERPOLATION_CUBIC_ANGLE); } } } } menu->reset_size(); moving_selection_attempt = false; moving_selection = false; Vector2 popup_pos = get_screen_position() + interp_mode_rect.position + Vector2(0, interp_mode_rect.size.height); menu->set_position(popup_pos); menu->popup(); accept_event(); } if (loop_wrap_rect.has_point(pos)) { if (!menu) { menu = memnew(PopupMenu); add_child(menu); menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationTrackEdit::_menu_selected)); } menu->clear(); menu->add_icon_item(get_editor_theme_icon(SNAME("InterpWrapClamp")), TTR("Clamp Loop Interp"), MENU_LOOP_CLAMP); menu->add_icon_item(get_editor_theme_icon(SNAME("InterpWrapLoop")), TTR("Wrap Loop Interp"), MENU_LOOP_WRAP); menu->reset_size(); moving_selection_attempt = false; moving_selection = false; Vector2 popup_pos = get_screen_position() + loop_wrap_rect.position + Vector2(0, loop_wrap_rect.size.height); menu->set_position(popup_pos); menu->popup(); accept_event(); } if (remove_rect.has_point(pos)) { emit_signal(SNAME("remove_request"), track); accept_event(); return; } } if (mb->is_command_or_control_pressed() && _lookup_key(hovering_key_idx)) { accept_event(); return; } if (_try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), true)) { accept_event(); } } if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { Point2 pos = mb->get_position(); if (pos.x >= timeline->get_name_limit() && pos.x <= get_size().width - timeline->get_buttons_width()) { // Can do something with menu too! show insert key. float offset = (pos.x - timeline->get_name_limit()) / timeline->get_zoom_scale(); if (!read_only) { if (!menu) { menu = memnew(PopupMenu); add_child(menu); menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationTrackEdit::_menu_selected)); } bool selected = _try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), false); menu->clear(); if (animation->track_get_type(track) == Animation::TYPE_METHOD) { if (hovering_key_idx != -1) { lookup_key_idx = hovering_key_idx; menu->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); menu->add_separator(); } } menu->add_icon_item(get_editor_theme_icon(SNAME("Key")), TTR("Insert Key..."), MENU_KEY_INSERT); if (selected || editor->is_selection_active()) { menu->add_separator(); menu->add_icon_item(get_editor_theme_icon(SNAME("Duplicate")), TTR("Duplicate Key(s)"), MENU_KEY_DUPLICATE); menu->add_icon_item(get_editor_theme_icon(SNAME("ActionCut")), TTR("Cut Key(s)"), MENU_KEY_CUT); menu->add_icon_item(get_editor_theme_icon(SNAME("ActionCopy")), TTR("Copy Key(s)"), MENU_KEY_COPY); } if (editor->is_key_clipboard_active()) { menu->add_icon_item(get_editor_theme_icon(SNAME("ActionPaste")), TTR("Paste Key(s)"), MENU_KEY_PASTE); } if (selected || editor->is_selection_active()) { AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); if ((!player->has_animation(SceneStringName(RESET)) || animation != player->get_animation(SceneStringName(RESET))) && editor->can_add_reset_key()) { menu->add_icon_item(get_editor_theme_icon(SNAME("Reload")), TTR("Add RESET Value(s)"), MENU_KEY_ADD_RESET); } menu->add_separator(); menu->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Delete Key(s)"), MENU_KEY_DELETE); } menu->reset_size(); moving_selection_attempt = false; moving_selection = false; menu->set_position(get_screen_position() + get_local_mouse_position()); menu->popup(); insert_at_pos = offset + timeline->get_value(); accept_event(); } } } if (mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && clicking_on_name) { if (!path) { path_popup = memnew(Popup); path_popup->set_wrap_controls(true); add_child(path_popup); path = memnew(LineEdit); path_popup->add_child(path); path->set_anchors_and_offsets_preset(PRESET_FULL_RECT); path->connect(SceneStringName(text_submitted), callable_mp(this, &AnimationTrackEdit::_path_submitted)); } path->set_text(String(animation->track_get_path(track))); const Vector2 theme_ofs = path->get_theme_stylebox(CoreStringName(normal), SNAME("LineEdit"))->get_offset(); moving_selection_attempt = false; moving_selection = false; path_popup->set_position(get_screen_position() + path_rect.position - theme_ofs); path_popup->set_size(path_rect.size); path_popup->popup(); path->grab_focus(); path->set_caret_column(path->get_text().length()); clicking_on_name = false; } if (mb.is_valid() && moving_selection_attempt) { if (!mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { moving_selection_attempt = false; if (moving_selection && moving_selection_effective) { if (std::abs(editor->get_moving_selection_offset()) > CMP_EPSILON) { emit_signal(SNAME("move_selection_commit")); } } else if (select_single_attempt != -1) { emit_signal(SNAME("select_key"), select_single_attempt, true); } moving_selection = false; select_single_attempt = -1; } if (moving_selection && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { moving_selection_attempt = false; moving_selection = false; emit_signal(SNAME("move_selection_cancel")); } } Ref mm = p_event; if (mm.is_valid()) { const int previous_hovering_key_idx = hovering_key_idx; command_or_control_pressed = mm->is_command_or_control_pressed(); // Hovering compressed keyframes for editing is not possible. if (!animation->track_is_compressed(track)) { const float scale = timeline->get_zoom_scale(); const int limit = timeline->get_name_limit(); const int limit_end = get_size().width - timeline->get_buttons_width(); // Left Border including space occupied by keyframes on t=0. const int limit_start_hitbox = limit - type_icon->get_width(); const Point2 pos = mm->get_position(); if (pos.x >= limit_start_hitbox && pos.x <= limit_end) { // Use the same logic as key selection to ensure that hovering accurately represents // which key will be selected when clicking. int key_idx = -1; float key_distance = 1e20; hovering_key_idx = -1; // Hovering should happen in the opposite order of drawing for more accurate overlap hovering. for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) { Rect2 rect = get_key_rect(i, scale); float offset = animation->track_get_key_time(track, i) - timeline->get_value(); offset = offset * scale + limit; rect.position.x += offset; if (rect.has_point(pos)) { if (is_key_selectable_by_distance()) { const float distance = Math::abs(offset - pos.x); if (key_idx == -1 || distance < key_distance) { key_idx = i; key_distance = distance; hovering_key_idx = i; } } else { // First one does it. hovering_key_idx = i; break; } } } if (hovering_key_idx != previous_hovering_key_idx) { // Required to draw keyframe hover feedback on the correct keyframe. queue_redraw(); } } } } if (mm.is_valid() && mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && moving_selection_attempt) { if (!moving_selection) { moving_selection = true; emit_signal(SNAME("move_selection_begin")); } float moving_begin_time = ((moving_selection_mouse_begin_x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value(); float new_time = ((mm->get_position().x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value(); float delta = new_time - moving_begin_time; float snapped_time = editor->snap_time(moving_selection_pivot + delta); float offset = 0.0; if (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)) { offset = snapped_time - moving_selection_pivot; moving_selection_effective = true; } emit_signal(SNAME("move_selection"), offset); } } bool AnimationTrackEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable) { if (!animation->track_is_compressed(track)) { // Selecting compressed keyframes for editing is not possible. float scale = timeline->get_zoom_scale(); int limit = timeline->get_name_limit(); int limit_end = get_size().width - timeline->get_buttons_width(); // Left Border including space occupied by keyframes on t=0. int limit_start_hitbox = limit - type_icon->get_width(); if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) { int key_idx = -1; float key_distance = 1e20; // Select should happen in the opposite order of drawing for more accurate overlap select. for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) { Rect2 rect = get_key_rect(i, scale); float offset = animation->track_get_key_time(track, i) - timeline->get_value(); offset = offset * scale + limit; rect.position.x += offset; if (rect.has_point(p_pos)) { if (is_key_selectable_by_distance()) { float distance = Math::abs(offset - p_pos.x); if (key_idx == -1 || distance < key_distance) { key_idx = i; key_distance = distance; } } else { // First one does it. key_idx = i; break; } } } if (key_idx != -1) { if (p_aggregate) { if (editor->is_key_selected(track, key_idx)) { if (p_deselectable) { emit_signal(SNAME("deselect_key"), key_idx); moving_selection_pivot = 0.0f; moving_selection_mouse_begin_x = 0.0f; } } else { emit_signal(SNAME("select_key"), key_idx, false); moving_selection_attempt = true; moving_selection_effective = false; select_single_attempt = -1; moving_selection_pivot = animation->track_get_key_time(track, key_idx); moving_selection_mouse_begin_x = p_pos.x; } } else { if (!editor->is_key_selected(track, key_idx)) { emit_signal(SNAME("select_key"), key_idx, true); select_single_attempt = -1; } else { select_single_attempt = key_idx; } moving_selection_attempt = true; moving_selection_effective = false; moving_selection_pivot = animation->track_get_key_time(track, key_idx); moving_selection_mouse_begin_x = p_pos.x; } if (read_only) { moving_selection_attempt = false; moving_selection_pivot = 0.0f; moving_selection_mouse_begin_x = 0.0f; } return true; } } } return false; } bool AnimationTrackEdit::_lookup_key(int p_key_idx) const { if (p_key_idx < 0 || p_key_idx >= animation->track_get_key_count(track)) { return false; } if (animation->track_get_type(track) == Animation::TYPE_METHOD) { Node *target = root->get_node_or_null(animation->track_get_path(track)); if (target) { StringName method = animation->method_track_get_name(track, p_key_idx); // First, check every script in the inheritance chain. bool found_in_script = false; Ref