Parcourir la source

Merge pull request #59980 from reduz/animation-libraries

Rémi Verschelde il y a 3 ans
Parent
commit
4ab86c6731

+ 70 - 0
doc/classes/AnimationLibrary.xml

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="AnimationLibrary" inherits="Resource" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+	<brief_description>
+	</brief_description>
+	<description>
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="add_animation">
+			<return type="int" enum="Error" />
+			<argument index="0" name="name" type="StringName" />
+			<argument index="1" name="animation" type="Animation" />
+			<description>
+			</description>
+		</method>
+		<method name="get_animation" qualifiers="const">
+			<return type="Animation" />
+			<argument index="0" name="name" type="StringName" />
+			<description>
+			</description>
+		</method>
+		<method name="get_animation_list" qualifiers="const">
+			<return type="StringName[]" />
+			<description>
+			</description>
+		</method>
+		<method name="has_animation" qualifiers="const">
+			<return type="bool" />
+			<argument index="0" name="name" type="StringName" />
+			<description>
+			</description>
+		</method>
+		<method name="remove_animation">
+			<return type="void" />
+			<argument index="0" name="name" type="StringName" />
+			<description>
+			</description>
+		</method>
+		<method name="rename_animation">
+			<return type="void" />
+			<argument index="0" name="name" type="StringName" />
+			<argument index="1" name="newname" type="StringName" />
+			<description>
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="_data" type="Dictionary" setter="_set_data" getter="_get_data" default="{}">
+		</member>
+	</members>
+	<signals>
+		<signal name="animation_added">
+			<argument index="0" name="name" type="Animation" />
+			<description>
+			</description>
+		</signal>
+		<signal name="animation_removed">
+			<argument index="0" name="name" type="Animation" />
+			<description>
+			</description>
+		</signal>
+		<signal name="animation_renamed">
+			<argument index="0" name="name" type="Animation" />
+			<argument index="1" name="to_name" type="Animation" />
+			<description>
+			</description>
+		</signal>
+	</signals>
+</class>

+ 27 - 7
doc/classes/AnimationPlayer.xml

@@ -14,12 +14,11 @@
 		<link title="Third Person Shooter Demo">https://godotengine.org/asset-library/asset/678</link>
 	</tutorials>
 	<methods>
-		<method name="add_animation">
+		<method name="add_animation_library">
 			<return type="int" enum="Error" />
 			<argument index="0" name="name" type="StringName" />
-			<argument index="1" name="animation" type="Animation" />
+			<argument index="1" name="library" type="AnimationLibrary" />
 			<description>
-				Adds [code]animation[/code] to the player accessible with the key [code]name[/code].
 			</description>
 		</method>
 		<method name="advance">
@@ -63,6 +62,12 @@
 				Returns the name of [code]animation[/code] or an empty string if not found.
 			</description>
 		</method>
+		<method name="find_animation_library" qualifiers="const">
+			<return type="StringName" />
+			<argument index="0" name="animation" type="Animation" />
+			<description>
+			</description>
+		</method>
 		<method name="get_animation" qualifiers="const">
 			<return type="Animation" />
 			<argument index="0" name="name" type="StringName" />
@@ -70,6 +75,17 @@
 				Returns the [Animation] with key [code]name[/code] or [code]null[/code] if not found.
 			</description>
 		</method>
+		<method name="get_animation_library" qualifiers="const">
+			<return type="AnimationLibrary" />
+			<argument index="0" name="name" type="StringName" />
+			<description>
+			</description>
+		</method>
+		<method name="get_animation_library_list" qualifiers="const">
+			<return type="StringName[]" />
+			<description>
+			</description>
+		</method>
 		<method name="get_animation_list" qualifiers="const">
 			<return type="PackedStringArray" />
 			<description>
@@ -103,6 +119,12 @@
 				Returns [code]true[/code] if the [AnimationPlayer] stores an [Animation] with key [code]name[/code].
 			</description>
 		</method>
+		<method name="has_animation_library" qualifiers="const">
+			<return type="bool" />
+			<argument index="0" name="name" type="StringName" />
+			<description>
+			</description>
+		</method>
 		<method name="is_playing" qualifiers="const">
 			<return type="bool" />
 			<description>
@@ -138,19 +160,17 @@
 				[b]Note:[/b] If a looped animation is currently playing, the queued animation will never play unless the looped animation is stopped somehow.
 			</description>
 		</method>
-		<method name="remove_animation">
+		<method name="remove_animation_library">
 			<return type="void" />
 			<argument index="0" name="name" type="StringName" />
 			<description>
-				Removes the animation with key [code]name[/code].
 			</description>
 		</method>
-		<method name="rename_animation">
+		<method name="rename_animation_library">
 			<return type="void" />
 			<argument index="0" name="name" type="StringName" />
 			<argument index="1" name="newname" type="StringName" />
 			<description>
-				Renames an existing animation with key [code]name[/code] to [code]newname[/code].
 			</description>
 		</method>
 		<method name="seek">

+ 19 - 1
doc/classes/OptionButton.xml

@@ -30,8 +30,9 @@
 		</method>
 		<method name="add_separator">
 			<return type="void" />
+			<argument index="0" name="text" type="String" default="&quot;&quot;" />
 			<description>
-				Adds a separator to the list of items. Separators help to group items. Separator also takes up an index and is appended at the end.
+				Adds a separator to the list of items. Separators help to group items, and can optionally be given a [code]text[/code] header. A separator also gets an index assigned, and is appended at the end of the item list.
 			</description>
 		</method>
 		<method name="clear">
@@ -89,6 +90,12 @@
 				[b]Warning:[/b] This is a required internal node, removing and freeing it may cause a crash. If you wish to hide it or any of its children, use their [member Window.visible] property.
 			</description>
 		</method>
+		<method name="get_selectable_item" qualifiers="const">
+			<return type="int" />
+			<argument index="0" name="from_last" type="bool" default="false" />
+			<description>
+			</description>
+		</method>
 		<method name="get_selected_id" qualifiers="const">
 			<return type="int" />
 			<description>
@@ -101,6 +108,11 @@
 				Gets the metadata of the selected item. Metadata for items can be set using [method set_item_metadata].
 			</description>
 		</method>
+		<method name="has_selectable_items" qualifiers="const">
+			<return type="bool" />
+			<description>
+			</description>
+		</method>
 		<method name="is_item_disabled" qualifiers="const">
 			<return type="bool" />
 			<argument index="0" name="idx" type="int" />
@@ -108,6 +120,12 @@
 				Returns [code]true[/code] if the item at index [code]idx[/code] is disabled.
 			</description>
 		</method>
+		<method name="is_item_separator" qualifiers="const">
+			<return type="bool" />
+			<argument index="0" name="idx" type="int" />
+			<description>
+			</description>
+		</method>
 		<method name="remove_item">
 			<return type="void" />
 			<argument index="0" name="idx" type="int" />

+ 2 - 1
doc/classes/Tree.xml

@@ -179,8 +179,9 @@
 			<return type="Rect2" />
 			<argument index="0" name="item" type="TreeItem" />
 			<argument index="1" name="column" type="int" default="-1" />
+			<argument index="2" name="button_index" type="int" default="-1" />
 			<description>
-				Returns the rectangle area for the specified [TreeItem]. If [code]column[/code] is specified, only get the position and size of that column, otherwise get the rectangle containing all columns.
+				Returns the rectangle area for the specified [TreeItem]. If [code]column[/code] is specified, only get the position and size of that column, otherwise get the rectangle containing all columns. If a button index is specified, the rectangle of that button will be returned.
 			</description>
 		</method>
 		<method name="get_item_at_position" qualifiers="const">

+ 1 - 0
editor/icons/AnimationLibrary.svg

@@ -0,0 +1 @@
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M14.519 2.006A6 6 0 0 0 8.599 8a6 6 0 0 0 5.92 5.994v-1.01a1 1 0 0 1-.92-.984 1 1 0 0 1 .92-.984V4.984a1 1 0 0 1-.92-.984 1 1 0 0 1 .92-.984Zm-3.432 2.996a1 1 0 0 1 .547.133 1 1 0 0 1 .367 1.365 1 1 0 0 1-1.367.365A1 1 0 0 1 10.27 5.5a1 1 0 0 1 .818-.498ZM11.111 9a1 1 0 0 1 .89.5 1 1 0 0 1-.367 1.365 1 1 0 0 1-1.365-.365 1 1 0 0 1 .365-1.365A1 1 0 0 1 11.111 9Z" style="fill:#e0e0e0;fill-opacity:1"/><path d="M11.094 2.104a6 6 0 0 0-5.92 5.994 6 6 0 0 0 5.92 5.994v-.023a5.795 6.506 0 0 1-2.89-3.104 1 1 0 0 1-1.36-.367 1 1 0 0 1 .365-1.365 1 1 0 0 1 .475-.135 5.795 6.506 0 0 1-.076-.984 5.795 6.506 0 0 1 .082-1.027 1 1 0 0 1-.48-.124 1 1 0 0 1-.366-1.365 1 1 0 0 1 .818-.498 1 1 0 0 1 .547.133 1 1 0 0 1 .004.002 5.795 6.506 0 0 1 2.881-3.076z" style="fill:#e0e0e0;fill-opacity:1"/><path d="M7.616 2.104a6 6 0 0 0-5.92 5.994 6 6 0 0 0 5.92 5.994v-.023a5.795 6.506 0 0 1-2.89-3.104 1 1 0 0 1-1.36-.367 1 1 0 0 1 .366-1.365 1 1 0 0 1 .474-.135 5.795 6.506 0 0 1-.076-.984 5.795 6.506 0 0 1 .082-1.027 1 1 0 0 1-.48-.124 1 1 0 0 1-.366-1.365 1 1 0 0 1 .819-.498 1 1 0 0 1 .547.133 1 1 0 0 1 .003.002 5.795 6.506 0 0 1 2.881-3.076z" style="fill:#e0e0e0;fill-opacity:1"/></svg>

+ 8 - 1
editor/import/editor_import_collada.cpp

@@ -1801,7 +1801,14 @@ Node *EditorSceneFormatImporterCollada::import_scene(const String &p_path, uint3
 				name = state.animations[i]->get_name();
 			}
 
-			ap->add_animation(name, state.animations[i]);
+			Ref<AnimationLibrary> library;
+			if (!ap->has_animation_library("")) {
+				library.instantiate();
+				ap->add_animation_library("", library);
+			} else {
+				library = ap->get_animation_library("");
+			}
+			library->add_animation(name, state.animations[i]);
 		}
 		state.scene->add_child(ap, true);
 		ap->set_owner(state.scene);

+ 10 - 5
editor/import/resource_importer_scene.cpp

@@ -473,7 +473,9 @@ Node *ResourceImporterScene::_pre_fix_node(Node *p_node, Node *p_root, Map<Ref<I
 				if (_teststr(animname, loop_strings[i])) {
 					anim->set_loop_mode(Animation::LoopMode::LOOP_LINEAR);
 					animname = _fixstr(animname, loop_strings[i]);
-					ap->rename_animation(E, animname);
+
+					Ref<AnimationLibrary> library = ap->get_animation_library(ap->find_animation_library(anim));
+					library->rename_animation(E, animname);
 				}
 			}
 		}
@@ -1019,7 +1021,8 @@ Node *ResourceImporterScene::_post_fix_node(Node *p_node, Node *p_root, Map<Ref<
 					Ref<Animation> saved_anim = _save_animation_to_file(anim, save, path, keep_custom);
 
 					if (saved_anim != anim) {
-						ap->add_animation(name, saved_anim); //replace
+						Ref<AnimationLibrary> al = ap->get_animation_library(ap->find_animation_library(anim));
+						al->add_animation(name, saved_anim); //replace
 					}
 				}
 			}
@@ -1109,6 +1112,7 @@ void ResourceImporterScene::_create_clips(AnimationPlayer *anim, const Array &p_
 	}
 
 	Ref<Animation> default_anim = anim->get_animation("default");
