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 | |