1/**************************************************************************/
2/* root_motion_editor_plugin.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "root_motion_editor_plugin.h"
32#include "editor/editor_node.h"
33#include "scene/animation/animation_player.h"
34#include "scene/animation/animation_tree.h"
35#include "scene/gui/button.h"
36#include "scene/gui/dialogs.h"
37#include "scene/gui/tree.h"
38#include "scene/main/window.h"
39
40void EditorPropertyRootMotion::_confirmed() {
41 TreeItem *ti = filters->get_selected();
42 if (!ti) {
43 return;
44 }
45
46 NodePath path = ti->get_metadata(0);
47 emit_changed(get_edited_property(), path);
48 update_property();
49 filter_dialog->hide(); //may come from activated
50}
51
52void EditorPropertyRootMotion::_node_assign() {
53 AnimationTree *atree = Object::cast_to<AnimationTree>(get_edited_object());
54 if (!atree->has_node(atree->get_animation_player())) {
55 EditorNode::get_singleton()->show_warning(TTR("AnimationTree has no path set to an AnimationPlayer"));
56 return;
57 }
58 AnimationPlayer *player = Object::cast_to<AnimationPlayer>(atree->get_node(atree->get_animation_player()));
59 if (!player) {
60 EditorNode::get_singleton()->show_warning(TTR("Path to AnimationPlayer is invalid"));
61 return;
62 }
63
64 Node *base = player->get_node(player->get_root());
65
66 if (!base) {
67 EditorNode::get_singleton()->show_warning(TTR("Animation player has no valid root node path, so unable to retrieve track names."));
68 return;
69 }
70
71 HashSet<String> paths;
72 {
73 List<StringName> animations;
74 player->get_animation_list(&animations);
75
76 for (const StringName &E : animations) {
77 Ref<Animation> anim = player->get_animation(E);
78 for (int i = 0; i < anim->get_track_count(); i++) {
79 String pathname = anim->track_get_path(i).get_concatenated_names();
80 if (!paths.has(pathname)) {
81 paths.insert(pathname);
82 }
83 }
84 }
85 }
86
87 filters->clear();
88 TreeItem *root = filters->create_item();
89
90 HashMap<String, TreeItem *> parenthood;
91
92 for (const String &E : paths) {
93 NodePath path = E;
94 TreeItem *ti = nullptr;
95 String accum;
96 for (int i = 0; i < path.get_name_count(); i++) {
97 String name = path.get_name(i);
98 if (!accum.is_empty()) {
99 accum += "/";
100 }
101 accum += name;
102 if (!parenthood.has(accum)) {
103 if (ti) {
104 ti = filters->create_item(ti);
105 } else {
106 ti = filters->create_item(root);
107 }
108 parenthood[accum] = ti;
109 ti->set_text(0, name);
110 ti->set_selectable(0, false);
111 ti->set_editable(0, false);
112
113 if (base->has_node(accum)) {
114 Node *node = base->get_node(accum);
115 ti->set_icon(0, EditorNode::get_singleton()->get_object_icon(node, "Node"));
116 }
117
118 } else {
119 ti = parenthood[accum];
120 }
121 }
122
123 Node *node = nullptr;
124 if (base->has_node(accum)) {
125 node = base->get_node(accum);
126 }
127 if (!node) {
128 continue; //no node, can't edit
129 }
130
131 Skeleton3D *skeleton = Object::cast_to<Skeleton3D>(node);
132 if (skeleton) {
133 HashMap<int, TreeItem *> items;
134 items.insert(-1, ti);
135 Ref<Texture> bone_icon = get_editor_theme_icon(SNAME("BoneAttachment3D"));
136 Vector<int> bones_to_process = skeleton->get_parentless_bones();
137 while (bones_to_process.size() > 0) {
138 int current_bone_idx = bones_to_process[0];
139 bones_to_process.erase(current_bone_idx);
140
141 Vector<int> current_bone_child_bones = skeleton->get_bone_children(current_bone_idx);
142 int child_bone_size = current_bone_child_bones.size();
143 for (int i = 0; i < child_bone_size; i++) {
144 bones_to_process.push_back(current_bone_child_bones[i]);
145 }
146
147 const int parent_idx = skeleton->get_bone_parent(current_bone_idx);
148 TreeItem *parent_item = items.find(parent_idx)->value;
149
150 TreeItem *joint_item = filters->create_item(parent_item);
151 items.insert(current_bone_idx, joint_item);
152
153 joint_item->set_text(0, skeleton->get_bone_name(current_bone_idx));
154 joint_item->set_icon(0, bone_icon);
155 joint_item->set_selectable(0, true);
156 joint_item->set_metadata(0, accum + ":" + skeleton->get_bone_name(current_bone_idx));
157 joint_item->set_collapsed(true);
158 }
159 }
160 }
161
162 filters->ensure_cursor_is_visible();
163 filter_dialog->popup_centered_ratio();
164}
165
166void EditorPropertyRootMotion::_node_clear() {
167 emit_changed(get_edited_property(), NodePath());
168 update_property();
169}
170
171void EditorPropertyRootMotion::update_property() {
172 NodePath p = get_edited_property_value();
173 assign->set_tooltip_text(p);
174 if (p == NodePath()) {
175 assign->set_icon(Ref<Texture2D>());
176 assign->set_text(TTR("Assign..."));
177 assign->set_flat(false);
178 return;
179 }
180
181 assign->set_icon(Ref<Texture2D>());
182 assign->set_text(p);
183}
184
185void EditorPropertyRootMotion::setup(const NodePath &p_base_hint) {
186 base_hint = p_base_hint;
187}
188
189void EditorPropertyRootMotion::_notification(int p_what) {
190 switch (p_what) {
191 case NOTIFICATION_ENTER_TREE:
192 case NOTIFICATION_THEME_CHANGED: {
193 Ref<Texture2D> t = get_editor_theme_icon(SNAME("Clear"));
194 clear->set_icon(t);
195 } break;
196 }
197}
198
199void EditorPropertyRootMotion::_bind_methods() {
200}
201
202EditorPropertyRootMotion::EditorPropertyRootMotion() {
203 HBoxContainer *hbc = memnew(HBoxContainer);
204 add_child(hbc);
205 assign = memnew(Button);
206 assign->set_h_size_flags(SIZE_EXPAND_FILL);
207 assign->set_clip_text(true);
208 assign->connect("pressed", callable_mp(this, &EditorPropertyRootMotion::_node_assign));
209 hbc->add_child(assign);
210
211 clear = memnew(Button);
212 clear->connect("pressed", callable_mp(this, &EditorPropertyRootMotion::_node_clear));
213 hbc->add_child(clear);
214
215 filter_dialog = memnew(ConfirmationDialog);
216 add_child(filter_dialog);
217 filter_dialog->set_title(TTR("Edit Filtered Tracks:"));
218 filter_dialog->connect("confirmed", callable_mp(this, &EditorPropertyRootMotion::_confirmed));
219
220 filters = memnew(Tree);
221 filter_dialog->add_child(filters);
222 filters->set_v_size_flags(SIZE_EXPAND_FILL);
223 filters->set_hide_root(true);
224 filters->connect("item_activated", callable_mp(this, &EditorPropertyRootMotion::_confirmed));
225 //filters->connect("item_edited", this, "_filter_edited");
226}
227
228//////////////////////////
229
230bool EditorInspectorRootMotionPlugin::can_handle(Object *p_object) {
231 return true; // Can handle everything.
232}
233
234bool EditorInspectorRootMotionPlugin::parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide) {
235 if (p_path == "root_motion_track" && p_object->is_class("AnimationTree") && p_type == Variant::NODE_PATH) {
236 EditorPropertyRootMotion *editor = memnew(EditorPropertyRootMotion);
237 add_property_editor(p_path, editor);
238 return true;
239 }
240
241 return false;
242}
243