+	Ref<AnimationLibrary> al = anim->get_animation_library(anim->find_animation(default_anim));
 
 	for (int i = 0; i < p_clips.size(); i += 7) {
 		String name = p_clips[i];
@@ -1246,15 +1250,16 @@ void ResourceImporterScene::_create_clips(AnimationPlayer *anim, const Array &p_
 
 		new_anim->set_loop_mode(loop_mode);
 		new_anim->set_length(to - from);
-		anim->add_animation(name, new_anim);
+
+		al->add_animation(name, new_anim);
 
 		Ref<Animation> saved_anim = _save_animation_to_file(new_anim, save_to_file, save_to_path, keep_current);
 		if (saved_anim != new_anim) {
-			anim->add_animation(name, saved_anim);
+			al->add_animation(name, saved_anim);
 		}
 	}
 
-	anim->remove_animation("default"); //remove default (no longer needed)
+	al->remove_animation("default"); // Remove default (no longer needed).
 }
 
 void ResourceImporterScene::_optimize_animations(AnimationPlayer *anim, float p_max_lin_error, float p_max_ang_error, float p_max_angle) {

+ 689 - 0
editor/plugins/animation_library_editor.cpp

@@ -0,0 +1,689 @@
+/*************************************************************************/
+/*  animation_library_editor.cpp                                         */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* 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_library_editor.h"
+#include "editor/editor_file_dialog.h"
+#include "editor/editor_node.h"
+#include "editor/editor_scale.h"
+
+void AnimationLibraryEditor::set_animation_player(Object *p_player) {
+	player = p_player;
+}
+
+void AnimationLibraryEditor::_add_library() {
+	add_library_dialog->set_title(TTR("Library Name:"));
+	add_library_name->set_text("");
+	add_library_dialog->popup_centered();
+	add_library_name->grab_focus();
+	adding_animation = false;
+	adding_animation_to_library = StringName();
+	_add_library_validate("");
+}
+
+void AnimationLibraryEditor::_add_library_validate(const String &p_name) {
+	String error;
+
+	if (adding_animation) {
+		Ref<AnimationLibrary> al = player->call("get_animation_library", adding_animation_to_library);
+		ERR_FAIL_COND(al.is_null());
+		if (p_name == "") {
+			error = TTR("Animation name can't be empty.");
+
+		} else if (String(p_name).contains("/") || String(p_name).contains(":") || String(p_name).contains(",") || String(p_name).contains("[")) {
+			error = TTR("Animation name contains invalid characters: '/', ':', ',' or '['.");
+		} else if (al->has_animation(p_name)) {
+			error = TTR("Animation with the same name already exists.");
+		}
+
+	} else {
+		if (p_name == "" && bool(player->call("has_animation_library", ""))) {
+			error = TTR("Enter a library name.");
+		} else if (String(p_name).contains("/") || String(p_name).contains(":") || String(p_name).contains(",") || String(p_name).contains("[")) {
+			error = TTR("Library name contains invalid characters: '/', ':', ',' or '['.");
+		} else if (bool(player->call("has_animation_library", p_name))) {
+			error = TTR("Library with the same name already exists.");
+		}
+	}
+
+	if (error != "") {
+		add_library_validate->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), SNAME("Editor")));
+		add_library_validate->set_text(error);
+		add_library_dialog->get_ok_button()->set_disabled(true);
+	} else {
+		add_library_validate->add_theme_color_override("font_color", get_theme_color(SNAME("success_color"), SNAME("Editor")));
+		if (p_name == "") {
+			add_library_validate->set_text(TTR("Global library will be created."));
+		} else {
+			add_library_validate->set_text(TTR("Library name is valid."));
+		}
+		add_library_dialog->get_ok_button()->set_disabled(false);
+	}
+}
+
+void AnimationLibraryEditor::_add_library_confirm() {
+	if (adding_animation) {
+		String anim_name = add_library_name->get_text();
+		UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+
+		Ref<AnimationLibrary> al = player->call("get_animation_library", adding_animation_to_library);
+		ERR_FAIL_COND(!al.is_valid());
+
+		Ref<Animation> anim;
+		anim.instantiate();
+
+		undo_redo->create_action(vformat(TTR("Add Animation to Library: %s"), anim_name));
+		undo_redo->add_do_method(al.ptr(), "add_animation", anim_name, anim);
+		undo_redo->add_undo_method(al.ptr(), "remove_animation", anim_name);
+		undo_redo->add_do_method(this, "_update_editor", player);
+		undo_redo->add_undo_method(this, "_update_editor", player);
+		undo_redo->commit_action();
+
+	} else {
+		String lib_name = add_library_name->get_text();
+		UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+
+		Ref<AnimationLibrary> al;
+		al.instantiate();
+
+		undo_redo->create_action(vformat(TTR("Add Animation Library: %s"), lib_name));
+		undo_redo->add_do_method(player, "add_animation_library", lib_name, al);
+		undo_redo->add_undo_method(player, "remove_animation_library", lib_name);
+		undo_redo->add_do_method(this, "_update_editor", player);
+		undo_redo->add_undo_method(this, "_update_editor", player);
+		undo_redo->commit_action();
+	}
+}
+
+void AnimationLibraryEditor::_load_library() {
+	List<String> extensions;
+	ResourceLoader::get_recognized_extensions_for_type("AnimationLibrary", &extensions);
+
+	file_dialog->set_title(TTR("Load Animation"));
+	file_dialog->clear_filters();
+	for (const String &K : extensions) {
+		file_dialog->add_filter("*." + K);
+	}
+
+	file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
+	file_dialog->set_current_file("");
+	file_dialog->popup_centered_ratio();
+
+	file_dialog_action = FILE_DIALOG_ACTION_OPEN_LIBRARY;
+}
+
+void AnimationLibraryEditor::_file_popup_selected(int p_id) {
+	Ref<AnimationLibrary> al = player->call("get_animation_library", file_dialog_library);
+	Ref<Animation> anim;
+	if (file_dialog_animation != StringName()) {
+		anim = al->get_animation(file_dialog_animation);
+		ERR_FAIL_COND(anim.is_null());
+	}
+	switch (p_id) {
+		case FILE_MENU_SAVE_LIBRARY: {
+			if (al->get_path().is_resource_file()) {
+				EditorNode::get_singleton()->save_resource(al);
+				break;
+			}
+			[[fallthrough]];
+		}
+		case FILE_MENU_SAVE_AS_LIBRARY: {
+			file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
+			file_dialog->set_title(TTR("Save Library"));
+			if (al->get_path().is_resource_file()) {
+				file_dialog->set_current_path(al->get_path());
+			} else {
+				file_dialog->set_current_file(String(file_dialog_library) + ".res");
+			}
+			file_dialog->clear_filters();
+			List<String> exts;
+			ResourceLoader::get_recognized_extensions_for_type("AnimationLibrary", &exts);
+			for (const String &K : exts) {
+				file_dialog->add_filter("*." + K);
+			}
+
+			file_dialog->popup_centered_ratio();
+			file_dialog_action = FILE_DIALOG_ACTION_SAVE_LIBRARY;
+		} break;
+		case FILE_MENU_MAKE_LIBRARY_UNIQUE: {
+			StringName lib_name = file_dialog_library;
+
+			Ref<AnimationLibrary> ald = al->duplicate();
+
+			UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+			undo_redo->create_action(vformat(TTR("Make Animation Library Unique: %s"), lib_name));
+			undo_redo->add_do_method(player, "remove_animation_library", lib_name);
+			undo_redo->add_do_method(player, "add_animation_library", lib_name, ald);
+			undo_redo->add_undo_method(player, "remove_animation_library", lib_name);
+			undo_redo->add_undo_method(player, "add_animation_library", lib_name, al);
+			undo_redo->add_do_method(this, "_update_editor", player);
+			undo_redo->add_undo_method(this, "_update_editor", player);
+			undo_redo->commit_action();
+
+		} break;
+		case FILE_MENU_EDIT_LIBRARY: {
+			EditorNode::get_singleton()->push_item(al.ptr());
+		} break;
+
+		case FILE_MENU_SAVE_ANIMATION: {
+			if (anim->get_path().is_resource_file()) {
+				EditorNode::get_singleton()->save_resource(anim);
+				break;
+			}
+			[[fallthrough]];
+		}
+		case FILE_MENU_SAVE_AS_ANIMATION: {
+			file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
+			file_dialog->set_title(TTR("Save Animation"));
+			if (anim->get_path().is_resource_file()) {
+				file_dialog->set_current_path(anim->get_path());
+			} else {
+				file_dialog->set_current_file(String(file_dialog_animation) + ".res");
+			}
+			file_dialog->clear_filters();
+			List<String> exts;
+			ResourceLoader::get_recognized_extensions_for_type("Animation", &exts);
+			for (const String &K : exts) {
+				file_dialog->add_filter("*." + K);
+			}
+
+			file_dialog->popup_centered_ratio();
+			file_dialog_action = FILE_DIALOG_ACTION_SAVE_ANIMATION;
+		} break;
+		case FILE_MENU_MAKE_ANIMATION_UNIQUE: {
+			StringName anim_name = file_dialog_animation;
+
+			Ref<Animation> animd = anim->duplicate();
+
+			UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+			undo_redo->create_action(vformat(TTR("Make Animation Unique: %s"), anim_name));
+			undo_redo->add_do_method(al.ptr(), "remove_animation", anim_name);
+			undo_redo->add_do_method(al.ptr(), "add_animation", anim_name, animd);
+			undo_redo->add_undo_method(al.ptr(), "remove_animation", anim_name);
+			undo_redo->add_undo_method(al.ptr(), "add_animation", anim_name, anim);
+			undo_redo->add_do_method(this, "_update_editor", player);
+			undo_redo->add_undo_method(this, "_update_editor", player);
+			undo_redo->commit_action();
+		} break;
+		case FILE_MENU_EDIT_ANIMATION: {
+			EditorNode::get_singleton()->push_item(anim.ptr());
+		} break;
+	}
+}
+void AnimationLibraryEditor::_load_file(String p_path) {
+	switch (file_dialog_action) {
+		case FILE_DIALOG_ACTION_OPEN_LIBRARY: {
+			Ref<AnimationLibrary> al = ResourceLoader::load(p_path);
+			if (al.is_null()) {
+				error_dialog->set_text(TTR("Invalid AnimationLibrary file."));
+				error_dialog->popup_centered();
+				return;
+			}
+
+			TypedArray<StringName> libs = player->call("get_animation_library_list");
+			for (int i = 0; i < libs.size(); i++) {
+				const StringName K = libs[i];
+				Ref<AnimationLibrary> al2 = player->call("get_animation_library", K);
+				if (al2 == al) {
+					error_dialog->set_text(TTR("This library is already added to the player."));
+					error_dialog->popup_centered();
+
+					return;
+				}
+			}
+
+			String name = p_path.get_file().get_basename();
+
+			int attempt = 1;
+
+			while (bool(player->call("has_animation_library", name))) {
+				attempt++;
+				name = p_path.get_file().get_basename() + " " + itos(attempt);
+			}
+
+			UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+
+			undo_redo->create_action(vformat(TTR("Add Animation Library: %s"), name));
+			undo_redo->add_do_method(player, "add_animation_library", name, al);
+			undo_redo->add_undo_method(player, "remove_animation_library", name);
+			undo_redo->add_do_method(this, "_update_editor", player);
+			undo_redo->add_undo_method(this, "_update_editor", player);
+			undo_redo->commit_action();
+		} break;
+		case FILE_DIALOG_ACTION_OPEN_ANIMATION: {
+			Ref<Animation> anim = ResourceLoader::load(p_path);
+			if (anim.is_null()) {
+				error_dialog->set_text(TTR("Invalid Animation file."));
+				error_dialog->popup_centered();
+				return;
+			}
+
+			Ref<AnimationLibrary> al = player->call("get_animation_library", adding_animation_to_library);
+			List<StringName> anims;
+			al->get_animation_list(&anims);
+			for (const StringName &K : anims) {
+				Ref<Animation> a2 = al->get_animation(K);
+				if (a2 == anim) {
+					error_dialog->set_text(TTR("This animation is already added to the library."));
+					error_dialog->popup_centered();
+					return;
+				}
+			}
+
+			String name = p_path.get_file().get_basename();
+
+			int attempt = 1;
+
+			while (al->has_animation(name)) {
+				attempt++;
+				name = p_path.get_file().get_basename() + " " + itos(attempt);
+			}
+
+			UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+
+			undo_redo->create_action(vformat(TTR("Load Animation into Library: %s"), name));
+			undo_redo->add_do_method(al.ptr(), "add_animation", name, anim);
+			undo_redo->add_undo_method(al.ptr(), "remove_animation", name);
+			undo_redo->add_do_method(this, "_update_editor", player);
+			undo_redo->add_undo_method(this, "_update_editor", player);
+			undo_redo->commit_action();
+		} break;
+
+		case FILE_DIALOG_ACTION_SAVE_LIBRARY: {
+			Ref<AnimationLibrary> al = player->call("get_animation_library", file_dialog_library);
+			String prev_path = al->get_path();
+			EditorNode::get_singleton()->save_resource_in_path(al, p_path);
+
+			if (al->get_path() != prev_path) { // Save successful.
+				UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+
+				undo_redo->create_action(vformat(TTR("Save Animation library to File: %s"), file_dialog_library));
+				undo_redo->add_do_method(al.ptr(), "set_path", al->get_path());
+				undo_redo->add_undo_method(al.ptr(), "set_path", prev_path);
+				undo_redo->add_do_method(this, "_update_editor", player);
+				undo_redo->add_undo_method(this, "_update_editor", player);
+				undo_redo->commit_action();
+			}
+
+		} break;
+		case FILE_DIALOG_ACTION_SAVE_ANIMATION: {
+			Ref<AnimationLibrary> al = player->call("get_animation_library", file_dialog_library);
+			Ref<Animation> anim;
+			if (file_dialog_animation != StringName()) {
+				anim = al->get_animation(file_dialog_animation);
+				ERR_FAIL_COND(anim.is_null());
+			}
+			String prev_path = anim->get_path();
+			EditorNode::get_singleton()->save_resource_in_path(anim, p_path);
+			if (anim->get_path() != prev_path) { // Save successful.
+				UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+
+				undo_redo->create_action(vformat(TTR("Save Animation to File: %s"), file_dialog_animation));
+				undo_redo->add_do_method(anim.ptr(), "set_path", anim->get_path());
+				undo_redo->add_undo_method(anim.ptr(), "set_path", prev_path);
+				undo_redo->add_do_method(this, "_update_editor", player);
+				undo_redo->add_undo_method(this, "_update_editor", player);
+				undo_redo->commit_action();
+			}
+		} break;
+	}
+}
+
+void AnimationLibraryEditor::_item_renamed() {
+	TreeItem *ti = tree->get_edited();
+	String text = ti->get_text(0);
+	String old_text = ti->get_metadata(0);
+	bool restore_text = false;
+	UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+
+	if (String(text).contains("/") || String(text).contains(":") || String(text).contains(",") || String(text).contains("[")) {
+		restore_text = true;
+	} else {
+		if (ti->get_parent() == tree->get_root()) {
+			// Renamed library
+
+			if (player->call("has_animation_library", text)) {
+				restore_text = true;
+			} else {
+				undo_redo->create_action(vformat(TTR("Rename Animation Library: %s"), text));
+				undo_redo->add_do_method(player, "rename_animation_library", old_text, text);
+				undo_redo->add_undo_method(player, "rename_animation_library", text, old_text);
+				undo_redo->add_do_method(this, "_update_editor", player);
+				undo_redo->add_undo_method(this, "_update_editor", player);
+				updating = true;
+				undo_redo->commit_action();
+				updating = false;
+				ti->set_metadata(0, text);
+				if (text == "") {
+					ti->set_suffix(0, TTR("[Global]"));
+				} else {
+					ti->set_suffix(0, "");
+				}
+			}
+		} else {
+			// Renamed anim
+			StringName library = ti->get_parent()->get_metadata(0);
+			Ref<AnimationLibrary> al = player->call("get_animation_library", library);
+
+			if (al.is_valid()) {
+				if (al->has_animation(text)) {
+					restore_text = true;
+				} else {
+					undo_redo->create_action(vformat(TTR("Rename Animation: %s"), text));
+					undo_redo->add_do_method(al.ptr(), "rename_animation", old_text, text);
+					undo_redo->add_undo_method(al.ptr(), "rename_animation", text, old_text);
+					undo_redo->add_do_method(this, "_update_editor", player);
+					undo_redo->add_undo_method(this, "_update_editor", player);
+					updating = true;
+					undo_redo->commit_action();
+					updating = false;
+
+					ti->set_metadata(0, text);
+				}
+			} else {
+				restore_text = true;
+			}
+		}
+	}
+
+	if (restore_text) {
+		ti->set_text(0, old_text);
+	}
+}
+
+void AnimationLibraryEditor::_button_pressed(TreeItem *p_item, int p_column, int p_button) {
+	if (p_item->get_parent() == tree->get_root()) {
+		// Library
+		StringName lib_name = p_item->get_metadata(0);
+		Ref<AnimationLibrary> al = player->call("get_animation_library", lib_name);
+		switch (p_button) {
+			case LIB_BUTTON_ADD: {
+				add_library_dialog->set_title(TTR("Animation Name:"));
+				add_library_name->set_text("");
+				add_library_dialog->popup_centered();
+				add_library_name->grab_focus();
+				adding_animation = true;
+				adding_animation_to_library = p_item->get_metadata(0);
+				_add_library_validate("");
+			} break;
+			case LIB_BUTTON_LOAD: {
+				adding_animation_to_library = p_item->get_metadata(0);
+				List<String> extensions;
+				ResourceLoader::get_recognized_extensions_for_type("Animation", &extensions);
+
+				file_dialog->clear_filters();
+				for (const String &K : extensions) {
+					file_dialog->add_filter("*." + K);
+				}
+
+				file_dialog->set_title(TTR("Load Animation"));
+				file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
+				file_dialog->set_current_file("");
+				file_dialog->popup_centered_ratio();
+
+				file_dialog_action = FILE_DIALOG_ACTION_OPEN_ANIMATION;
+
+			} break;
+			case LIB_BUTTON_PASTE: {
+				Ref<Animation> anim = EditorSettings::get_singleton()->get_resource_clipboard();
+				if (!anim.is_valid()) {
+					error_dialog->set_text(TTR("No animation resource in clipboard!"));
+					error_dialog->popup_centered();
+					return;
+				}
+
+				anim = anim->duplicate(); // Users simply dont care about referencing, so making a copy works better here.
+
+				String base_name;
+				if (anim->get_name() != "") {
+					base_name = anim->get_name();
+				} else {
+					base_name = TTR("Pasted Animation");
+				}
+
+				String name = base_name;
+				int attempt = 1;
+				while (al->has_animation(name)) {
+					attempt++;
+					name = base_name + " " + itos(attempt);
+				}
+
+				UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+
+				undo_redo->create_action(vformat(TTR("Add Animation to Library: %s"), name));
+				undo_redo->add_do_method(al.ptr(), "add_animation", name, anim);
+				undo_redo->add_undo_method(al.ptr(), "remove_animation", name);
+				undo_redo->add_do_method(this, "_update_editor", player);
+				undo_redo->add_undo_method(this, "_update_editor", player);
+				undo_redo->commit_action();
+
+			} break;
+			case LIB_BUTTON_FILE: {
+				file_popup->clear();
+				file_popup->add_item(TTR("Save"), FILE_MENU_SAVE_LIBRARY);
+				file_popup->add_item(TTR("Save As"), FILE_MENU_SAVE_AS_LIBRARY);
+				file_popup->add_separator();
+				file_popup->add_item(TTR("Make Unique"), FILE_MENU_MAKE_LIBRARY_UNIQUE);
+				file_popup->add_separator();
+				file_popup->add_item(TTR("Open in Inspector"), FILE_MENU_EDIT_LIBRARY);
+				Rect2 pos = tree->get_item_rect(p_item, 1, 0);
+				Vector2 popup_pos = tree->get_screen_position() + pos.position + Vector2(0, pos.size.height);
+				file_popup->popup(Rect2(popup_pos, Size2()));
+
+				file_dialog_animation = StringName();
+				file_dialog_library = lib_name;
+			} break;
+			case LIB_BUTTON_DELETE: {
+				UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+				undo_redo->create_action(vformat(TTR("Remove Animation Library: %s"), lib_name));
+				undo_redo->add_do_method(player, "remove_animation_library", lib_name);
+				undo_redo->add_undo_method(player, "add_animation_library", lib_name, al);
+				undo_redo->add_do_method(this, "_update_editor", player);
+				undo_redo->add_undo_method(this, "_update_editor", player);
+				undo_redo->commit_action();
+			} break;
+		}
+
+	} else {
+		// Animation
+		StringName lib_name = p_item->get_parent()->get_metadata(0);
+		StringName anim_name = p_item->get_metadata(0);
+		Ref<AnimationLibrary> al = player->call("get_animation_library", lib_name);
+		Ref<Animation> anim = al->get_animation(anim_name);
+		ERR_FAIL_COND(!anim.is_valid());
+		switch (p_button) {
+			case ANIM_BUTTON_COPY: {
+				if (anim->get_name() == "") {
+					anim->set_name(anim_name); // Keep the name around
+				}
+				EditorSettings::get_singleton()->set_resource_clipboard(anim);
+			} break;
+			case ANIM_BUTTON_FILE: {
+				file_popup->clear();
+				file_popup->add_item(TTR("Save"), FILE_MENU_SAVE_ANIMATION);
+				file_popup->add_item(TTR("Save As"), FILE_MENU_SAVE_AS_ANIMATION);
+				file_popup->add_separator();
+				file_popup->add_item(TTR("Make Unique"), FILE_MENU_MAKE_ANIMATION_UNIQUE);
+				file_popup->add_separator();
+				file_popup->add_item(TTR("Open in Inspector"), FILE_MENU_EDIT_ANIMATION);
+				Rect2 pos = tree->get_item_rect(p_item, 1, 0);
+				Vector2 popup_pos = tree->get_screen_position() + pos.position + Vector2(0, pos.size.height);
+				file_popup->popup(Rect2(popup_pos, Size2()));
+
+				file_dialog_animation = anim_name;
+				file_dialog_library = lib_name;
+
+			} break;
+			case ANIM_BUTTON_DELETE: {
+				UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+				undo_redo->create_action(vformat(TTR("Remove Animation from Library: %s"), anim_name));
+				undo_redo->add_do_method(al.ptr(), "remove_animation", anim_name);
+				undo_redo->add_undo_method(al.ptr(), "add_animation", anim_name, anim);
+				undo_redo->add_do_method(this, "_update_editor", player);
+				undo_redo->add_undo_method(this, "_update_editor", player);
+				undo_redo->commit_action();
+			} break;
+		}
+	}
+}
+
+void AnimationLibraryEditor::update_tree() {
+	if (updating) {
+		return;
+	}
+
+	tree->clear();
+	ERR_FAIL_COND(!player);
+
+	Color ss_color = get_theme_color(SNAME("prop_subsection"), SNAME("Editor"));
+
+	TreeItem *root = tree->create_item();
+	TypedArray<StringName> libs = player->call("get_animation_library_list");
+
+	for (int i = 0; i < libs.size(); i++) {
+		const StringName K = libs[i];
+		TreeItem *libitem = tree->create_item(root);
+		libitem->set_text(0, K);
+		if (K == StringName()) {
+			libitem->set_suffix(0, TTR("[Global]"));
+		} else {
+			libitem->set_suffix(0, "");
+		}
+		libitem->set_editable(0, true);
+		libitem->set_metadata(0, K);
+		libitem->set_icon(0, get_theme_icon("AnimationLibrary", "EditorIcons"));
+		libitem->add_button(0, get_theme_icon("Add", "EditorIcons"), LIB_BUTTON_ADD, false, TTR("Add Animation to Library"));
+		libitem->add_button(0, get_theme_icon("Load", "EditorIcons"), LIB_BUTTON_LOAD, false, TTR("Load animation from file and add to library"));
+		libitem->add_button(0, get_theme_icon("ActionPaste", "EditorIcons"), LIB_BUTTON_PASTE, false, TTR("Paste Animation to Library from clipboard"));
+		Ref<AnimationLibrary> al = player->call("get_animation_library", K);
+		if (al->get_path().is_resource_file()) {
+			libitem->set_text(1, al->get_path().get_file());
+			libitem->set_tooltip(1, al->get_path());
+		} else {
+			libitem->set_text(1, TTR("[built-in]"));
+		}
+		libitem->add_button(1, get_theme_icon("Save", "EditorIcons"), LIB_BUTTON_FILE, false, TTR("Save animation library to resource on disk"));
+		libitem->add_button(1, get_theme_icon("Remove", "EditorIcons"), LIB_BUTTON_DELETE, false, TTR("Remove animation library"));
+
+		libitem->set_custom_bg_color(0, ss_color);
+
+		List<StringName> animations;
+		al->get_animation_list(&animations);
+		for (const StringName &L : animations) {
+			TreeItem *anitem = tree->create_item(libitem);
+			anitem->set_text(0, L);
+			anitem->set_editable(0, true);
+			anitem->set_metadata(0, L);
+			anitem->set_icon(0, get_theme_icon("Animation", "EditorIcons"));
+			anitem->add_button(0, get_theme_icon("ActionCopy", "EditorIcons"), ANIM_BUTTON_COPY, false, TTR("Copy animation to clipboard"));
+			Ref<Animation> anim = al->get_animation(L);
+
+			if (anim->get_path().is_resource_file()) {
+				anitem->set_text(1, anim->get_path().get_file());
+				anitem->set_tooltip(1, anim->get_path());
+			} else {
+				anitem->set_text(1, TTR("[built-in]"));
+			}
+			anitem->add_button(1, get_theme_icon("Save", "EditorIcons"), ANIM_BUTTON_FILE, false, TTR("Save animation to resource on disk"));
+			anitem->add_button(1, get_theme_icon("Remove", "EditorIcons"), ANIM_BUTTON_DELETE, false, TTR("Remove animation from Library"));
+		}
+	}
+}
+
+void AnimationLibraryEditor::show_dialog() {
+	update_tree();
+	popup_centered_ratio(0.5);
+}
+
+void AnimationLibraryEditor::_update_editor(Object *p_player) {
+	emit_signal("update_editor", p_player);
+}
+
+void AnimationLibraryEditor::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("_update_editor", "player"), &AnimationLibraryEditor::_update_editor);
+	ADD_SIGNAL(MethodInfo("update_editor"));
+}
+
+AnimationLibraryEditor::AnimationLibraryEditor() {
+	set_title(TTR("Edit Animation Libraries"));
+
+	file_dialog = memnew(EditorFileDialog);
+	add_child(file_dialog);
+	file_dialog->connect("file_selected", callable_mp(this, &AnimationLibraryEditor::_load_file));
+
+	add_library_dialog = memnew(ConfirmationDialog);
+	VBoxContainer *dialog_vb = memnew(VBoxContainer);
+	add_library_name = memnew(LineEdit);
+	dialog_vb->add_child(add_library_name);
+	add_library_name->connect("text_changed", callable_mp(this, &AnimationLibraryEditor::_add_library_validate));
+	add_child(add_library_dialog);
+
+	add_library_validate = memnew(Label);
+	dialog_vb->add_child(add_library_validate);
+	add_library_dialog->add_child(dialog_vb);
+	add_library_dialog->connect("confirmed", callable_mp(this, &AnimationLibraryEditor::_add_library_confirm));
+	add_library_dialog->register_text_enter(add_library_name);
+
+	VBoxContainer *vb = memnew(VBoxContainer);
+	HBoxContainer *hb = memnew(HBoxContainer);
+	hb->add_spacer(true);
+	Button *b = memnew(Button(TTR("Add Library")));
+	b->connect("pressed", callable_mp(this, &AnimationLibraryEditor::_add_library));
+	hb->add_child(b);
+	b = memnew(Button(TTR("Load Library")));
+	b->connect("pressed", callable_mp(this, &AnimationLibraryEditor::_load_library));
+	hb->add_child(b);
+	vb->add_child(hb);
+	tree = memnew(Tree);
+	vb->add_child(tree);
+
+	tree->set_columns(2);
+	tree->set_column_titles_visible(true);
+	tree->set_column_title(0, TTR("Resource"));
+	tree->set_column_title(1, TTR("Storage"));
+	tree->set_column_expand(0, true);
+	tree->set_column_custom_minimum_width(1, EDSCALE * 250);
+	tree->set_column_expand(1, false);
+	tree->set_hide_root(true);
+	tree->set_hide_folding(true);
+	tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+
+	tree->connect("item_edited", callable_mp(this, &AnimationLibraryEditor::_item_renamed));
+	tree->connect("button_pressed", callable_mp(this, &AnimationLibraryEditor::_button_pressed));
+
+	file_popup = memnew(PopupMenu);
+	add_child(file_popup);
+	file_popup->connect("id_pressed", callable_mp(this, &AnimationLibraryEditor::_file_popup_selected));
+
+	add_child(vb);
+
+	error_dialog = memnew(AcceptDialog);
+	error_dialog->set_title(TTR("Error:"));
+	add_child(error_dialog);
+}

