| 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 | |
| 40 | void 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 | |
| 52 | void 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 | |
| 166 | void EditorPropertyRootMotion::_node_clear() { |
| 167 | emit_changed(get_edited_property(), NodePath()); |
| 168 | update_property(); |
| 169 | } |
| 170 | |
| 171 | void 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 | |
| 185 | void EditorPropertyRootMotion::setup(const NodePath &p_base_hint) { |
| 186 | base_hint = p_base_hint; |
| 187 | } |
| 188 | |
| 189 | void 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 | |
| 199 | void EditorPropertyRootMotion::_bind_methods() { |
| 200 | } |
| 201 | |
| 202 | EditorPropertyRootMotion::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 | |
| 230 | bool EditorInspectorRootMotionPlugin::can_handle(Object *p_object) { |
| 231 | return true; // Can handle everything. |
| 232 | } |
| 233 | |
| 234 | bool 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 | |