| 1 | /**************************************************************************/ | 
|---|
| 2 | /*  editor_debugger_tree.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 "editor_debugger_tree.h" | 
|---|
| 32 |  | 
|---|
| 33 | #include "editor/editor_node.h" | 
|---|
| 34 | #include "editor/gui/editor_file_dialog.h" | 
|---|
| 35 | #include "editor/scene_tree_dock.h" | 
|---|
| 36 | #include "scene/debugger/scene_debugger.h" | 
|---|
| 37 | #include "scene/gui/texture_rect.h" | 
|---|
| 38 | #include "scene/resources/packed_scene.h" | 
|---|
| 39 | #include "servers/display_server.h" | 
|---|
| 40 |  | 
|---|
| 41 | EditorDebuggerTree::EditorDebuggerTree() { | 
|---|
| 42 | set_v_size_flags(SIZE_EXPAND_FILL); | 
|---|
| 43 | set_allow_rmb_select(true); | 
|---|
| 44 |  | 
|---|
| 45 | // Popup | 
|---|
| 46 | item_menu = memnew(PopupMenu); | 
|---|
| 47 | item_menu->connect( "id_pressed", callable_mp(this, &EditorDebuggerTree::_item_menu_id_pressed)); | 
|---|
| 48 | add_child(item_menu); | 
|---|
| 49 |  | 
|---|
| 50 | // File Dialog | 
|---|
| 51 | file_dialog = memnew(EditorFileDialog); | 
|---|
| 52 | file_dialog->connect( "file_selected", callable_mp(this, &EditorDebuggerTree::_file_selected)); | 
|---|
| 53 | add_child(file_dialog); | 
|---|
| 54 | } | 
|---|
| 55 |  | 
|---|
| 56 | void EditorDebuggerTree::_notification(int p_what) { | 
|---|
| 57 | switch (p_what) { | 
|---|
| 58 | case NOTIFICATION_POSTINITIALIZE: { | 
|---|
| 59 | connect( "cell_selected", callable_mp(this, &EditorDebuggerTree::_scene_tree_selected)); | 
|---|
| 60 | connect( "item_collapsed", callable_mp(this, &EditorDebuggerTree::_scene_tree_folded)); | 
|---|
| 61 | connect( "item_mouse_selected", callable_mp(this, &EditorDebuggerTree::_scene_tree_rmb_selected)); | 
|---|
| 62 | } break; | 
|---|
| 63 | } | 
|---|
| 64 | } | 
|---|
| 65 |  | 
|---|
| 66 | void EditorDebuggerTree::_bind_methods() { | 
|---|
| 67 | ADD_SIGNAL(MethodInfo( "object_selected", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::INT, "debugger"))); | 
|---|
| 68 | ADD_SIGNAL(MethodInfo( "save_node", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "filename"), PropertyInfo(Variant::INT, "debugger"))); | 
|---|
| 69 | ADD_SIGNAL(MethodInfo( "open")); | 
|---|
| 70 | } | 
|---|
| 71 |  | 
|---|
| 72 | void EditorDebuggerTree::_scene_tree_selected() { | 
|---|
| 73 | if (updating_scene_tree) { | 
|---|
| 74 | return; | 
|---|
| 75 | } | 
|---|
| 76 |  | 
|---|
| 77 | TreeItem *item = get_selected(); | 
|---|
| 78 | if (!item) { | 
|---|
| 79 | return; | 
|---|
| 80 | } | 
|---|
| 81 |  | 
|---|
| 82 | inspected_object_id = uint64_t(item->get_metadata(0)); | 
|---|
| 83 |  | 
|---|
| 84 | emit_signal(SNAME( "object_selected"), inspected_object_id, debugger_id); | 
|---|
| 85 | } | 
|---|
| 86 |  | 
|---|
| 87 | void EditorDebuggerTree::_scene_tree_folded(Object *p_obj) { | 
|---|
| 88 | if (updating_scene_tree) { | 
|---|
| 89 | return; | 
|---|
| 90 | } | 
|---|
| 91 | TreeItem *item = Object::cast_to<TreeItem>(p_obj); | 
|---|
| 92 |  | 
|---|
| 93 | if (!item) { | 
|---|
| 94 | return; | 
|---|
| 95 | } | 
|---|
| 96 |  | 
|---|
| 97 | ObjectID id = ObjectID(uint64_t(item->get_metadata(0))); | 
|---|
| 98 | if (unfold_cache.has(id)) { | 
|---|
| 99 | unfold_cache.erase(id); | 
|---|
| 100 | } else { | 
|---|
| 101 | unfold_cache.insert(id); | 
|---|
| 102 | } | 
|---|
| 103 | } | 
|---|
| 104 |  | 
|---|
| 105 | void EditorDebuggerTree::_scene_tree_rmb_selected(const Vector2 &p_position, MouseButton p_button) { | 
|---|
| 106 | if (p_button != MouseButton::RIGHT) { | 
|---|
| 107 | return; | 
|---|
| 108 | } | 
|---|
| 109 |  | 
|---|
| 110 | TreeItem *item = get_item_at_position(p_position); | 
|---|
| 111 | if (!item) { | 
|---|
| 112 | return; | 
|---|
| 113 | } | 
|---|
| 114 |  | 
|---|
| 115 | item->select(0); | 
|---|
| 116 |  | 
|---|
| 117 | item_menu->clear(); | 
|---|
| 118 | item_menu->add_icon_item(get_editor_theme_icon(SNAME( "CreateNewSceneFrom")), TTR( "Save Branch as Scene"), ITEM_MENU_SAVE_REMOTE_NODE); | 
|---|
| 119 | item_menu->add_icon_item(get_editor_theme_icon(SNAME( "CopyNodePath")), TTR( "Copy Node Path"), ITEM_MENU_COPY_NODE_PATH); | 
|---|
| 120 | item_menu->set_position(get_screen_position() + get_local_mouse_position()); | 
|---|
| 121 | item_menu->reset_size(); | 
|---|
| 122 | item_menu->popup(); | 
|---|
| 123 | } | 
|---|
| 124 |  | 
|---|
| 125 | /// Populates inspect_scene_tree given data in nodes as a flat list, encoded depth first. | 
|---|
| 126 | /// | 
|---|
| 127 | /// Given a nodes array like [R,A,B,C,D,E] the following Tree will be generated, assuming | 
|---|
| 128 | /// filter is an empty String, R and A child count are 2, B is 1 and C, D and E are 0. | 
|---|
| 129 | /// | 
|---|
| 130 | /// R | 
|---|
| 131 | /// |-A | 
|---|
| 132 | /// | |-B | 
|---|
| 133 | /// | | |-C | 
|---|
| 134 | /// | | | 
|---|
| 135 | /// | |-D | 
|---|
| 136 | /// | | 
|---|
| 137 | /// |-E | 
|---|
| 138 | /// | 
|---|
| 139 | void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int p_debugger) { | 
|---|
| 140 | updating_scene_tree = true; | 
|---|
| 141 | const String last_path = get_selected_path(); | 
|---|
| 142 | const String filter = SceneTreeDock::get_singleton()->get_filter(); | 
|---|
| 143 | bool filter_changed = filter != last_filter; | 
|---|
| 144 | TreeItem *scroll_item = nullptr; | 
|---|
| 145 |  | 
|---|
| 146 | // Nodes are in a flatten list, depth first. Use a stack of parents, avoid recursion. | 
|---|
| 147 | List<Pair<TreeItem *, int>> parents; | 
|---|
| 148 | for (int i = 0; i < p_tree->nodes.size(); i++) { | 
|---|
| 149 | TreeItem *parent = nullptr; | 
|---|
| 150 | if (parents.size()) { // Find last parent. | 
|---|
| 151 | Pair<TreeItem *, int> &p = parents[0]; | 
|---|
| 152 | parent = p.first; | 
|---|
| 153 | if (!(--p.second)) { // If no child left, remove it. | 
|---|
| 154 | parents.pop_front(); | 
|---|
| 155 | } | 
|---|
| 156 | } | 
|---|
| 157 | // Add this node. | 
|---|
| 158 | const SceneDebuggerTree::RemoteNode &node = p_tree->nodes[i]; | 
|---|
| 159 | TreeItem *item = create_item(parent); | 
|---|
| 160 | item->set_text(0, node.name); | 
|---|
| 161 | if (node.scene_file_path.is_empty()) { | 
|---|
| 162 | item->set_tooltip_text(0, node.name + "\n"+ TTR( "Type:") + " "+ node.type_name); | 
|---|
| 163 | } else { | 
|---|
| 164 | item->set_tooltip_text(0, node.name + "\n"+ TTR( "Instance:") + " "+ node.scene_file_path + "\n"+ TTR( "Type:") + " "+ node.type_name); | 
|---|
| 165 | } | 
|---|
| 166 | Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(node.type_name, ""); | 
|---|
| 167 | if (icon.is_valid()) { | 
|---|
| 168 | item->set_icon(0, icon); | 
|---|
| 169 | } | 
|---|
| 170 | item->set_metadata(0, node.id); | 
|---|
| 171 |  | 
|---|
| 172 | // Set current item as collapsed if necessary (root is never collapsed). | 
|---|
| 173 | if (parent) { | 
|---|
| 174 | if (!unfold_cache.has(node.id)) { | 
|---|
| 175 | item->set_collapsed(true); | 
|---|
| 176 | } | 
|---|
| 177 | } | 
|---|
| 178 | // Select previously selected node. | 
|---|
| 179 | if (debugger_id == p_debugger) { // Can use remote id. | 
|---|
| 180 | if (node.id == inspected_object_id) { | 
|---|
| 181 | item->select(0); | 
|---|
| 182 | if (filter_changed) { | 
|---|
| 183 | scroll_item = item; | 
|---|
| 184 | } | 
|---|
| 185 | } | 
|---|
| 186 | } else { // Must use path | 
|---|
| 187 | if (last_path == _get_path(item)) { | 
|---|
| 188 | updating_scene_tree = false; // Force emission of new selection. | 
|---|
| 189 | item->select(0); | 
|---|
| 190 | if (filter_changed) { | 
|---|
| 191 | scroll_item = item; | 
|---|
| 192 | } | 
|---|
| 193 | updating_scene_tree = true; | 
|---|
| 194 | } | 
|---|
| 195 | } | 
|---|
| 196 |  | 
|---|
| 197 | // Add buttons. | 
|---|
| 198 | const Color remote_button_color = Color(1, 1, 1, 0.8); | 
|---|
| 199 | if (!node.scene_file_path.is_empty()) { | 
|---|
| 200 | String node_scene_file_path = node.scene_file_path; | 
|---|
| 201 | Ref<Texture2D> button_icon = get_editor_theme_icon(SNAME( "InstanceOptions")); | 
|---|
| 202 | String tooltip = vformat(TTR( "This node has been instantiated from a PackedScene file:\n%s\nClick to open the original file in the Editor."), node_scene_file_path); | 
|---|
| 203 |  | 
|---|
| 204 | item->set_meta( "scene_file_path", node_scene_file_path); | 
|---|
| 205 | item->add_button(0, button_icon, BUTTON_SUBSCENE, false, tooltip); | 
|---|
| 206 | item->set_button_color(0, item->get_button_count(0) - 1, remote_button_color); | 
|---|
| 207 | } | 
|---|
| 208 |  | 
|---|
| 209 | if (node.view_flags & SceneDebuggerTree::RemoteNode::VIEW_HAS_VISIBLE_METHOD) { | 
|---|
| 210 | bool node_visible = node.view_flags & SceneDebuggerTree::RemoteNode::VIEW_VISIBLE; | 
|---|
| 211 | bool node_visible_in_tree = node.view_flags & SceneDebuggerTree::RemoteNode::VIEW_VISIBLE_IN_TREE; | 
|---|
| 212 | Ref<Texture2D> button_icon = get_editor_theme_icon(node_visible ? SNAME( "GuiVisibilityVisible") : SNAME( "GuiVisibilityHidden")); | 
|---|
| 213 | String tooltip = TTR( "Toggle Visibility"); | 
|---|
| 214 |  | 
|---|
| 215 | item->set_meta( "visible", node_visible); | 
|---|
| 216 | item->add_button(0, button_icon, BUTTON_VISIBILITY, false, tooltip); | 
|---|
| 217 | if (ClassDB::is_parent_class(node.type_name, "CanvasItem") || ClassDB::is_parent_class(node.type_name, "Node3D")) { | 
|---|
| 218 | item->set_button_color(0, item->get_button_count(0) - 1, node_visible_in_tree ? remote_button_color : Color(1, 1, 1, 0.6)); | 
|---|
| 219 | } else { | 
|---|
| 220 | item->set_button_color(0, item->get_button_count(0) - 1, remote_button_color); | 
|---|
| 221 | } | 
|---|
| 222 | } | 
|---|
| 223 |  | 
|---|
| 224 | // Add in front of the parents stack if children are expected. | 
|---|
| 225 | if (node.child_count) { | 
|---|
| 226 | parents.push_front(Pair<TreeItem *, int>(item, node.child_count)); | 
|---|
| 227 | } else { | 
|---|
| 228 | // Apply filters. | 
|---|
| 229 | while (parent) { | 
|---|
| 230 | const bool had_siblings = item->get_prev() || item->get_next(); | 
|---|
| 231 | if (filter.is_subsequence_ofn(item->get_text(0))) { | 
|---|
| 232 | break; // Filter matches, must survive. | 
|---|
| 233 | } | 
|---|
| 234 | parent->remove_child(item); | 
|---|
| 235 | memdelete(item); | 
|---|
| 236 | if (scroll_item == item) { | 
|---|
| 237 | scroll_item = nullptr; | 
|---|
| 238 | } | 
|---|
| 239 | if (had_siblings) { | 
|---|
| 240 | break; // Parent must survive. | 
|---|
| 241 | } | 
|---|
| 242 | item = parent; | 
|---|
| 243 | parent = item->get_parent(); | 
|---|
| 244 | // Check if parent expects more children. | 
|---|
| 245 | for (int j = 0; j < parents.size(); j++) { | 
|---|
| 246 | if (parents[j].first == item) { | 
|---|
| 247 | parent = nullptr; | 
|---|
| 248 | break; // Might have more children. | 
|---|
| 249 | } | 
|---|
| 250 | } | 
|---|
| 251 | } | 
|---|
| 252 | } | 
|---|
| 253 | } | 
|---|
| 254 | debugger_id = p_debugger; // Needed by hook, could be avoided if every debugger had its own tree | 
|---|
| 255 | if (scroll_item) { | 
|---|
| 256 | call_deferred(SNAME( "scroll_to_item"), scroll_item); | 
|---|
| 257 | } | 
|---|
| 258 | last_filter = filter; | 
|---|
| 259 | updating_scene_tree = false; | 
|---|
| 260 | } | 
|---|
| 261 |  | 
|---|
| 262 | Variant EditorDebuggerTree::get_drag_data(const Point2 &p_point) { | 
|---|
| 263 | if (get_button_id_at_position(p_point) != -1) { | 
|---|
| 264 | return Variant(); | 
|---|
| 265 | } | 
|---|
| 266 |  | 
|---|
| 267 | TreeItem *selected = get_selected(); | 
|---|
| 268 | if (!selected) { | 
|---|
| 269 | return Variant(); | 
|---|
| 270 | } | 
|---|
| 271 |  | 
|---|
| 272 | String path = selected->get_text(0); | 
|---|
| 273 |  | 
|---|
| 274 | HBoxContainer *hb = memnew(HBoxContainer); | 
|---|
| 275 | TextureRect *tf = memnew(TextureRect); | 
|---|
| 276 | tf->set_texture(selected->get_icon(0)); | 
|---|
| 277 | tf->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED); | 
|---|
| 278 | hb->add_child(tf); | 
|---|
| 279 | Label *label = memnew(Label(path)); | 
|---|
| 280 | hb->add_child(label); | 
|---|
| 281 | set_drag_preview(hb); | 
|---|
| 282 |  | 
|---|
| 283 | if (!selected->get_parent() || !selected->get_parent()->get_parent()) { | 
|---|
| 284 | path = "."; | 
|---|
| 285 | } else { | 
|---|
| 286 | while (selected->get_parent()->get_parent() != get_root()) { | 
|---|
| 287 | selected = selected->get_parent(); | 
|---|
| 288 | path = selected->get_text(0) + "/"+ path; | 
|---|
| 289 | } | 
|---|
| 290 | } | 
|---|
| 291 |  | 
|---|
| 292 | return vformat( "\"%s\"", path); | 
|---|
| 293 | } | 
|---|
| 294 |  | 
|---|
| 295 | String EditorDebuggerTree::get_selected_path() { | 
|---|
| 296 | if (!get_selected()) { | 
|---|
| 297 | return ""; | 
|---|
| 298 | } | 
|---|
| 299 | return _get_path(get_selected()); | 
|---|
| 300 | } | 
|---|
| 301 |  | 
|---|
| 302 | String EditorDebuggerTree::_get_path(TreeItem *p_item) { | 
|---|
| 303 | ERR_FAIL_NULL_V(p_item, ""); | 
|---|
| 304 |  | 
|---|
| 305 | if (p_item->get_parent() == nullptr) { | 
|---|
| 306 | return "/root"; | 
|---|
| 307 | } | 
|---|
| 308 | String text = p_item->get_text(0); | 
|---|
| 309 | TreeItem *cur = p_item->get_parent(); | 
|---|
| 310 | while (cur) { | 
|---|
| 311 | text = cur->get_text(0) + "/"+ text; | 
|---|
| 312 | cur = cur->get_parent(); | 
|---|
| 313 | } | 
|---|
| 314 | return "/"+ text; | 
|---|
| 315 | } | 
|---|
| 316 |  | 
|---|
| 317 | void EditorDebuggerTree::(int p_option) { | 
|---|
| 318 | switch (p_option) { | 
|---|
| 319 | case ITEM_MENU_SAVE_REMOTE_NODE: { | 
|---|
| 320 | file_dialog->set_access(EditorFileDialog::ACCESS_RESOURCES); | 
|---|
| 321 | file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE); | 
|---|
| 322 |  | 
|---|
| 323 | List<String> extensions; | 
|---|
| 324 | Ref<PackedScene> sd = memnew(PackedScene); | 
|---|
| 325 | ResourceSaver::get_recognized_extensions(sd, &extensions); | 
|---|
| 326 | file_dialog->clear_filters(); | 
|---|
| 327 | for (int i = 0; i < extensions.size(); i++) { | 
|---|
| 328 | file_dialog->add_filter( "*."+ extensions[i], extensions[i].to_upper()); | 
|---|
| 329 | } | 
|---|
| 330 |  | 
|---|
| 331 | String filename = get_selected_path().get_file() + "."+ extensions.front()->get().to_lower(); | 
|---|
| 332 | file_dialog->set_current_path(filename); | 
|---|
| 333 | file_dialog->popup_file_dialog(); | 
|---|
| 334 | } break; | 
|---|
| 335 | case ITEM_MENU_COPY_NODE_PATH: { | 
|---|
| 336 | String text = get_selected_path(); | 
|---|
| 337 | if (text.is_empty()) { | 
|---|
| 338 | return; | 
|---|
| 339 | } else if (text == "/root") { | 
|---|
| 340 | text = "."; | 
|---|
| 341 | } else { | 
|---|
| 342 | text = text.replace( "/root/", ""); | 
|---|
| 343 | int slash = text.find( "/"); | 
|---|
| 344 | if (slash < 0) { | 
|---|
| 345 | text = "."; | 
|---|
| 346 | } else { | 
|---|
| 347 | text = text.substr(slash + 1); | 
|---|
| 348 | } | 
|---|
| 349 | } | 
|---|
| 350 | DisplayServer::get_singleton()->clipboard_set(text); | 
|---|
| 351 | } break; | 
|---|
| 352 | } | 
|---|
| 353 | } | 
|---|
| 354 |  | 
|---|
| 355 | void EditorDebuggerTree::_file_selected(const String &p_file) { | 
|---|
| 356 | if (inspected_object_id.is_null()) { | 
|---|
| 357 | return; | 
|---|
| 358 | } | 
|---|
| 359 | emit_signal(SNAME( "save_node"), inspected_object_id, p_file, debugger_id); | 
|---|
| 360 | } | 
|---|
| 361 |  | 
|---|