+ 119 - 0
editor/plugins/animation_library_editor.h

@@ -0,0 +1,119 @@
+/*************************************************************************/
+/*  animation_library_editor.h                                           */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* 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.                */
+/*************************************************************************/
+
+#ifndef ANIMATION_LIBRARY_EDITOR_H
+#define ANIMATION_LIBRARY_EDITOR_H
+
+#include "editor/animation_track_editor.h"
+#include "editor/editor_plugin.h"
+#include "scene/animation/animation_player.h"
+#include "scene/gui/dialogs.h"
+#include "scene/gui/tree.h"
+
+class EditorFileDialog;
+
+class AnimationLibraryEditor : public AcceptDialog {
+	GDCLASS(AnimationLibraryEditor, AcceptDialog)
+
+	enum {
+		LIB_BUTTON_ADD,
+		LIB_BUTTON_LOAD,
+		LIB_BUTTON_PASTE,
+		LIB_BUTTON_FILE,
+		LIB_BUTTON_DELETE,
+	};
+	enum {
+		ANIM_BUTTON_COPY,
+		ANIM_BUTTON_FILE,
+		ANIM_BUTTON_DELETE,
+	};
+
+	enum FileMenuAction {
+		FILE_MENU_SAVE_LIBRARY,
+		FILE_MENU_SAVE_AS_LIBRARY,
+		FILE_MENU_MAKE_LIBRARY_UNIQUE,
+		FILE_MENU_EDIT_LIBRARY,
+
+		FILE_MENU_SAVE_ANIMATION,
+		FILE_MENU_SAVE_AS_ANIMATION,
+		FILE_MENU_MAKE_ANIMATION_UNIQUE,
+		FILE_MENU_EDIT_ANIMATION,
+	};
+
+	enum FileDialogAction {
+		FILE_DIALOG_ACTION_OPEN_LIBRARY,
+		FILE_DIALOG_ACTION_SAVE_LIBRARY,
+		FILE_DIALOG_ACTION_OPEN_ANIMATION,
+		FILE_DIALOG_ACTION_SAVE_ANIMATION,
+	};
+
+	FileDialogAction file_dialog_action = FILE_DIALOG_ACTION_OPEN_ANIMATION;
+
+	StringName file_dialog_animation;
+	StringName file_dialog_library;
+
+	AcceptDialog *error_dialog = nullptr;
+	bool adding_animation = false;
+	StringName adding_animation_to_library;
+	EditorFileDialog *file_dialog = nullptr;
+	ConfirmationDialog *add_library_dialog = nullptr;
+	LineEdit *add_library_name = nullptr;
+	Label *add_library_validate = nullptr;
+	PopupMenu *file_popup = nullptr;
+
+	Tree *tree = nullptr;
+
+	Object *player = nullptr;
+
+	void _add_library();
+	void _add_library_validate(const String &p_name);
+	void _add_library_confirm();
+	void _load_library();
+	void _load_file(String p_path);
+
+	void _item_renamed();
+	void _button_pressed(TreeItem *p_item, int p_column, int p_button);
+
+	void _file_popup_selected(int p_id);
+
+	bool updating = false;
+
+protected:
+	void _update_editor(Object *p_player);
+	static void _bind_methods();
+
+public:
+	void set_animation_player(Object *p_player);
+	void show_dialog();
+	void update_tree();
+	AnimationLibraryEditor();
+};
+
+#endif // ANIMATIONPLAYERLIBRARYEDITOR_H

+ 160 - 284
editor/plugins/animation_player_editor_plugin.cpp

@@ -47,6 +47,8 @@
 #include "scene/scene_string_names.h"
 #include "servers/rendering_server.h"
 
+///////////////////////////////////
+
 void AnimationPlayerEditor::_node_removed(Node *p_node) {
 	if (player && player == p_node) {
 		player = nullptr;
@@ -148,9 +150,7 @@ void AnimationPlayerEditor::_notification(int p_what) {
 #define ITEM_ICON(m_item, m_icon) tool_anim->get_popup()->set_item_icon(tool_anim->get_popup()->get_item_index(m_item), get_theme_icon(SNAME(m_icon), SNAME("EditorIcons")))
 
 			ITEM_ICON(TOOL_NEW_ANIM, "New");
-			ITEM_ICON(TOOL_LOAD_ANIM, "Load");
-			ITEM_ICON(TOOL_SAVE_ANIM, "Save");
-			ITEM_ICON(TOOL_SAVE_AS_ANIM, "Save");
+			ITEM_ICON(TOOL_ANIM_LIBRARY, "AnimationLibrary");
 			ITEM_ICON(TOOL_DUPLICATE_ANIM, "Duplicate");
 			ITEM_ICON(TOOL_RENAME_ANIM, "Rename");
 			ITEM_ICON(TOOL_EDIT_TRANSITIONS, "Blend");
@@ -166,7 +166,7 @@ void AnimationPlayerEditor::_autoplay_pressed() {
 	if (updating) {
 		return;
 	}
-	if (animation->get_item_count() == 0) {
+	if (animation->has_selectable_items() == 0) {
 		return;
 	}
 
@@ -192,10 +192,7 @@ void AnimationPlayerEditor::_autoplay_pressed() {
 }
 
 void AnimationPlayerEditor::_play_pressed() {
-	String current;
-	if (animation->get_selected() >= 0 && animation->get_selected() < animation->get_item_count()) {
-		current = animation->get_item_text(animation->get_selected());
-	}
+	String current = _get_current();
 
 	if (!current.is_empty()) {
 		if (current == player->get_assigned_animation()) {
@@ -209,10 +206,7 @@ void AnimationPlayerEditor::_play_pressed() {
 }
 
 void AnimationPlayerEditor::_play_from_pressed() {
-	String current;
-	if (animation->get_selected() >= 0 && animation->get_selected() < animation->get_item_count()) {
-		current = animation->get_item_text(animation->get_selected());
-	}
+	String current = _get_current();
 
 	if (!current.is_empty()) {
 		float time = player->get_current_animation_position();
@@ -229,12 +223,15 @@ void AnimationPlayerEditor::_play_from_pressed() {
 	stop->set_pressed(false);
 }
 
-void AnimationPlayerEditor::_play_bw_pressed() {
+String AnimationPlayerEditor::_get_current() const {
 	String current;
-	if (animation->get_selected() >= 0 && animation->get_selected() < animation->get_item_count()) {
+	if (animation->get_selected() >= 0 && animation->get_selected() < animation->get_item_count() && !animation->is_item_separator(animation->get_selected())) {
 		current = animation->get_item_text(animation->get_selected());
 	}
-
+	return current;
+}
+void AnimationPlayerEditor::_play_bw_pressed() {
+	String current = _get_current();
 	if (!current.is_empty()) {
 		if (current == player->get_assigned_animation()) {
 			player->stop(); //so it won't blend with itself
@@ -247,10 +244,7 @@ void AnimationPlayerEditor::_play_bw_pressed() {
 }
 
 void AnimationPlayerEditor::_play_bw_from_pressed() {
-	String current;
-	if (animation->get_selected() >= 0 && animation->get_selected() < animation->get_item_count()) {
-		current = animation->get_item_text(animation->get_selected());
-	}
+	String current = _get_current();
 
 	if (!current.is_empty()) {
 		float time = player->get_current_animation_position();
@@ -282,10 +276,7 @@ void AnimationPlayerEditor::_animation_selected(int p_which) {
 	}
 	// when selecting an animation, the idea is that the only interesting behavior
 	// ui-wise is that it should play/blend the next one if currently playing
-	String current;
-	if (animation->get_selected() >= 0 && animation->get_selected() < animation->get_item_count()) {
-		current = animation->get_item_text(animation->get_selected());
-	}
+	String current = _get_current();
 
 	if (!current.is_empty()) {
 		player->set_assigned_animation(current);
@@ -330,6 +321,20 @@ void AnimationPlayerEditor::_animation_new() {
 		break;
 	}
 
+	List<StringName> libraries;
+	player->get_animation_library_list(&libraries);
+	library->clear();
+	for (const StringName &K : libraries) {
+		library->add_item((K == StringName()) ? String(TTR("[Global]")) : String(K));
+		library->set_item_metadata(0, String(K));
+	}
+
+	if (libraries.size() > 1) {
+		library->show();
+	} else {
+		library->hide();
+	}
+
 	name->set_text(base);
 	name_dialog->popup_centered(Size2(300, 90));
 	name->select_all();
@@ -337,7 +342,7 @@ void AnimationPlayerEditor::_animation_new() {
 }
 
 void AnimationPlayerEditor::_animation_rename() {
-	if (animation->get_item_count() == 0) {
+	if (!animation->has_selectable_items()) {
 		return;
 	}
 	int selected = animation->get_selected();
@@ -349,84 +354,11 @@ void AnimationPlayerEditor::_animation_rename() {
 	name_dialog->popup_centered(Size2(300, 90));
 	name->select_all();
 	name->grab_focus();
-}
-
-void AnimationPlayerEditor::_animation_load() {
-	ERR_FAIL_COND(!player);
-	file->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILES);
-	file->clear_filters();
-	List<String> extensions;
-
-	ResourceLoader::get_recognized_extensions_for_type("Animation", &extensions);
-	for (const String &E : extensions) {
-		file->add_filter("*." + E + " ; " + E.to_upper());
-	}
-
-	file->popup_file_dialog();
-}
-
-void AnimationPlayerEditor::_animation_save_in_path(const Ref<Resource> &p_resource, const String &p_path) {
-	int flg = 0;
-	if (EditorSettings::get_singleton()->get("filesystem/on_save/compress_binary_resources")) {
-		flg |= ResourceSaver::FLAG_COMPRESS;
-	}
-
-	String path = ProjectSettings::get_singleton()->localize_path(p_path);
-	Error err = ResourceSaver::save(path, p_resource, flg | ResourceSaver::FLAG_REPLACE_SUBRESOURCE_PATHS);
-
-	if (err != OK) {
-		EditorNode::get_singleton()->show_warning(TTR("Error saving resource!"));
-		return;
-	}
-
-	((Resource *)p_resource.ptr())->set_path(path);
-	EditorNode::get_singleton()->emit_signal(SNAME("resource_saved"), p_resource);
-}
-
-void AnimationPlayerEditor::_animation_save(const Ref<Resource> &p_resource) {
-	if (p_resource->get_path().is_resource_file()) {
-		_animation_save_in_path(p_resource, p_resource->get_path());
-	} else {
-		_animation_save_as(p_resource);
-	}
-}
-
-void AnimationPlayerEditor::_animation_save_as(const Ref<Resource> &p_resource) {
-	file->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
-
-	List<String> extensions;
-	ResourceSaver::get_recognized_extensions(p_resource, &extensions);
-	file->clear_filters();
-	for (int i = 0; i < extensions.size(); i++) {
-		file->add_filter("*." + extensions[i] + " ; " + extensions[i].to_upper());
-	}
-
-	String path;
-	//file->set_current_path(current_path);
-	if (!p_resource->get_path().is_empty()) {
-		path = p_resource->get_path();
-		if (extensions.size()) {
-			if (extensions.find(p_resource->get_path().get_extension().to_lower()) == nullptr) {
-				path = p_resource->get_path().get_base_dir() + p_resource->get_name() + "." + extensions.front()->get();
-			}
-		}
-	} else {
-		if (extensions.size()) {
-			if (!p_resource->get_name().is_empty()) {
-				path = p_resource->get_name() + "." + extensions.front()->get().to_lower();
-			} else {
-				String resource_name_snake_case = p_resource->get_class().camelcase_to_underscore();
-				path = "new_" + resource_name_snake_case + "." + extensions.front()->get().to_lower();
-			}
-		}
-	}
-	file->set_current_path(path);
-	file->set_title(TTR("Save Resource As..."));
-	file->popup_file_dialog();
+	library->hide();
 }
 
 void AnimationPlayerEditor::_animation_remove() {
-	if (animation->get_item_count() == 0) {
+	if (!animation->has_selectable_items()) {
 		return;
 	}
 
@@ -440,6 +372,9 @@ void AnimationPlayerEditor::_animation_remove_confirmed() {
 	String current = animation->get_item_text(animation->get_selected());
 	Ref<Animation> anim = player->get_animation(current);
 
+	Ref<AnimationLibrary> al = player->get_animation_library(player->find_animation_library(anim));
+	ERR_FAIL_COND(al.is_null());
+
 	undo_redo->create_action(TTR("Remove Animation"));
 	if (player->get_autoplay() == current) {
 		undo_redo->add_do_method(player, "set_autoplay", "");
@@ -447,11 +382,11 @@ void AnimationPlayerEditor::_animation_remove_confirmed() {
 		// Avoid having the autoplay icon linger around if there is only one animation in the player.
 		undo_redo->add_do_method(this, "_animation_player_changed", player);
 	}
-	undo_redo->add_do_method(player, "remove_animation", current);
-	undo_redo->add_undo_method(player, "add_animation", current, anim);
+	undo_redo->add_do_method(al.ptr(), "remove_animation", current);
+	undo_redo->add_undo_method(al.ptr(), "add_animation", current, anim);
 	undo_redo->add_do_method(this, "_animation_player_changed", player);
 	undo_redo->add_undo_method(this, "_animation_player_changed", player);
-	if (animation->get_item_count() == 1) {
+	if (animation->has_selectable_items() && animation->get_selectable_item(false) == animation->get_selectable_item(true)) { // Last item remaining.
 		undo_redo->add_do_method(this, "_stop_onion_skinning");
 		undo_redo->add_undo_method(this, "_start_onion_skinning");
 	}
@@ -498,7 +433,7 @@ void AnimationPlayerEditor::_animation_name_edited() {
 		return;
 	}
 
-	if (name_dialog_op == TOOL_RENAME_ANIM && animation->get_item_count() > 0 && animation->get_item_text(animation->get_selected()) == new_name) {
+	if (name_dialog_op == TOOL_RENAME_ANIM && animation->has_selectable_items() && animation->get_item_text(animation->get_selected()) == new_name) {
 		name_dialog->hide();
 		return;
 	}
@@ -514,10 +449,13 @@ void AnimationPlayerEditor::_animation_name_edited() {
 			String current = animation->get_item_text(animation->get_selected());
 			Ref<Animation> anim = player->get_animation(current);
 
+			Ref<AnimationLibrary> al = player->get_animation_library(player->find_animation_library(anim));
+			ERR_FAIL_COND(al.is_null());
+
 			undo_redo->create_action(TTR("Rename Animation"));
-			undo_redo->add_do_method(player, "rename_animation", current, new_name);
+			undo_redo->add_do_method(al.ptr(), "rename_animation", current, new_name);
 			undo_redo->add_do_method(anim.ptr(), "set_name", new_name);
-			undo_redo->add_undo_method(player, "rename_animation", new_name, current);
+			undo_redo->add_undo_method(al.ptr(), "rename_animation", new_name, current);
 			undo_redo->add_undo_method(anim.ptr(), "set_name", current);
 			undo_redo->add_do_method(this, "_animation_player_changed", player);
 			undo_redo->add_undo_method(this, "_animation_player_changed", player);
@@ -530,15 +468,35 @@ void AnimationPlayerEditor::_animation_name_edited() {
 			Ref<Animation> new_anim = Ref<Animation>(memnew(Animation));
 			new_anim->set_name(new_name);
 
+			Ref<AnimationLibrary> al;
+			if (library->is_visible()) {
+				al = player->get_animation_library(library->get_item_metadata(library->get_selected()));
+			} else {
+				if (player->has_animation_library("")) {
+					al = player->get_animation_library("");
+				}
+			}
+
 			undo_redo->create_action(TTR("Add Animation"));
-			undo_redo->add_do_method(player, "add_animation", new_name, new_anim);
-			undo_redo->add_undo_method(player, "remove_animation", new_name);
+
+			bool lib_added = false;
+			if (al.is_null()) {
+				al.instantiate();
+				lib_added = true;
+				undo_redo->add_do_method(player, "add_animation_library", "", al);
+			}
+
+			undo_redo->add_do_method(al.ptr(), "add_animation", new_name, new_anim);
+			undo_redo->add_undo_method(al.ptr(), "remove_animation", new_name);
 			undo_redo->add_do_method(this, "_animation_player_changed", player);
 			undo_redo->add_undo_method(this, "_animation_player_changed", player);
-			if (animation->get_item_count() == 0) {
+			if (!animation->has_selectable_items()) {
 				undo_redo->add_do_method(this, "_start_onion_skinning");
 				undo_redo->add_undo_method(this, "_stop_onion_skinning");
 			}
+			if (lib_added) {
+				undo_redo->add_undo_method(player, "remove_animation_library", "");
+			}
 			undo_redo->commit_action();
 
 			_select_anim_by_name(new_name);
@@ -551,9 +509,11 @@ void AnimationPlayerEditor::_animation_name_edited() {
 			Ref<Animation> new_anim = _animation_clone(anim);
 			new_anim->set_name(new_name);
 
+			Ref<AnimationLibrary> library = player->get_animation_library(player->find_animation_library(anim));
+
 			undo_redo->create_action(TTR("Duplicate Animation"));
-			undo_redo->add_do_method(player, "add_animation", new_name, new_anim);
-			undo_redo->add_undo_method(player, "remove_animation", new_name);
+			undo_redo->add_do_method(library.ptr(), "add_animation", new_name, new_anim);
+			undo_redo->add_undo_method(library.ptr(), "remove_animation", new_name);
 			undo_redo->add_do_method(player, "animation_set_next", new_name, player->animation_get_next(current));
 			undo_redo->add_do_method(this, "_animation_player_changed", player);
 			undo_redo->add_undo_method(this, "_animation_player_changed", player);
@@ -567,7 +527,7 @@ void AnimationPlayerEditor::_animation_name_edited() {
 }
 
 void AnimationPlayerEditor::_blend_editor_next_changed(const int p_idx) {
-	if (animation->get_item_count() == 0) {
+	if (!animation->has_selectable_items()) {
 		return;
 	}
 
@@ -588,7 +548,7 @@ void AnimationPlayerEditor::_animation_blend() {
 
 	blend_editor.tree->clear();
 
-	if (animation->get_item_count() == 0) {
+	if (!animation->has_selectable_items()) {
 		return;
 	}
 
@@ -643,7 +603,7 @@ void AnimationPlayerEditor::_blend_edited() {
 		return;
 	}
 
-	if (animation->get_item_count() == 0) {
+	if (!animation->has_selectable_items()) {
 		return;
 	}
 
@@ -722,16 +682,16 @@ void AnimationPlayerEditor::set_state(const Dictionary &p_state) {
 }
 
 void AnimationPlayerEditor::_animation_resource_edit() {
-	if (animation->get_item_count()) {
-		String current = animation->get_item_text(animation->get_selected());
+	String current = _get_current();
+	if (current != String()) {
 		Ref<Animation> anim = player->get_animation(current);
 		EditorNode::get_singleton()->edit_resource(anim);
 	}
 }
 
 void AnimationPlayerEditor::_animation_edit() {
-	if (animation->get_item_count()) {
-		String current = animation->get_item_text(animation->get_selected());
+	String current = _get_current();
+	if (current != String()) {
 		Ref<Animation> anim = player->get_animation(current);
 		track_editor->set_animation(anim);
 
@@ -745,51 +705,6 @@ void AnimationPlayerEditor::_animation_edit() {
 	}
 }
 
-void AnimationPlayerEditor::_save_animation(String p_file) {
-	String current = animation->get_item_text(animation->get_selected());
-	if (!current.is_empty()) {
-		Ref<Animation> anim = player->get_animation(current);
-
-		ERR_FAIL_COND(!Object::cast_to<Resource>(*anim));
-
-		RES current_res = RES(Object::cast_to<Resource>(*anim));
-
-		_animation_save_in_path(current_res, p_file);
-	}
-}
-
-void AnimationPlayerEditor::_load_animations(Vector<String> p_files) {
-	ERR_FAIL_COND(!player);
-
-	for (int i = 0; i < p_files.size(); i++) {
-		String file = p_files[i];
-
-		Ref<Resource> res = ResourceLoader::load(file, "Animation");
-		ERR_FAIL_COND_MSG(res.is_null(), "Cannot load Animation from file '" + file + "'.");
-		ERR_FAIL_COND_MSG(!res->is_class("Animation"), "Loaded resource from file '" + file + "' is not Animation.");
-		if (file.rfind("/") != -1) {
-			file = file.substr(file.rfind("/") + 1, file.length());
-		}
-		if (file.rfind("\\") != -1) {
-			file = file.substr(file.rfind("\\") + 1, file.length());
-		}
-
-		if (file.contains(".")) {
-			file = file.substr(0, file.find("."));
-		}
-
-		undo_redo->create_action(TTR("Load Animation"));
-		undo_redo->add_do_method(player, "add_animation", file, res);
-		undo_redo->add_undo_method(player, "remove_animation", file);
-		if (player->has_animation(file)) {
-			undo_redo->add_undo_method(player, "add_animation", file, player->get_animation(file));
-		}
-		undo_redo->add_do_method(this, "_animation_player_changed", player);
-		undo_redo->add_undo_method(this, "_animation_player_changed", player);
-		undo_redo->commit_action();
-	}
-}
-
 void AnimationPlayerEditor::_scale_changed(const String &p_scale) {
 	player->set_speed_scale(p_scale.to_float());
 }
@@ -824,49 +739,66 @@ void AnimationPlayerEditor::_update_animation() {
 
 void AnimationPlayerEditor::_update_player() {
 	updating = true;
-	List<StringName> animlist;
-	if (player) {
-		player->get_animation_list(&animlist);
-	}
 
 	animation->clear();
 
-#define ITEM_DISABLED(m_item, m_disabled) tool_anim->get_popup()->set_item_disabled(tool_anim->get_popup()->get_item_index(m_item), m_disabled)
-
-	ITEM_DISABLED(TOOL_SAVE_ANIM, animlist.size() == 0);
-	ITEM_DISABLED(TOOL_SAVE_AS_ANIM, animlist.size() == 0);
-	ITEM_DISABLED(TOOL_DUPLICATE_ANIM, animlist.size() == 0);
-	ITEM_DISABLED(TOOL_RENAME_ANIM, animlist.size() == 0);
-	ITEM_DISABLED(TOOL_EDIT_TRANSITIONS, animlist.size() == 0);
-	ITEM_DISABLED(TOOL_COPY_ANIM, animlist.size() == 0);
-	ITEM_DISABLED(TOOL_REMOVE_ANIM, animlist.size() == 0);
-
-	stop->set_disabled(animlist.size() == 0);
-	play->set_disabled(animlist.size() == 0);
-	play_bw->set_disabled(animlist.size() == 0);
-	play_bw_from->set_disabled(animlist.size() == 0);
-	play_from->set_disabled(animlist.size() == 0);
-	frame->set_editable(animlist.size() != 0);
-	animation->set_disabled(animlist.size() == 0);
-	autoplay->set_disabled(animlist.size() == 0);
-	tool_anim->set_disabled(player == nullptr);
-	onion_toggle->set_disabled(animlist.size() == 0);
-	onion_skinning->set_disabled(animlist.size() == 0);
-	pin->set_disabled(player == nullptr);
-
 	if (!player) {
 		AnimationPlayerEditor::get_singleton()->get_track_editor()->update_keying();
 		return;
 	}
 
+	List<StringName> libraries;
+	if (player) {
+		player->get_animation_library_list(&libraries);
+	}
+
 	int active_idx = -1;
-	for (const StringName &E : animlist) {
-		animation->add_item(E);
+	bool no_anims_found = true;
 
-		if (player->get_assigned_animation() == E) {
-			active_idx = animation->get_item_count() - 1;
+	for (const StringName &K : libraries) {
+		if (K != StringName()) {
+			animation->add_separator(K);
+		}
+
+		Ref<AnimationLibrary> library = player->get_animation_library(K);
+		List<StringName> animlist;
+		library->get_animation_list(&animlist);
+
+		for (const StringName &E : animlist) {
+			String path = K;
+			if (path != "") {
+				path += "/";
+			}
+			path += E;
+			animation->add_item(path);
+			if (player->get_assigned_animation() == path) {
+				active_idx = animation->get_selectable_item(true);
+			}
+			no_anims_found = false;
 		}
 	}
+#define ITEM_CHECK_DISABLED(m_item) tool_anim->get_popup()->set_item_disabled(tool_anim->get_popup()->get_item_index(m_item), no_anims_found)
+
+	ITEM_CHECK_DISABLED(TOOL_DUPLICATE_ANIM);
+	ITEM_CHECK_DISABLED(TOOL_RENAME_ANIM);
+	ITEM_CHECK_DISABLED(TOOL_EDIT_TRANSITIONS);
+	ITEM_CHECK_DISABLED(TOOL_REMOVE_ANIM);
+
+#undef ITEM_CHECK_DISABLED
+
+	stop->set_disabled(no_anims_found);
+	play->set_disabled(no_anims_found);
+	play_bw->set_disabled(no_anims_found);
+	play_bw_from->set_disabled(no_anims_found);
+	play_from->set_disabled(no_anims_found);
+	frame->set_editable(!no_anims_found);
+	animation->set_disabled(no_anims_found);
+	autoplay->set_disabled(no_anims_found);
+	tool_anim->set_disabled(player == nullptr);
+	onion_toggle->set_disabled(no_anims_found);
+	onion_skinning->set_disabled(no_anims_found);
+	pin->set_disabled(player == nullptr);
+
 	_update_animation_list_icons();
 
 	updating = false;
@@ -874,16 +806,16 @@ void AnimationPlayerEditor::_update_player() {
 		animation->select(active_idx);
 		autoplay->set_pressed(animation->get_item_text(active_idx) == player->get_autoplay());
 		_animation_selected(active_idx);
-
-	} else if (animation->get_item_count() > 0) {
-		animation->select(0);
-		autoplay->set_pressed(animation->get_item_text(0) == player->get_autoplay());
-		_animation_selected(0);
+	} else if (animation->has_selectable_items()) {
+		int item = animation->get_selectable_item();
+		animation->select(item);
+		autoplay->set_pressed(animation->get_item_text(item) == player->get_autoplay());
+		_animation_selected(item);
 	} else {
 		_animation_selected(0);
 	}
 
-	if (animation->get_item_count()) {
+	if (!no_anims_found) {
 		String current = animation->get_item_text(animation->get_selected());
 		Ref<Animation> anim = player->get_animation(current);
 		track_editor->set_animation(anim);
@@ -899,6 +831,9 @@ void AnimationPlayerEditor::_update_player() {
 void AnimationPlayerEditor::_update_animation_list_icons() {
 	for (int i = 0; i < animation->get_item_count(); i++) {
 		String name = animation->get_item_text(i);
+		if (animation->is_item_disabled(i) || animation->is_item_separator(i)) {
+			continue;
+		}
 
 		Ref<Texture2D> icon;
 		if (name == player->get_autoplay()) {
@@ -925,7 +860,7 @@ void AnimationPlayerEditor::edit(AnimationPlayer *p_player) {
 		_update_player();
 
 		if (onion.enabled) {
-			if (animation->get_item_count() > 0) {
+			if (animation->has_selectable_items()) {
 				_start_onion_skinning();
 			} else {
 				_stop_onion_skinning();
@@ -940,6 +875,8 @@ void AnimationPlayerEditor::edit(AnimationPlayer *p_player) {
 
 		track_editor->show_select_node_warning(true);
 	}
+
+	library_editor->set_animation_player(player);
 }
 
 void AnimationPlayerEditor::forward_force_draw_over_viewport(Control *p_overlay) {
@@ -993,7 +930,7 @@ void AnimationPlayerEditor::forward_force_draw_over_viewport(Control *p_overlay)
 }
 
 void AnimationPlayerEditor::_animation_duplicate() {
-	if (!animation->get_item_count()) {
+	if (!animation->has_selectable_items()) {
 		return;
 	}
 
@@ -1031,29 +968,6 @@ Ref<Animation> AnimationPlayerEditor::_animation_clone(Ref<Animation> p_anim) {
 	return new_anim;
 }
 
-void AnimationPlayerEditor::_animation_paste(Ref<Animation> p_anim) {
-	String name = p_anim->get_name();
-	if (name.is_empty()) {
-		name = TTR("Pasted Animation");
-	}
-
-	int idx = 1;
-	String base = name;
-	while (player->has_animation(name)) {
-		idx++;
-		name = base + " " + itos(idx);
-	}
-
-	undo_redo->create_action(TTR("Paste Animation"));
-	undo_redo->add_do_method(player, "add_animation", name, p_anim);
-	undo_redo->add_undo_method(player, "remove_animation", name);
-	undo_redo->add_do_method(this, "_animation_player_changed", player);
-	undo_redo->add_undo_method(this, "_animation_player_changed", player);
-	undo_redo->commit_action();
-
-	_select_anim_by_name(name);
-}
-
 void AnimationPlayerEditor::_seek_value_changed(float p_value, bool p_set, bool p_timeline_only) {
 	if (updating || !player || player->is_playing()) {
 		return;
@@ -1095,6 +1009,9 @@ void AnimationPlayerEditor::_animation_player_changed(Object *p_pl) {
 		if (blend_editor.dialog->is_visible()) {
 			_animation_blend(); // Update.
 		}
+		if (library_editor->is_visible()) {
+			library_editor->update_tree();
+		}
 	}
 }
 
@@ -1134,10 +1051,7 @@ void AnimationPlayerEditor::_animation_key_editor_seek(float p_pos, bool p_drag,
 }
 
 void AnimationPlayerEditor::_animation_tool_menu(int p_option) {
-	String current;
-	if (animation->get_selected() >= 0 && animation->get_selected() < animation->get_item_count()) {
-		current = animation->get_item_text(animation->get_selected());
-	}
+	String current = _get_current();
 
 	Ref<Animation> anim;
 	if (!current.is_empty()) {
@@ -1148,18 +1062,9 @@ void AnimationPlayerEditor::_animation_tool_menu(int p_option) {
 		case TOOL_NEW_ANIM: {
 			_animation_new();
 		} break;
-		case TOOL_LOAD_ANIM: {
-			_animation_load();
-		} break;
-		case TOOL_SAVE_ANIM: {
-			if (anim.is_valid()) {
-				_animation_save(anim);
-			}
-		} break;
-		case TOOL_SAVE_AS_ANIM: {
-			if (anim.is_valid()) {
-				_animation_save_as(anim);
-			}
+		case TOOL_ANIM_LIBRARY: {
+			library_editor->set_animation_player(player);
+			library_editor->show_dialog();
 		} break;
 		case TOOL_DUPLICATE_ANIM: {
 			_animation_duplicate();
@@ -1173,39 +1078,8 @@ void AnimationPlayerEditor::_animation_tool_menu(int p_option) {
 		case TOOL_REMOVE_ANIM: {
 			_animation_remove();
 		} break;
-		case TOOL_COPY_ANIM: {
-			if (!animation->get_item_count()) {
-				error_dialog->set_text(TTR("No animation to copy!"));
-				error_dialog->popup_centered();
-				return;
-			}
-
-			String current2 = animation->get_item_text(animation->get_selected());
-			Ref<Animation> anim2 = player->get_animation(current2);
-			EditorSettings::get_singleton()->set_resource_clipboard(anim2);
-		} break;
-		case TOOL_PASTE_ANIM: {
-			Ref<Animation> anim2 = EditorSettings::get_singleton()->get_resource_clipboard();
-			if (!anim2.is_valid()) {
-				error_dialog->set_text(TTR("No animation resource in clipboard!"));
-				error_dialog->popup_centered();
-				return;
-			}
-			Ref<Animation> new_anim = _animation_clone(anim2);
-			_animation_paste(new_anim);
-		} break;
-		case TOOL_PASTE_ANIM_REF: {
-			Ref<Animation> anim2 = EditorSettings::get_singleton()->get_resource_clipboard();
-			if (!anim2.is_valid()) {
-				error_dialog->set_text(TTR("No animation resource in clipboard!"));
-				error_dialog->popup_centered();
-				return;
-			}
-
-			_animation_paste(anim2);
-		} break;
 		case TOOL_EDIT_RESOURCE: {
-			if (!animation->get_item_count()) {
+			if (!animation->has_selectable_items()) {
 				error_dialog->set_text(TTR("No animation to edit!"));
 				error_dialog->popup_centered();
 				return;
@@ -1300,7 +1174,7 @@ void AnimationPlayerEditor::shortcut_input(const Ref<InputEvent> &p_ev) {
 }
 
 void AnimationPlayerEditor::_editor_visibility_changed() {
-	if (is_visible() && animation->get_item_count() > 0) {
+	if (is_visible() && animation->has_selectable_items()) {
 		_start_onion_skinning();
 	}
 }
@@ -1536,7 +1410,6 @@ void AnimationPlayerEditor::_pin_pressed() {
 void AnimationPlayerEditor::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("_animation_new"), &AnimationPlayerEditor::_animation_new);
 	ClassDB::bind_method(D_METHOD("_animation_rename"), &AnimationPlayerEditor::_animation_rename);
-	ClassDB::bind_method(D_METHOD("_animation_load"), &AnimationPlayerEditor::_animation_load);
 	ClassDB::bind_method(D_METHOD("_animation_remove"), &AnimationPlayerEditor::_animation_remove);
 	ClassDB::bind_method(D_METHOD("_animation_blend"), &AnimationPlayerEditor::_animation_blend);
 	ClassDB::bind_method(D_METHOD("_animation_edit"), &AnimationPlayerEditor::_animation_edit);
@@ -1623,13 +1496,7 @@ AnimationPlayerEditor::AnimationPlayerEditor(AnimationPlayerEditorPlugin *p_plug
 	tool_anim->set_text(TTR("Animation"));
 	tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/new_animation", TTR("New")), TOOL_NEW_ANIM);
 	tool_anim->get_popup()->add_separator();
-	tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/open_animation", TTR("Load")), TOOL_LOAD_ANIM);
-	tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/save_animation", TTR("Save")), TOOL_SAVE_ANIM);
-	tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/save_as_animation", TTR("Save As...")), TOOL_SAVE_AS_ANIM);
-	tool_anim->get_popup()->add_separator();
-	tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/copy_animation", TTR("Copy")), TOOL_COPY_ANIM);
-	tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/paste_animation", TTR("Paste")), TOOL_PASTE_ANIM);
-	tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/paste_animation_as_reference", TTR("Paste As Reference")), TOOL_PASTE_ANIM_REF);
+	tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/animation_libraries", TTR("Manage Animations...")), TOOL_ANIM_LIBRARY);
 	tool_anim->get_popup()->add_separator();
 	tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/duplicate_animation", TTR("Duplicate...")), TOOL_DUPLICATE_ANIM);
 	tool_anim->get_popup()->add_separator();
@@ -1638,6 +1505,7 @@ AnimationPlayerEditor::AnimationPlayerEditor(AnimationPlayerEditorPlugin *p_plug
 	tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/open_animation_in_inspector", TTR("Open in Inspector")), TOOL_EDIT_RESOURCE);
 	tool_anim->get_popup()->add_separator();
 	tool_anim->get_popup()->add_shortcut(ED_SHORTCUT("animation_player_editor/remove_animation", TTR("Remove")), TOOL_REMOVE_ANIM);
+	tool_anim->set_disabled(true);
 	hb->add_child(tool_anim);
 
 	animation = memnew(OptionButton);
@@ -1705,8 +1573,14 @@ AnimationPlayerEditor::AnimationPlayerEditor(AnimationPlayerEditorPlugin *p_plug
 	name_title = memnew(Label(TTR("Animation Name:")));
 	vb->add_child(name_title);
 
+	HBoxContainer *name_hb = memnew(HBoxContainer);
 	name = memnew(LineEdit);
-	vb->add_child(name);
+	name_hb->add_child(name);
+	name->set_h_size_flags(SIZE_EXPAND_FILL);
+	library = memnew(OptionButton);
+	name_hb->add_child(library);
+	library->hide();
+	vb->add_child(name_hb);
 	name_dialog->register_text_enter(name);
 
 	error_dialog = memnew(ConfirmationDialog);
@@ -1742,8 +1616,6 @@ AnimationPlayerEditor::AnimationPlayerEditor(AnimationPlayerEditorPlugin *p_plug
 
 	animation->connect("item_selected", callable_mp(this, &AnimationPlayerEditor::_animation_selected));
 
-	file->connect("file_selected", callable_mp(this, &AnimationPlayerEditor::_save_animation));
-	file->connect("files_selected", callable_mp(this, &AnimationPlayerEditor::_load_animations));
 	frame->connect("value_changed", callable_mp(this, &AnimationPlayerEditor::_seek_value_changed), make_binds(true, false));
 	scale->connect("text_submitted", callable_mp(this, &AnimationPlayerEditor::_scale_changed));
 
@@ -1759,6 +1631,10 @@ AnimationPlayerEditor::AnimationPlayerEditor(AnimationPlayerEditorPlugin *p_plug
 
 	_update_player();
 
+	library_editor = memnew(AnimationLibraryEditor);
+	add_child(library_editor);
+	library_editor->connect("update_editor", callable_mp(this, &AnimationPlayerEditor::_animation_player_changed));
+
 	// Onion skinning.
 
 	track_editor->connect("visibility_changed", callable_mp(this, &AnimationPlayerEditor::_editor_visibility_changed));

+ 7 - 15
editor/plugins/animation_player_editor_plugin.h

@@ -33,6 +33,7 @@
 
 #include "editor/animation_track_editor.h"
 #include "editor/editor_plugin.h"
+#include "editor/plugins/animation_library_editor.h"
 #include "scene/animation/animation_player.h"
 #include "scene/gui/dialogs.h"
 #include "scene/gui/slider.h"
@@ -40,7 +41,6 @@
 #include "scene/gui/texture_button.h"
 #include "scene/gui/tree.h"
 
-class EditorFileDialog;
 class AnimationPlayerEditorPlugin;
 
 class AnimationPlayerEditor : public VBoxContainer {
@@ -51,16 +51,11 @@ class AnimationPlayerEditor : public VBoxContainer {
 
 	enum {
 		TOOL_NEW_ANIM,
-		TOOL_LOAD_ANIM,
-		TOOL_SAVE_ANIM,
-		TOOL_SAVE_AS_ANIM,
+		TOOL_ANIM_LIBRARY,
 		TOOL_DUPLICATE_ANIM,
 		TOOL_RENAME_ANIM,
 		TOOL_EDIT_TRANSITIONS,
 		TOOL_REMOVE_ANIM,
-		TOOL_COPY_ANIM,
-		TOOL_PASTE_ANIM,
-		TOOL_PASTE_ANIM_REF,
 		TOOL_EDIT_RESOURCE
 	};
 
@@ -103,8 +98,10 @@ class AnimationPlayerEditor : public VBoxContainer {
 	SpinBox *frame = nullptr;
 	LineEdit *scale = nullptr;
 	LineEdit *name = nullptr;
+	OptionButton *library = nullptr;
 	Label *name_title = nullptr;
 	UndoRedo *undo_redo = nullptr;
+
 	Ref<Texture2D> autoplay_icon;
 	Ref<Texture2D> reset_icon;
 	Ref<ImageTexture> autoplay_reset_icon;
@@ -114,6 +111,8 @@ class AnimationPlayerEditor : public VBoxContainer {
 	EditorFileDialog *file = nullptr;
 	ConfirmationDialog *delete_dialog = nullptr;
 
+	AnimationLibraryEditor *library_editor = nullptr;
+
 	struct BlendEditor {
 		AcceptDialog *dialog = nullptr;
 		Tree *tree = nullptr;
@@ -173,11 +172,6 @@ class AnimationPlayerEditor : public VBoxContainer {
 	void _animation_new();
 	void _animation_rename();
 	void _animation_name_edited();
-	void _animation_load();
-
-	void _animation_save_in_path(const Ref<Resource> &p_resource, const String &p_path);
-	void _animation_save(const Ref<Resource> &p_resource);
-	void _animation_save_as(const Ref<Resource> &p_resource);
 
 	void _animation_remove();
 	void _animation_remove_confirmed();
@@ -185,11 +179,8 @@ class AnimationPlayerEditor : public VBoxContainer {
 	void _animation_edit();
 	void _animation_duplicate();
 	Ref<Animation> _animation_clone(const Ref<Animation> p_anim);
-	void _animation_paste(const Ref<Animation> p_anim);
 	void _animation_resource_edit();
 	void _scale_changed(const String &p_scale);
-	void _save_animation(String p_file);
-	void _load_animations(Vector<String> p_files);
 	void _seek_value_changed(float p_value, bool p_set = false, bool p_timeline_only = false);
 	void _blend_editor_next_changed(const int p_idx);
 
@@ -219,6 +210,7 @@ class AnimationPlayerEditor : public VBoxContainer {
 	void _stop_onion_skinning();
 
 	void _pin_pressed();
+	String _get_current() const;
 
 	~AnimationPlayerEditor();
 

+ 8 - 1
modules/gltf/gltf_document.cpp

@@ -6095,7 +6095,14 @@ void GLTFDocument::_import_animation(Ref<GLTFState> state, AnimationPlayer *ap,
 
 	animation->set_length(length);
 
-	ap->add_animation(name, animation);
+	Ref<AnimationLibrary> library;
+	if (!ap->has_animation_library("")) {
+		library.instantiate();
+		ap->add_animation_library("", library);
+	} else {
+		library = ap->get_animation_library("");
+	}
+	library->add_animation(name, animation);
 }
 
 void GLTFDocument::_convert_mesh_instances(Ref<GLTFState> state) {

+ 309 - 57
scene/animation/animation_player.cpp

@@ -83,8 +83,31 @@ bool AnimationPlayer::_set(const StringName &p_name, const Variant &p_value) {
 		set_current_animation(p_value);
 
 	} else if (name.begins_with("anims/")) {
+		// Backwards compatibility with 3.x, add them to "default" library.
 		String which = name.get_slicec('/', 1);
-		add_animation(which, p_value);
+
+		Ref<Animation> anim = p_value;
+		Ref<AnimationLibrary> al;
+		if (!has_animation_library(StringName())) {
+			al.instantiate();
+			add_animation_library(StringName(), al);
+		} else {
+			al = get_animation_library(StringName());
+		}
+		al->add_animation(which, anim);
+
+	} else if (name.begins_with("libraries")) {
+		Dictionary d = p_value;
+		while (animation_libraries.size()) {
+			remove_animation_library(animation_libraries[0].name);
+		}
+		List<Variant> keys;
+		d.get_key_list(&keys);
+		for (const Variant &K : keys) {
+			StringName lib_name = K;
+			Ref<AnimationLibrary> lib = d[lib_name];
+			add_animation_library(lib_name, lib);
+		}
 
 	} else if (name.begins_with("next/")) {
 		String which = name.get_slicec('/', 1);
@@ -117,9 +140,13 @@ bool AnimationPlayer::_get(const StringName &p_name, Variant &r_ret) const {
 
 		r_ret = get_current_animation();
 
-	} else if (name.begins_with("anims/")) {
-		String which = name.get_slicec('/', 1);
-		r_ret = get_animation(which);
+	} else if (name.begins_with("libraries")) {
+		Dictionary d;
+		for (uint32_t i = 0; i < animation_libraries.size(); i++) {
+			d[animation_libraries[i].name] = animation_libraries[i].library;
+		}
+
+		r_ret = d;
 
 	} else if (name.begins_with("next/")) {
 		String which = name.get_slicec('/', 1);
@@ -173,8 +200,9 @@ void AnimationPlayer::_validate_property(PropertyInfo &property) const {
 void AnimationPlayer::_get_property_list(List<PropertyInfo> *p_list) const {
 	List<PropertyInfo> anim_names;
 
+	anim_names.push_back(PropertyInfo(Variant::DICTIONARY, "libraries"));
+
 	for (const KeyValue<StringName, AnimationData> &E : animation_set) {
-		anim_names.push_back(PropertyInfo(Variant::OBJECT, "anims/" + String(E.key), PROPERTY_HINT_RESOURCE_TYPE, "Animation", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL | PROPERTY_USAGE_DO_NOT_SHARE_ON_DUPLICATE));
 		if (E.value.next != StringName()) {
 			anim_names.push_back(PropertyInfo(Variant::STRING, "next/" + String(E.key), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
 		}
@@ -1155,71 +1183,106 @@ void AnimationPlayer::_animation_process(double p_delta) {
 	}
 }
 
-Error AnimationPlayer::add_animation(const StringName &p_name, const Ref<Animation> &p_animation) {
-#ifdef DEBUG_ENABLED
-	ERR_FAIL_COND_V_MSG(String(p_name).contains("/") || String(p_name).contains(":") || String(p_name).contains(",") || String(p_name).contains("["), ERR_INVALID_PARAMETER, "Invalid animation name: " + String(p_name) + ".");
-#endif
+void AnimationPlayer::_animation_set_cache_update() {
+	// Relatively fast function to update all animations.
 
-	ERR_FAIL_COND_V(p_animation.is_null(), ERR_INVALID_PARAMETER);
+	animation_set_update_pass++;
+	bool clear_cache_needed = false;
 
-	if (animation_set.has(p_name)) {
-		_unref_anim(animation_set[p_name].animation);
-		animation_set[p_name].animation = p_animation;
-		clear_caches();
-	} else {
-		AnimationData ad;
-		ad.animation = p_animation;
-		ad.name = p_name;
-		animation_set[p_name] = ad;
-	}
+	// Update changed and add otherwise
+	for (uint32_t i = 0; i < animation_libraries.size(); i++) {
+		for (const KeyValue<StringName, Ref<Animation>> &K : animation_libraries[i].library->animations) {
+			StringName key = animation_libraries[i].name == StringName() ? K.key : StringName(String(animation_libraries[i].name) + "/" + String(K.key));
+			if (!animation_set.has(key)) {
+				AnimationData ad;
+				ad.animation = K.value;
+				ad.animation_library = animation_libraries[i].name;
+				ad.name = key;
+				ad.last_update = animation_set_update_pass;
+				animation_set.insert(ad.name, ad);
+			} else {
+				AnimationData &ad = animation_set[key];
+				if (ad.last_update != animation_set_update_pass) {
+					// Was not updated, update. If the animation is duplicated, the second one will be ignored.
+					if (ad.animation != K.value || ad.animation_library != animation_libraries[i].name) {
+						// Animation changed, update and clear caches.
+						clear_cache_needed = true;
+						ad.animation = K.value;
+						ad.animation_library = animation_libraries[i].name;
+					}
 
-	_ref_anim(p_animation);
-	notify_property_list_changed();
-	return OK;
-}
+					ad.last_update = animation_set_update_pass;
+				}
+			}
+		}
+	}
 
-void AnimationPlayer::remove_animation(const StringName &p_name) {
-	ERR_FAIL_COND(!animation_set.has(p_name));
+	// Check removed
+	List<StringName> to_erase;
+	for (const KeyValue<StringName, AnimationData> &E : animation_set) {
+		if (E.value.last_update != animation_set_update_pass) {
+			// Was not updated, must be erased
+			to_erase.push_back(E.key);
+			clear_cache_needed = true;
+		}
+	}
 
-	stop();
-	_unref_anim(animation_set[p_name].animation);
-	animation_set.erase(p_name);
+	while (to_erase.size()) {
+		animation_set.erase(to_erase.front()->get());
+		to_erase.pop_front();
+	}
 
-	clear_caches();
-	notify_property_list_changed();
+	if (clear_cache_needed) {
+		// If something was modified or removed, caches need to be cleared
+		clear_caches();
+	}
 }
 
-void AnimationPlayer::_ref_anim(const Ref<Animation> &p_anim) {
-	Ref<Animation>(p_anim)->connect(SceneStringNames::get_singleton()->tracks_changed, callable_mp(this, &AnimationPlayer::_animation_changed), varray(), CONNECT_REFERENCE_COUNTED);
-}
+void AnimationPlayer::_animation_added(const StringName &p_name, const StringName &p_library) {
+	_animation_set_cache_update();
 
-void AnimationPlayer::_unref_anim(const Ref<Animation> &p_anim) {
-	Ref<Animation>(p_anim)->disconnect(SceneStringNames::get_singleton()->tracks_changed, callable_mp(this, &AnimationPlayer::_animation_changed));
+	update_configuration_warnings();
 }
 
-void AnimationPlayer::rename_animation(const StringName &p_name, const StringName &p_new_name) {
-	ERR_FAIL_COND(!animation_set.has(p_name));
-	ERR_FAIL_COND(String(p_new_name).contains("/") || String(p_new_name).contains(":"));
-	ERR_FAIL_COND(animation_set.has(p_new_name));
+void AnimationPlayer::_animation_removed(const StringName &p_name, const StringName &p_library) {
+	StringName name = p_library == StringName() ? p_name : StringName(String(p_library) + "/" + String(p_name));
 
-	stop();
-	AnimationData ad = animation_set[p_name];
-	ad.name = p_new_name;
-	animation_set.erase(p_name);
-	animation_set[p_new_name] = ad;
+	if (!animation_set.has(name)) {
+		return; // No need to update because not the one from the library being used.
+	}
+	_animation_set_cache_update();
+
+	// Erase blends if needed
+	List<BlendKey> to_erase;
+	for (const KeyValue<BlendKey, float> &E : blend_times) {
+		BlendKey bk = E.key;
+		if (bk.from == name || bk.to == name) {
+			to_erase.push_back(bk);
+		}
+	}
+
+	while (to_erase.size()) {
+		blend_times.erase(to_erase.front()->get());
+		to_erase.pop_front();
+	}
+
+	update_configuration_warnings();
+}
 
+void AnimationPlayer::_rename_animation(const StringName &p_from_name, const StringName &p_to_name) {
+	// Rename autoplay or blends if needed.
 	List<BlendKey> to_erase;
 	Map<BlendKey, float> to_insert;
 	for (const KeyValue<BlendKey, float> &E : blend_times) {
 		BlendKey bk = E.key;
 		BlendKey new_bk = bk;
 		bool erase = false;
-		if (bk.from == p_name) {
-			new_bk.from = p_new_name;
+		if (bk.from == p_from_name) {
+			new_bk.from = p_to_name;
 			erase = true;
 		}
-		if (bk.to == p_name) {
-			new_bk.to = p_new_name;
+		if (bk.to == p_from_name) {
+			new_bk.to = p_to_name;
 			erase = true;
 		}
 
@@ -1239,12 +1302,184 @@ void AnimationPlayer::rename_animation(const StringName &p_name, const StringNam
 		to_insert.erase(to_insert.front());
 	}
 
-	if (autoplay == p_name) {
-		autoplay = p_new_name;
+	if (autoplay == p_from_name) {
+		autoplay = p_to_name;
 	}
+}
+
+void AnimationPlayer::_animation_renamed(const StringName &p_name, const StringName &p_to_name, const StringName &p_library) {
+	StringName from_name = p_library == StringName() ? p_name : StringName(String(p_library) + "/" + String(p_name));
+	StringName to_name = p_library == StringName() ? p_to_name : StringName(String(p_library) + "/" + String(p_to_name));
+
+	if (!animation_set.has(from_name)) {
+		return; // No need to update because not the one from the library being used.
+	}
+	_animation_set_cache_update();
+
+	_rename_animation(from_name, to_name);
+	update_configuration_warnings();
+}
+
+Error AnimationPlayer::add_animation_library(const StringName &p_name, const Ref<AnimationLibrary> &p_animation_library) {
+	ERR_FAIL_COND_V(p_animation_library.is_null(), ERR_INVALID_PARAMETER);
+#ifdef DEBUG_ENABLED
+	ERR_FAIL_COND_V_MSG(String(p_name).contains("/") || String(p_name).contains(":") || String(p_name).contains(",") || String(p_name).contains("["), ERR_INVALID_PARAMETER, "Invalid animation name: " + String(p_name) + ".");
+#endif
+
+	int insert_pos = 0;
+
+	for (uint32_t i = 0; i < animation_libraries.size(); i++) {
+		ERR_FAIL_COND_V_MSG(animation_libraries[i].name == p_name, ERR_ALREADY_EXISTS, "Can't add animation library twice with name: " + String(p_name));
+		ERR_FAIL_COND_V_MSG(animation_libraries[i].library == p_animation_library, ERR_ALREADY_EXISTS, "Can't add animation library twice (adding as '" + p_name.operator String() + "', exists as '" + animation_libraries[i].name.operator String() + "'.");
+
+		if (animation_libraries[i].name.operator String() >= p_name.operator String()) {
+			break;
+		}
+
+		insert_pos++;
+	}
+
+	AnimationLibraryData ald;
+	ald.name = p_name;
+	ald.library = p_animation_library;
+
+	animation_libraries.insert(insert_pos, ald);
+
+	ald.library->connect(SNAME("animation_added"), callable_mp(this, &AnimationPlayer::_animation_added), varray(p_name));
+	ald.library->connect(SNAME("animation_removed"), callable_mp(this, &AnimationPlayer::_animation_added), varray(p_name));
+	ald.library->connect(SNAME("animation_renamed"), callable_mp(this, &AnimationPlayer::_animation_renamed), varray(p_name));
+
+	_animation_set_cache_update();
+
+	notify_property_list_changed();
+
+	update_configuration_warnings();
+	return OK;
+}
+
+void AnimationPlayer::remove_animation_library(const StringName &p_name) {
+	int at_pos = -1;
+
+	for (uint32_t i = 0; i < animation_libraries.size(); i++) {
+		if (animation_libraries[i].name == p_name) {
+			at_pos = i;
+			break;
+		}
+	}
+
+	ERR_FAIL_COND(at_pos == -1);
+
+	animation_libraries[at_pos].library->disconnect(SNAME("animation_added"), callable_mp(this, &AnimationPlayer::_animation_added));
+	animation_libraries[at_pos].library->disconnect(SNAME("animation_removed"), callable_mp(this, &AnimationPlayer::_animation_added));
+	animation_libraries[at_pos].library->disconnect(SNAME("animation_renamed"), callable_mp(this, &AnimationPlayer::_animation_renamed));
+
+	stop();
+
+	for (const KeyValue<StringName, Ref<Animation>> &K : animation_libraries[at_pos].library->animations) {
+		_unref_anim(K.value);
+	}
+
+	animation_libraries.remove_at(at_pos);
+	_animation_set_cache_update();
 
-	clear_caches();
 	notify_property_list_changed();
+	update_configuration_warnings();
+}
+
+void AnimationPlayer::_ref_anim(const Ref<Animation> &p_anim) {
+	Ref<Animation>(p_anim)->connect(SceneStringNames::get_singleton()->tracks_changed, callable_mp(this, &AnimationPlayer::_animation_changed), varray(), CONNECT_REFERENCE_COUNTED);
+}
+
+void AnimationPlayer::_unref_anim(const Ref<Animation> &p_anim) {
+	Ref<Animation>(p_anim)->disconnect(SceneStringNames::get_singleton()->tracks_changed, callable_mp(this, &AnimationPlayer::_animation_changed));
+}
+
+void AnimationPlayer::rename_animation_library(const StringName &p_name, const StringName &p_new_name) {
+	if (p_name == p_new_name) {
+		return;
+	}
+#ifdef DEBUG_ENABLED
+	ERR_FAIL_COND_MSG(String(p_new_name).contains("/") || String(p_new_name).contains(":") || String(p_new_name).contains(",") || String(p_new_name).contains("["), "Invalid animation library name: " + String(p_new_name) + ".");
+#endif
+
+	bool found = false;
+	for (uint32_t i = 0; i < animation_libraries.size(); i++) {
+		ERR_FAIL_COND_MSG(animation_libraries[i].name == p_new_name, "Can't rename animation library to another existing name: " + String(p_new_name));
+		if (animation_libraries[i].name == p_name) {
+			found = true;
+			animation_libraries[i].name = p_new_name;
+			// rename connections
+			animation_libraries[i].library->disconnect(SNAME("animation_added"), callable_mp(this, &AnimationPlayer::_animation_added));
+			animation_libraries[i].library->disconnect(SNAME("animation_removed"), callable_mp(this, &AnimationPlayer::_animation_added));
+			animation_libraries[i].library->disconnect(SNAME("animation_renamed"), callable_mp(this, &AnimationPlayer::_animation_renamed));
+
+			animation_libraries[i].library->connect(SNAME("animation_added"), callable_mp(this, &AnimationPlayer::_animation_added), varray(p_new_name));
+			animation_libraries[i].library->connect(SNAME("animation_removed"), callable_mp(this, &AnimationPlayer::_animation_added), varray(p_new_name));
+			animation_libraries[i].library->connect(SNAME("animation_renamed"), callable_mp(this, &AnimationPlayer::_animation_renamed), varray(p_new_name));
+
+			for (const KeyValue<StringName, Ref<Animation>> &K : animation_libraries[i].library->animations) {
+				StringName old_name = p_name == StringName() ? K.key : StringName(String(p_name) + "/" + String(K.key));
+				StringName new_name = p_new_name == StringName() ? K.key : StringName(String(p_new_name) + "/" + String(K.key));
+				_rename_animation(old_name, new_name);
+			}
+		}
+	}
+
+	ERR_FAIL_COND(!found);
+
+	stop();
+
+	animation_libraries.sort(); // Must keep alphabetical order.
+
+	_animation_set_cache_update(); // Update cache.
+
+	notify_property_list_changed();
+}
+
+bool AnimationPlayer::has_animation_library(const StringName &p_name) const {
+	for (uint32_t i = 0; i < animation_libraries.size(); i++) {
+		if (animation_libraries[i].name == p_name) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+Ref<AnimationLibrary> AnimationPlayer::get_animation_library(const StringName &p_name) const {
+	for (uint32_t i = 0; i < animation_libraries.size(); i++) {
+		if (animation_libraries[i].name == p_name) {
+			return animation_libraries[i].library;
+		}
+	}
+	ERR_FAIL_V(Ref<AnimationLibrary>());
+}
+
+TypedArray<StringName> AnimationPlayer::_get_animation_library_list() const {
+	TypedArray<StringName> ret;
+	for (uint32_t i = 0; i < animation_libraries.size(); i++) {
+		ret.push_back(animation_libraries[i].name);
+	}
+	return ret;
+}
+
+void AnimationPlayer::get_animation_library_list(List<StringName> *p_libraries) const {
+	for (uint32_t i = 0; i < animation_libraries.size(); i++) {
+		p_libraries->push_back(animation_libraries[i].name);
+	}
+}
+
+TypedArray<String> AnimationPlayer::get_configuration_warnings() const {
+	TypedArray<String> warnings = Node::get_configuration_warnings();
+
+	for (uint32_t i = 0; i < animation_libraries.size(); i++) {
+		for (const KeyValue<StringName, Ref<Animation>> &K : animation_libraries[i].library->animations) {
+			if (animation_set.has(K.key) && animation_set[K.key].animation_library != animation_libraries[i].name) {
+				warnings.push_back(vformat(RTR("Animation '%s' in library '%s' is unused because another animation with the same name exists in library '%s'."), K.key, animation_libraries[i].name, animation_set[K.key].animation_library));
+			}
+		}
+	}
+	return warnings;
 }
 
 bool AnimationPlayer::has_animation(const StringName &p_name) const {
@@ -1585,7 +1820,16 @@ StringName AnimationPlayer::find_animation(const Ref<Animation> &p_animation) co
 		}
 	}
 
-	return "";
+	return StringName();
+}
+
+StringName AnimationPlayer::find_animation_library(const Ref<Animation> &p_animation) const {
+	for (const KeyValue<StringName, AnimationData> &E : animation_set) {
+		if (E.value.animation == p_animation) {
+			return E.value.animation_library;
+		}
+	}
+	return StringName();
 }
 
 void AnimationPlayer::set_autoplay(const String &p_name) {
@@ -1764,7 +2008,10 @@ Ref<AnimatedValuesBackup> AnimationPlayer::apply_reset(bool p_user_initiated) {
 
 	AnimationPlayer *aux_player = memnew(AnimationPlayer);
 	EditorNode::get_singleton()->add_child(aux_player);
-	aux_player->add_animation(SceneStringNames::get_singleton()->RESET, reset_anim);
+	Ref<AnimationLibrary> al;
+	al.instantiate();
+	al->add_animation(SceneStringNames::get_singleton()->RESET, reset_anim);
+	aux_player->add_animation_library("default", al);
 	aux_player->set_assigned_animation(SceneStringNames::get_singleton()->RESET);
 	// Forcing the use of the original root because the scene where original player belongs may be not the active one
 	Node *root = get_node(get_root());
@@ -1792,9 +2039,13 @@ bool AnimationPlayer::can_apply_reset() const {
 #endif // TOOLS_ENABLED
 
 void AnimationPlayer::_bind_methods() {
-	ClassDB::bind_method(D_METHOD("add_animation", "name", "animation"), &AnimationPlayer::add_animation);
-	ClassDB::bind_method(D_METHOD("remove_animation", "name"), &AnimationPlayer::remove_animation);
-	ClassDB::bind_method(D_METHOD("rename_animation", "name", "newname"), &AnimationPlayer::rename_animation);
+	ClassDB::bind_method(D_METHOD("add_animation_library", "name", "library"), &AnimationPlayer::add_animation_library);
+	ClassDB::bind_method(D_METHOD("remove_animation_library", "name"), &AnimationPlayer::remove_animation_library);
+	ClassDB::bind_method(D_METHOD("rename_animation_library", "name", "newname"), &AnimationPlayer::rename_animation_library);
+	ClassDB::bind_method(D_METHOD("has_animation_library", "name"), &AnimationPlayer::has_animation_library);
+	ClassDB::bind_method(D_METHOD("get_animation_library", "name"), &AnimationPlayer::get_animation_library);
+	ClassDB::bind_method(D_METHOD("get_animation_library_list"), &AnimationPlayer::_get_animation_library_list);
+
 	ClassDB::bind_method(D_METHOD("has_animation", "name"), &AnimationPlayer::has_animation);
 	ClassDB::bind_method(D_METHOD("get_animation", "name"), &AnimationPlayer::get_animation);
 	ClassDB::bind_method(D_METHOD("get_animation_list"), &AnimationPlayer::_get_animation_list);
@@ -1838,6 +2089,7 @@ void AnimationPlayer::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_root"), &AnimationPlayer::get_root);
 
 	ClassDB::bind_method(D_METHOD("find_animation", "animation"), &AnimationPlayer::find_animation);
+	ClassDB::bind_method(D_METHOD("find_animation_library", "animation"), &AnimationPlayer::find_animation_library);
 
 	ClassDB::bind_method(D_METHOD("clear_caches"), &AnimationPlayer::clear_caches);
 

+ 32 - 4
scene/animation/animation_player.h

@@ -36,6 +36,7 @@
 #include "scene/3d/node_3d.h"
 #include "scene/3d/skeleton_3d.h"
 #include "scene/resources/animation.h"
+#include "scene/resources/animation_library.h"
 
 #ifdef TOOLS_ENABLED
 class AnimatedValuesBackup : public RefCounted {
@@ -184,9 +185,20 @@ private:
 		StringName next;
 		Vector<TrackNodeCache *> node_cache;
 		Ref<Animation> animation;
+		StringName animation_library;
+		uint64_t last_update = 0;
 	};
 
 	Map<StringName, AnimationData> animation_set;
+
+	struct AnimationLibraryData {
+		StringName name;
+		Ref<AnimationLibrary> library;
+		bool operator<(const AnimationLibraryData &p_data) const { return name.operator String() < p_data.name.operator String(); }
+	};
+
+	LocalVector<AnimationLibraryData> animation_libraries;
+
 	struct BlendKey {
 		StringName from;
 		StringName to;
@@ -261,6 +273,15 @@ private:
 
 	bool playing = false;
 
+	uint64_t animation_set_update_pass = 1;
+	void _animation_set_cache_update();
+	void _animation_added(const StringName &p_name, const StringName &p_library);
+	void _animation_removed(const StringName &p_name, const StringName &p_library);
+	void _animation_renamed(const StringName &p_name, const StringName &p_to_name, const StringName &p_library);
+	void _rename_animation(const StringName &p_from_name, const StringName &p_to_name);
+
+	TypedArray<StringName> _get_animation_library_list() const;
+
 protected:
 	bool _set(const StringName &p_name, const Variant &p_value);
 	bool _get(const StringName &p_name, Variant &r_ret) const;
@@ -272,13 +293,18 @@ protected:
 
 public:
 	StringName find_animation(const Ref<Animation> &p_animation) const;
+	StringName find_animation_library(const Ref<Animation> &p_animation) const;
+
+	Error add_animation_library(const StringName &p_name, const Ref<AnimationLibrary> &p_animation_library);
+	void remove_animation_library(const StringName &p_name);
+	void rename_animation_library(const StringName &p_name, const StringName &p_new_name);
+	Ref<AnimationLibrary> get_animation_library(const StringName &p_name) const;
+	void get_animation_library_list(List<StringName> *p_animations) const;
+	bool has_animation_library(const StringName &p_name) const;
 
-	Error add_animation(const StringName &p_name, const Ref<Animation> &p_animation);
-	void remove_animation(const StringName &p_name);
-	void rename_animation(const StringName &p_name, const StringName &p_new_name);
-	bool has_animation(const StringName &p_name) const;
 	Ref<Animation> get_animation(const StringName &p_name) const;
 	void get_animation_list(List<StringName> *p_animations) const;
+	bool has_animation(const StringName &p_name) const;
 
 	void set_blend_time(const StringName &p_animation1, const StringName &p_animation2, float p_time);
 	float get_blend_time(const StringName &p_animation1, const StringName &p_animation2) const;
@@ -340,6 +366,8 @@ public:
 	bool can_apply_reset() const;
 #endif
 
+	TypedArray<String> get_configuration_warnings() const override;
+
 	AnimationPlayer();
 	~AnimationPlayer();
 };

+ 40 - 7
scene/gui/option_button.cpp

@@ -203,16 +203,18 @@ void OptionButton::pressed() {
 }
 
 void OptionButton::add_icon_item(const Ref<Texture2D> &p_icon, const String &p_label, int p_id) {
+	bool first_selectable = !has_selectable_items();
 	popup->add_icon_radio_check_item(p_icon, p_label, p_id);
-	if (popup->get_item_count() == 1) {
-		select(0);
+	if (first_selectable) {
+		select(get_item_count() - 1);
 	}
 }
 
 void OptionButton::add_item(const String &p_label, int p_id) {
+	bool first_selectable = !has_selectable_items();
 	popup->add_radio_check_item(p_label, p_id);
-	if (popup->get_item_count() == 1) {
-		select(0);
+	if (first_selectable) {
+		select(get_item_count() - 1);
 	}
 }
 
@@ -280,6 +282,9 @@ bool OptionButton::is_item_disabled(int p_idx) const {
 	return popup->is_item_disabled(p_idx);
 }
 
+bool OptionButton::is_item_separator(int p_idx) const {
+	return popup->is_item_separator(p_idx);
+}
 void OptionButton::set_item_count(int p_count) {
 	ERR_FAIL_COND(p_count < 0);
 
@@ -299,12 +304,37 @@ void OptionButton::set_item_count(int p_count) {
 	notify_property_list_changed();
 }
 
+bool OptionButton::has_selectable_items() const {
+	for (int i = 0; i < get_item_count(); i++) {
+		if (!is_item_disabled(i) && !is_item_separator(i)) {
+			return true;
+		}
+	}
+	return false;
+}
+int OptionButton::get_selectable_item(bool p_from_last) const {
+	if (!p_from_last) {
+		for (int i = 0; i < get_item_count(); i++) {
+			if (!is_item_disabled(i) && !is_item_separator(i)) {
+				return i;
+			}
+		}
+	} else {
+		for (int i = get_item_count() - 1; i >= 0; i++) {
+			if (!is_item_disabled(i) && !is_item_separator(i)) {
+				return i;
+			}
+		}
+	}
+	return -1;
+}
+
 int OptionButton::get_item_count() const {
 	return popup->get_item_count();
 }
 
-void OptionButton::add_separator() {
-	popup->add_separator();
+void OptionButton::add_separator(const String &p_text) {
+	popup->add_separator(p_text);
 }
 
 void OptionButton::clear() {
@@ -407,7 +437,8 @@ void OptionButton::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_item_metadata", "idx"), &OptionButton::get_item_metadata);
 	ClassDB::bind_method(D_METHOD("get_item_tooltip", "idx"), &OptionButton::get_item_tooltip);
 	ClassDB::bind_method(D_METHOD("is_item_disabled", "idx"), &OptionButton::is_item_disabled);
-	ClassDB::bind_method(D_METHOD("add_separator"), &OptionButton::add_separator);
+	ClassDB::bind_method(D_METHOD("is_item_separator", "idx"), &OptionButton::is_item_separator);
+	ClassDB::bind_method(D_METHOD("add_separator", "text"), &OptionButton::add_separator, DEFVAL(String()));
 	ClassDB::bind_method(D_METHOD("clear"), &OptionButton::clear);
 	ClassDB::bind_method(D_METHOD("select", "idx"), &OptionButton::select);
 	ClassDB::bind_method(D_METHOD("get_selected"), &OptionButton::get_selected);
@@ -420,6 +451,8 @@ void OptionButton::_bind_methods() {
 
 	ClassDB::bind_method(D_METHOD("set_item_count", "count"), &OptionButton::set_item_count);
 	ClassDB::bind_method(D_METHOD("get_item_count"), &OptionButton::get_item_count);
+	ClassDB::bind_method(D_METHOD("has_selectable_items"), &OptionButton::has_selectable_items);
+	ClassDB::bind_method(D_METHOD("get_selectable_item", "from_last"), &OptionButton::get_selectable_item, DEFVAL(false));
 
 	// "selected" property must come after "item_count", otherwise GH-10213 occurs.
 	ADD_ARRAY_COUNT("Items", "item_count", "set_item_count", "get_item_count", "popup/item_");

+ 5 - 1
scene/gui/option_button.h

@@ -77,12 +77,16 @@ public:
 	int get_item_index(int p_id) const;
 	Variant get_item_metadata(int p_idx) const;
 	bool is_item_disabled(int p_idx) const;
+	bool is_item_separator(int p_idx) const;
 	String get_item_tooltip(int p_idx) const;
 
+	bool has_selectable_items() const;
+	int get_selectable_item(bool p_from_last = false) const;
+
 	void set_item_count(int p_count);
 	int get_item_count() const;
 
-	void add_separator();
+	void add_separator(const String &p_text = "");
 
 	void clear();
 

+ 31 - 6
scene/gui/tree.cpp

@@ -101,6 +101,7 @@ void TreeItem::_change_tree(Tree *p_tree) {
 
 		if (tree->popup_edited_item == this) {
 			tree->popup_edited_item = nullptr;
+			tree->popup_pressing_edited_item = nullptr;
 			tree->pressing_for_editor = false;
 		}
 
@@ -2670,8 +2671,8 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int
 		}
 
 		click_handled = true;
-		popup_edited_item = p_item;
-		popup_edited_item_col = col;
+		popup_pressing_edited_item = p_item;
+		popup_pressing_edited_item_column = col;
 
 		pressing_item_rect = Rect2(get_global_position() + Point2i(col_ofs, _get_title_button_height() + y_ofs) - cache.offset, Size2(col_width, item_h));
 		pressing_for_editor_text = editor_text;
@@ -3206,10 +3207,16 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) {
 			update();
 		}
 
-		if (pressing_for_editor && popup_edited_item && (popup_edited_item->get_cell_mode(popup_edited_item_col) == TreeItem::CELL_MODE_RANGE)) {
-			//range drag
+		if (pressing_for_editor && popup_pressing_edited_item && (popup_pressing_edited_item->get_cell_mode(popup_pressing_edited_item_column) == TreeItem::CELL_MODE_RANGE)) {
+			/* This needs to happen now, because the popup can be closed when pressing another item, and must remain the popup edited item until it actually closes */
+			popup_edited_item = popup_pressing_edited_item;
+			popup_edited_item_col = popup_pressing_edited_item_column;
+
+			popup_pressing_edited_item = nullptr;
+			popup_pressing_edited_item_column = -1;
 
 			if (!range_drag_enabled) {
+				//range drag
 				Vector2 cpos = mm->get_position();
 				if (rtl) {
 					cpos.x = get_size().width - cpos.x;
@@ -3994,6 +4001,7 @@ void Tree::clear() {
 	selected_item = nullptr;
 	edited_item = nullptr;
 	popup_edited_item = nullptr;
+	popup_pressing_edited_item = nullptr;
 
 	update();
 };
@@ -4309,12 +4317,16 @@ int Tree::get_pressed_button() const {
 	return pressed_button;
 }
 
-Rect2 Tree::get_item_rect(TreeItem *p_item, int p_column) const {
+Rect2 Tree::get_item_rect(TreeItem *p_item, int p_column, int p_button) const {
 	ERR_FAIL_NULL_V(p_item, Rect2());
 	ERR_FAIL_COND_V(p_item->tree != this, Rect2());
 	if (p_column != -1) {
 		ERR_FAIL_INDEX_V(p_column, columns.size(), Rect2());
 	}
+	if (p_button != -1) {
+		ERR_FAIL_COND_V(p_column == -1, Rect2()); // pass a column if you want to pass a button
+		ERR_FAIL_INDEX_V(p_button, p_item->cells[p_column].buttons.size(), Rect2());
+	}
 
 	int ofs = get_item_offset(p_item);
 	int height = compute_item_height(p_item);
@@ -4332,6 +4344,19 @@ Rect2 Tree::get_item_rect(TreeItem *p_item, int p_column) const {
 		}
 		r.position.x = accum;
 		r.size.x = get_column_width(p_column);
+		if (p_button != -1) {
+			const TreeItem::Cell &c = p_item->cells[p_column];
+			Vector2 ofst = Vector2(r.position.x + r.size.x, r.position.y);
+			for (int j = c.buttons.size() - 1; j >= 0; j--) {
+				Ref<Texture2D> b = c.buttons[j].texture;
+				Size2 size = b->get_size() + cache.button_pressed->get_minimum_size();
+				ofst.x -= size.x;
+
+				if (j == p_button) {
+					return Rect2(ofst, size);
+				}
+			}
+		}
 	}
 
 	return r;
@@ -4870,7 +4895,7 @@ void Tree::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_edited_column"), &Tree::get_edited_column);
 	ClassDB::bind_method(D_METHOD("edit_selected"), &Tree::edit_selected);
 	ClassDB::bind_method(D_METHOD("get_custom_popup_rect"), &Tree::get_custom_popup_rect);
-	ClassDB::bind_method(D_METHOD("get_item_area_rect", "item", "column"), &Tree::get_item_rect, DEFVAL(-1));
+	ClassDB::bind_method(D_METHOD("get_item_area_rect", "item", "column", "button_index"), &Tree::get_item_rect, DEFVAL(-1), DEFVAL(-1));
 	ClassDB::bind_method(D_METHOD("get_item_at_position", "position"), &Tree::get_item_at_position);
 	ClassDB::bind_method(D_METHOD("get_column_at_position", "position"), &Tree::get_column_at_position);
 	ClassDB::bind_method(D_METHOD("get_drop_section_at_position", "position"), &Tree::get_drop_section_at_position);

+ 4 - 1
scene/gui/tree.h

@@ -379,6 +379,9 @@ private:
 	TreeItem *selected_item = nullptr;
 	TreeItem *edited_item = nullptr;
 
+	TreeItem *popup_pressing_edited_item = nullptr; // Candidate.
+	int popup_pressing_edited_item_column = -1;
+
 	TreeItem *drop_mode_over = nullptr;
 	int drop_mode_section = 0;
 
@@ -673,7 +676,7 @@ public:
 	Rect2 get_custom_popup_rect() const;
 
 	int get_item_offset(TreeItem *p_item) const;
-	Rect2 get_item_rect(TreeItem *p_item, int p_column = -1) const;
+	Rect2 get_item_rect(TreeItem *p_item, int p_column = -1, int p_button = -1) const;
 	bool edit_selected();
 	bool is_editing();
 

+ 2 - 0
scene/register_scene_types.cpp

@@ -139,6 +139,7 @@
 #include "scene/multiplayer/scene_cache_interface.h"
 #include "scene/multiplayer/scene_replication_interface.h"
 #include "scene/multiplayer/scene_rpc_interface.h"
+#include "scene/resources/animation_library.h"
 #include "scene/resources/audio_stream_sample.h"
 #include "scene/resources/bit_map.h"
 #include "scene/resources/box_shape_3d.h"
@@ -834,6 +835,7 @@ void register_scene_types() {
 	GDREGISTER_CLASS(CompressedTexture2DArray);
 
 	GDREGISTER_CLASS(Animation);
+	GDREGISTER_CLASS(AnimationLibrary);
 	GDREGISTER_CLASS(FontData);
 	GDREGISTER_CLASS(Font);
 	GDREGISTER_CLASS(Curve);

+ 134 - 0
scene/resources/animation_library.cpp

@@ -0,0 +1,134 @@
+/*************************************************************************/
+/*  animation_library.cpp                                                */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* 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_library.h"
+
+Error AnimationLibrary::add_animation(const StringName &p_name, const Ref<Animation> &p_animation) {
+	ERR_FAIL_COND_V_MSG(String(p_name).contains("/") || String(p_name).contains(":") || String(p_name).contains(",") || String(p_name).contains("["), ERR_INVALID_PARAMETER, "Invalid animation name: " + String(p_name) + ".");
+	ERR_FAIL_COND_V(p_animation.is_null(), ERR_INVALID_PARAMETER);
+
+	if (animations.has(p_name)) {
+		animations.erase(p_name);
+		emit_signal(SNAME("animation_removed"), p_name);
+	}
+
+	animations.insert(p_name, p_animation);
+	emit_signal(SNAME("animation_added"), p_name);
+	notify_property_list_changed();
+	return OK;
+}
+
+void AnimationLibrary::remove_animation(const StringName &p_name) {
+	ERR_FAIL_COND(!animations.has(p_name));
+
+	animations.erase(p_name);
+	emit_signal(SNAME("animation_removed"), p_name);
+	notify_property_list_changed();
+}
+
+void AnimationLibrary::rename_animation(const StringName &p_name, const StringName &p_new_name) {
+	ERR_FAIL_COND(!animations.has(p_name));
+	ERR_FAIL_COND_MSG(String(p_name).contains("/") || String(p_name).contains(":") || String(p_name).contains(",") || String(p_name).contains("["), "Invalid animation name: " + String(p_name) + ".");
+	ERR_FAIL_COND(animations.has(p_new_name));
+
+	animations.insert(p_new_name, animations[p_name]);
+	animations.erase(p_name);
+	emit_signal(SNAME("animation_renamed"), p_name, p_new_name);
+}
+
+bool AnimationLibrary::has_animation(const StringName &p_name) const {
+	return animations.has(p_name);
+}
+
+Ref<Animation> AnimationLibrary::get_animation(const StringName &p_name) const {
+	ERR_FAIL_COND_V(!animations.has(p_name), Ref<Animation>());
+
+	return animations[p_name];
+}
+
+TypedArray<StringName> AnimationLibrary::_get_animation_list() const {
+	TypedArray<StringName> ret;
+	List<StringName> names;
+	get_animation_list(&names);
+	for (const StringName &K : names) {
+		ret.push_back(K);
+	}
+	return ret;
+}
+
+void AnimationLibrary::get_animation_list(List<StringName> *p_animations) const {
+	List<StringName> anims;
+
+	for (const KeyValue<StringName, Ref<Animation>> &E : animations) {
+		anims.push_back(E.key);
+	}
+
+	anims.sort_custom<StringName::AlphCompare>();
+
+	for (const StringName &E : anims) {
+		p_animations->push_back(E);
+	}
+}
+
+void AnimationLibrary::_set_data(const Dictionary &p_data) {
+	animations.clear();
+	List<Variant> keys;
+	p_data.get_key_list(&keys);
+	for (const Variant &K : keys) {
+		add_animation(K, p_data[K]);
+	}
+}
+
+Dictionary AnimationLibrary::_get_data() const {
+	Dictionary ret;
+	for (const KeyValue<StringName, Ref<Animation>> &K : animations) {
+		ret[K.key] = K.value;
+	}
+	return ret;
+}
+
+void AnimationLibrary::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("add_animation", "name", "animation"), &AnimationLibrary::add_animation);
+	ClassDB::bind_method(D_METHOD("remove_animation", "name"), &AnimationLibrary::remove_animation);
+	ClassDB::bind_method(D_METHOD("rename_animation", "name", "newname"), &AnimationLibrary::rename_animation);
+	ClassDB::bind_method(D_METHOD("has_animation", "name"), &AnimationLibrary::has_animation);
+	ClassDB::bind_method(D_METHOD("get_animation", "name"), &AnimationLibrary::get_animation);
+	ClassDB::bind_method(D_METHOD("get_animation_list"), &AnimationLibrary::_get_animation_list);
+
+	ClassDB::bind_method(D_METHOD("_set_data", "data"), &AnimationLibrary::_set_data);
+	ClassDB::bind_method(D_METHOD("_get_data"), &AnimationLibrary::_get_data);
+
+	ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "_data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "_set_data", "_get_data");
+	ADD_SIGNAL(MethodInfo("animation_added", PropertyInfo(Variant::OBJECT, "name", PROPERTY_HINT_RESOURCE_TYPE, "Animation")));
+	ADD_SIGNAL(MethodInfo("animation_removed", PropertyInfo(Variant::OBJECT, "name", PROPERTY_HINT_RESOURCE_TYPE, "Animation")));
+	ADD_SIGNAL(MethodInfo("animation_renamed", PropertyInfo(Variant::OBJECT, "name", PROPERTY_HINT_RESOURCE_TYPE, "Animation"), PropertyInfo(Variant::OBJECT, "to_name", PROPERTY_HINT_RESOURCE_TYPE, "Animation")));
+}
+AnimationLibrary::AnimationLibrary() {
+}

+ 62 - 0
scene/resources/animation_library.h

@@ -0,0 +1,62 @@
+/*************************************************************************/
+/*  animation_library.h                                                  */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* 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.                */
+/*************************************************************************/
+
+#ifndef ANIMATION_LIBRARY_H
+#define ANIMATION_LIBRARY_H
+
+#include "core/variant/typed_array.h"
+#include "scene/resources/animation.h"
+
+class AnimationLibrary : public Resource {
+	GDCLASS(AnimationLibrary, Resource)
+
+	void _set_data(const Dictionary &p_data);
+	Dictionary _get_data() const;
+
+	TypedArray<StringName> _get_animation_list() const;
+
+	friend class AnimationPlayer; //for faster access
+	Map<StringName, Ref<Animation>> animations;
+
+protected:
+	static void _bind_methods();
+
+public:
+	Error add_animation(const StringName &p_name, const Ref<Animation> &p_animation);
+	void remove_animation(const StringName &p_name);
+	void rename_animation(const StringName &p_name, const StringName &p_new_name);
+	bool has_animation(const StringName &p_name) const;
+	Ref<Animation> get_animation(const StringName &p_name) const;
+	void get_animation_list(List<StringName> *p_animations) const;
+
+	AnimationLibrary();
+};
+
+#endif // ANIMATIONLIBRARY_H