/**************************************************************************/ /* scene_tree_dock.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /**************************************************************************/ /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ /* */ /* 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 "scene_tree_dock.h" #include "core/config/project_settings.h" #include "core/input/input.h" #include "core/io/resource_saver.h" #include "core/object/class_db.h" #include "core/os/keyboard.h" #include "editor/debugger/editor_debugger_node.h" #include "editor/editor_feature_profile.h" #include "editor/editor_node.h" #include "editor/editor_paths.h" #include "editor/editor_quick_open.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" #include "editor/filesystem_dock.h" #include "editor/gui/editor_file_dialog.h" #include "editor/inspector_dock.h" #include "editor/multi_node_edit.h" #include "editor/node_dock.h" #include "editor/plugins/animation_player_editor_plugin.h" #include "editor/plugins/canvas_item_editor_plugin.h" #include "editor/plugins/editor_context_menu_plugin.h" #include "editor/plugins/node_3d_editor_plugin.h" #include "editor/plugins/script_editor_plugin.h" #include "editor/reparent_dialog.h" #include "editor/shader_create_dialog.h" #include "editor/themes/editor_scale.h" #include "scene/animation/animation_tree.h" #include "scene/audio/audio_stream_player.h" #include "scene/gui/check_box.h" #include "scene/property_utils.h" #include "scene/resources/packed_scene.h" #include "servers/display_server.h" #include "modules/modules_enabled.gen.h" // For regex. #ifdef MODULE_REGEX_ENABLED #include "editor/rename_dialog.h" #endif // MODULE_REGEX_ENABLED void SceneTreeDock::_nodes_drag_begin() { pending_click_select = nullptr; } void SceneTreeDock::_quick_open() { instantiate_scenes(quick_open->get_selected_files(), scene_tree->get_selected()); } void SceneTreeDock::_inspect_hovered_node() { select_node_hovered_at_end_of_drag = true; Tree *tree = scene_tree->get_scene_tree(); TreeItem *item = tree->get_item_with_metadata(node_hovered_now->get_path()); if (item) { if (tree_item_inspected) { tree_item_inspected->clear_custom_color(0); } tree_item_inspected = item; tree_item_inspected->set_custom_color(0, get_theme_color(SNAME("accent_color"), EditorStringName(Editor))); } InspectorDock::get_inspector_singleton()->edit(node_hovered_now); InspectorDock::get_inspector_singleton()->propagate_notification(NOTIFICATION_DRAG_BEGIN); // Enable inspector drag preview after it updated. InspectorDock::get_singleton()->update(node_hovered_now); EditorNode::get_singleton()->hide_unused_editors(); } void SceneTreeDock::_handle_hover_to_inspect() { Tree *tree = scene_tree->get_scene_tree(); TreeItem *item = tree->get_item_at_position(tree->get_local_mouse_position()); if (item) { const NodePath &np = item->get_metadata(0); node_hovered_now = get_node_or_null(np); if (node_hovered_previously != node_hovered_now) { inspect_hovered_node_delay->start(); } node_hovered_previously = node_hovered_now; } else { _reset_hovering_timer(); } } void SceneTreeDock::_reset_hovering_timer() { if (!inspect_hovered_node_delay->is_stopped()) { inspect_hovered_node_delay->stop(); } node_hovered_previously = nullptr; } void SceneTreeDock::input(const Ref &p_event) { ERR_FAIL_COND(p_event.is_null()); Ref mb = p_event; if (mb.is_valid() && (mb->get_button_index() == MouseButton::LEFT || mb->get_button_index() == MouseButton::RIGHT)) { if (mb->is_pressed() && scene_tree->get_rect().has_point(scene_tree->get_local_mouse_position())) { tree_clicked = true; } else if (!mb->is_pressed()) { tree_clicked = false; } if (!mb->is_pressed() && pending_click_select) { _push_item(pending_click_select); pending_click_select = nullptr; } if (mb->is_released()) { if (tree_item_inspected) { tree_item_inspected->clear_custom_color(0); tree_item_inspected = nullptr; } _reset_hovering_timer(); } } if (tree_clicked && get_viewport()->gui_is_dragging()) { _handle_hover_to_inspect(); } } void SceneTreeDock::shortcut_input(const Ref &p_event) { ERR_FAIL_COND(p_event.is_null()); Control *focus_owner = get_viewport()->gui_get_focus_owner(); if (focus_owner && focus_owner->is_text_field()) { return; } if (!p_event->is_pressed() || p_event->is_echo()) { return; } if (ED_IS_SHORTCUT("scene_tree/rename", p_event)) { // Prevent renaming if a button is focused // to avoid conflict with Enter shortcut on macOS if (!focus_owner || !Object::cast_to(focus_owner)) { _tool_selected(TOOL_RENAME); } #ifdef MODULE_REGEX_ENABLED } else if (ED_IS_SHORTCUT("scene_tree/batch_rename", p_event)) { _tool_selected(TOOL_BATCH_RENAME); #endif // MODULE_REGEX_ENABLED } else if (ED_IS_SHORTCUT("scene_tree/add_child_node", p_event)) { _tool_selected(TOOL_NEW); } else if (ED_IS_SHORTCUT("scene_tree/instantiate_scene", p_event)) { _tool_selected(TOOL_INSTANTIATE); } else if (ED_IS_SHORTCUT("scene_tree/expand_collapse_all", p_event)) { _tool_selected(TOOL_EXPAND_COLLAPSE); } else if (ED_IS_SHORTCUT("scene_tree/cut_node", p_event)) { _tool_selected(TOOL_CUT); } else if (ED_IS_SHORTCUT("scene_tree/copy_node", p_event)) { _tool_selected(TOOL_COPY); } else if (ED_IS_SHORTCUT("scene_tree/paste_node", p_event)) { _tool_selected(TOOL_PASTE); } else if (ED_IS_SHORTCUT("scene_tree/paste_node_as_sibling", p_event)) { _tool_selected(TOOL_PASTE_AS_SIBLING); } else if (ED_IS_SHORTCUT("scene_tree/change_node_type", p_event)) { _tool_selected(TOOL_REPLACE); } else if (ED_IS_SHORTCUT("scene_tree/duplicate", p_event)) { _tool_selected(TOOL_DUPLICATE); } else if (ED_IS_SHORTCUT("scene_tree/attach_script", p_event)) { _tool_selected(TOOL_ATTACH_SCRIPT); } else if (ED_IS_SHORTCUT("scene_tree/detach_script", p_event)) { _tool_selected(TOOL_DETACH_SCRIPT); } else if (ED_IS_SHORTCUT("scene_tree/move_up", p_event)) { _tool_selected(TOOL_MOVE_UP); } else if (ED_IS_SHORTCUT("scene_tree/move_down", p_event)) { _tool_selected(TOOL_MOVE_DOWN); } else if (ED_IS_SHORTCUT("scene_tree/reparent", p_event)) { _tool_selected(TOOL_REPARENT); } else if (ED_IS_SHORTCUT("scene_tree/reparent_to_new_node", p_event)) { _tool_selected(TOOL_REPARENT_TO_NEW_NODE); } else if (ED_IS_SHORTCUT("scene_tree/save_branch_as_scene", p_event)) { _tool_selected(TOOL_NEW_SCENE_FROM); } else if (ED_IS_SHORTCUT("scene_tree/delete_no_confirm", p_event)) { _tool_selected(TOOL_ERASE, true); } else if (ED_IS_SHORTCUT("scene_tree/copy_node_path", p_event)) { _tool_selected(TOOL_COPY_NODE_PATH); } else if (ED_IS_SHORTCUT("scene_tree/show_in_file_system", p_event)) { _tool_selected(TOOL_SHOW_IN_FILE_SYSTEM); } else if (ED_IS_SHORTCUT("scene_tree/toggle_unique_name", p_event)) { _tool_selected(TOOL_TOGGLE_SCENE_UNIQUE_NAME); } else if (ED_IS_SHORTCUT("scene_tree/toggle_editable_children", p_event)) { _tool_selected(TOOL_SCENE_EDITABLE_CHILDREN); } else if (ED_IS_SHORTCUT("scene_tree/delete", p_event)) { _tool_selected(TOOL_ERASE); } else { Callable custom_callback = EditorContextMenuPluginManager::get_singleton()->match_custom_shortcut(EditorContextMenuPlugin::CONTEXT_SLOT_SCENE_TREE, p_event); if (custom_callback.is_valid()) { EditorContextMenuPluginManager::get_singleton()->invoke_callback(custom_callback, _get_selection_array()); } else { return; } } // Tool selection was successful, accept the event to stop propagation. accept_event(); } void SceneTreeDock::_scene_tree_gui_input(Ref p_event) { Ref key = p_event; if (key.is_null() || !key->is_pressed() || key->is_echo()) { return; } if (ED_IS_SHORTCUT("editor/open_search", p_event)) { filter->grab_focus(); filter->select_all(); accept_event(); } } void SceneTreeDock::instantiate(const String &p_file) { Vector scenes; scenes.push_back(p_file); instantiate_scenes(scenes, scene_tree->get_selected()); } void SceneTreeDock::instantiate_scenes(const Vector &p_files, Node *p_parent) { Node *parent = p_parent; if (!parent) { parent = scene_tree->get_selected(); } if (!parent) { parent = edited_scene; } if (!parent) { if (p_files.size() == 1) { accept->set_text(TTR("No parent to instantiate a child at.")); } else { accept->set_text(TTR("No parent to instantiate the scenes at.")); } accept->popup_centered(); return; }; _perform_instantiate_scenes(p_files, parent, -1); } void SceneTreeDock::_perform_instantiate_scenes(const Vector &p_files, Node *p_parent, int p_pos) { ERR_FAIL_NULL(p_parent); Vector instances; bool error = false; for (int i = 0; i < p_files.size(); i++) { Ref sdata = ResourceLoader::load(p_files[i]); if (!sdata.is_valid()) { current_option = -1; accept->set_text(vformat(TTR("Error loading scene from %s"), p_files[i])); accept->popup_centered(); error = true; break; } Node *instantiated_scene = sdata->instantiate(PackedScene::GEN_EDIT_STATE_INSTANCE); if (!instantiated_scene) { current_option = -1; accept->set_text(vformat(TTR("Error instantiating scene from %s"), p_files[i])); accept->popup_centered(); error = true; break; } if (!edited_scene->get_scene_file_path().is_empty()) { if (_cyclical_dependency_exists(edited_scene->get_scene_file_path(), instantiated_scene)) { accept->set_text(vformat(TTR("Cannot instantiate the scene '%s' because the current scene exists within one of its nodes."), p_files[i])); accept->popup_centered(); error = true; break; } } instantiated_scene->set_scene_file_path(ProjectSettings::get_singleton()->localize_path(p_files[i])); instances.push_back(instantiated_scene); } if (error) { for (int i = 0; i < instances.size(); i++) { memdelete(instances[i]); } return; } EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action_for_history(TTRN("Instantiate Scene", "Instantiate Scenes", instances.size()), editor_data->get_current_edited_scene_history_id()); undo_redo->add_do_method(editor_selection, "clear"); for (int i = 0; i < instances.size(); i++) { Node *instantiated_scene = instances[i]; undo_redo->add_do_method(p_parent, "add_child", instantiated_scene, true); if (p_pos >= 0) { undo_redo->add_do_method(p_parent, "move_child", instantiated_scene, p_pos + i); } undo_redo->add_do_method(instantiated_scene, "set_owner", edited_scene); undo_redo->add_do_method(editor_selection, "add_node", instantiated_scene); undo_redo->add_do_reference(instantiated_scene); undo_redo->add_undo_method(p_parent, "remove_child", instantiated_scene); String new_name = p_parent->validate_child_name(instantiated_scene); EditorDebuggerNode *ed = EditorDebuggerNode::get_singleton(); undo_redo->add_do_method(ed, "live_debug_instantiate_node", edited_scene->get_path_to(p_parent), p_files[i], new_name); undo_redo->add_undo_method(ed, "live_debug_remove_node", NodePath(String(edited_scene->get_path_to(p_parent)).path_join(new_name))); } undo_redo->commit_action(); _push_item(instances[instances.size() - 1]); for (int i = 0; i < instances.size(); i++) { emit_signal(SNAME("node_created"), instances[i]); } } void SceneTreeDock::_perform_create_audio_stream_players(const Vector &p_files, Node *p_parent, int p_pos) { ERR_FAIL_NULL(p_parent); StringName node_type = "AudioStreamPlayer"; if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) { if (Object::cast_to(p_parent)) { node_type = "AudioStreamPlayer2D"; } else if (Object::cast_to(p_parent)) { node_type = "AudioStreamPlayer3D"; } } Vector nodes; bool error = false; for (const String &path : p_files) { Ref stream = ResourceLoader::load(path); if (stream.is_null()) { current_option = -1; accept->set_text(vformat(TTR("Error loading audio stream from %s"), path)); accept->popup_centered(); error = true; break; } Node *player = Object::cast_to(ClassDB::instantiate(node_type)); player->set("stream", stream); // Adjust casing according to project setting. The file name is expected to be in snake_case, but will work for others. const String &node_name = Node::adjust_name_casing(path.get_file().get_basename()); if (!node_name.is_empty()) { player->set_name(node_name); } nodes.push_back(player); } if (error) { for (Node *node : nodes) { memdelete(node); } return; } EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action_for_history(TTRN("Create AudioStreamPlayer", "Create AudioStreamPlayers", nodes.size()), editor_data->get_current_edited_scene_history_id()); undo_redo->add_do_method(editor_selection, "clear"); for (int i = 0; i < nodes.size(); i++) { Node *node = nodes[i]; undo_redo->add_do_method(p_parent, "add_child", node, true); if (p_pos >= 0) { undo_redo->add_do_method(p_parent, "move_child", node, p_pos + i); } undo_redo->add_do_method(node, "set_owner", edited_scene); undo_redo->add_do_method(editor_selection, "add_node", node); undo_redo->add_do_reference(node); undo_redo->add_undo_method(p_parent, "remove_child", node); String new_name = p_parent->validate_child_name(node); EditorDebuggerNode *ed = EditorDebuggerNode::get_singleton(); undo_redo->add_do_method(ed, "live_debug_create_node", edited_scene->get_path_to(p_parent), node->get_class(), new_name); undo_redo->add_undo_method(ed, "live_debug_remove_node", NodePath(String(edited_scene->get_path_to(p_parent)).path_join(new_name))); } undo_redo->commit_action(); } void SceneTreeDock::_replace_with_branch_scene(const String &p_file, Node *base) { // `move_child` + `get_index` doesn't really work for internal nodes. ERR_FAIL_COND_MSG(base->get_internal_mode() != INTERNAL_MODE_DISABLED, "Trying to replace internal node, this is not supported."); Ref sdata = ResourceLoader::load(p_file); if (!sdata.is_valid()) { accept->set_text(vformat(TTR("Error loading scene from %s"), p_file)); accept->popup_centered(); return; } Node *instantiated_scene = sdata->instantiate(PackedScene::GEN_EDIT_STATE_INSTANCE); if (!instantiated_scene) { accept->set_text(vformat(TTR("Error instantiating scene from %s"), p_file)); accept->popup_centered(); return; } instantiated_scene->set_unique_name_in_owner(base->is_unique_name_in_owner()); EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Replace with Branch Scene")); Node *parent = base->get_parent(); int pos = base->get_index(false); undo_redo->add_do_method(parent, "remove_child", base); undo_redo->add_undo_method(parent, "remove_child", instantiated_scene); undo_redo->add_do_method(parent, "add_child", instantiated_scene, true); undo_redo->add_undo_method(parent, "add_child", base, true); undo_redo->add_do_method(parent, "move_child", instantiated_scene, pos); undo_redo->add_undo_method(parent, "move_child", base, pos); List owned; base->get_owned_by(base->get_owner(), &owned); Array owners; for (Node *F : owned) { owners.push_back(F); } undo_redo->add_do_method(instantiated_scene, "set_owner", edited_scene); undo_redo->add_undo_method(this, "_set_owners", edited_scene, owners); undo_redo->add_do_method(editor_selection, "clear"); undo_redo->add_undo_method(editor_selection, "clear"); undo_redo->add_do_method(editor_selection, "add_node", instantiated_scene); undo_redo->add_undo_method(editor_selection, "add_node", base); undo_redo->add_do_property(scene_tree, "set_selected", instantiated_scene); undo_redo->add_undo_property(scene_tree, "set_selected", base); undo_redo->add_do_reference(instantiated_scene); undo_redo->add_undo_reference(base); undo_redo->commit_action(); } bool SceneTreeDock::_cyclical_dependency_exists(const String &p_target_scene_path, Node *p_desired_node) { int childCount = p_desired_node->get_child_count(); if (_track_inherit(p_target_scene_path, p_desired_node)) { return true; } for (int i = 0; i < childCount; i++) { Node *child = p_desired_node->get_child(i); if (_cyclical_dependency_exists(p_target_scene_path, child)) { return true; } } return false; } bool SceneTreeDock::_track_inherit(const String &p_target_scene_path, Node *p_desired_node) { Node *p = p_desired_node; bool result = false; Vector instances; while (true) { if (p->get_scene_file_path() == p_target_scene_path) { result = true; break; } Ref ss = p->get_scene_inherited_state(); if (ss.is_valid()) { String path = ss->get_path(); Ref pack_data = ResourceLoader::load(path); if (pack_data.is_valid()) { p = pack_data->instantiate(PackedScene::GEN_EDIT_STATE_INSTANCE); if (!p) { continue; } instances.push_back(p); } else { break; } } else { break; } } for (int i = 0; i < instances.size(); i++) { memdelete(instances[i]); } return result; } void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) { current_option = p_tool; switch (p_tool) { #ifdef MODULE_REGEX_ENABLED case TOOL_BATCH_RENAME: { if (!profile_allow_editing) { break; } if (editor_selection->get_selected_node_list().size() > 1) { if (!_validate_no_foreign()) { break; } rename_dialog->popup_centered(); } } break; #endif // MODULE_REGEX_ENABLED case TOOL_RENAME: { if (!profile_allow_editing) { break; } Tree *tree = scene_tree->get_scene_tree(); if (tree->is_anything_selected()) { if (!_validate_no_foreign()) { break; } tree->grab_focus(); tree->edit_selected(); } } break; case TOOL_REPARENT_TO_NEW_NODE: if (!_validate_no_foreign()) { break; } [[fallthrough]]; case TOOL_NEW: { if (!profile_allow_editing) { break; } if (reset_create_dialog && !p_confirm_override) { create_dialog->set_base_type("Node"); reset_create_dialog = false; } // Prefer nodes that inherit from the current scene root. Node *current_edited_scene_root = EditorNode::get_singleton()->get_edited_scene(); if (current_edited_scene_root) { String root_class = current_edited_scene_root->get_class_name(); static Vector preferred_types; if (preferred_types.is_empty()) { preferred_types.push_back("Control"); preferred_types.push_back("Node2D"); preferred_types.push_back("Node3D"); } for (int i = 0; i < preferred_types.size(); i++) { if (ClassDB::is_parent_class(root_class, preferred_types[i])) { create_dialog->set_preferred_search_result_type(preferred_types[i]); break; } } } create_dialog->popup_create(true); if (!p_confirm_override) { emit_signal(SNAME("add_node_used")); } } break; case TOOL_INSTANTIATE: { if (!profile_allow_editing) { break; } Node *scene = edited_scene; if (!scene) { EditorNode::get_singleton()->new_inherited_scene(); break; } quick_open->popup_dialog("PackedScene", true); quick_open->set_title(TTR("Instantiate Child Scene")); if (!p_confirm_override) { emit_signal(SNAME("add_node_used")); } } break; case TOOL_EXPAND_COLLAPSE: { Tree *tree = scene_tree->get_scene_tree(); TreeItem *selected_item = tree->get_selected(); if (!selected_item) { selected_item = tree->get_root(); if (!selected_item) { break; } } bool collapsed = selected_item->is_any_collapsed(); selected_item->set_collapsed_recursive(!collapsed); tree->ensure_cursor_is_visible(); } break; case TOOL_CUT: case TOOL_COPY: { if (!edited_scene || (p_tool == TOOL_CUT && !_validate_no_foreign())) { break; } List selection = editor_selection->get_selected_node_list(); if (selection.size() == 0) { break; } bool was_empty = false; if (!node_clipboard.is_empty()) { _clear_clipboard(); } else { was_empty = true; } clipboard_source_scene = EditorNode::get_singleton()->get_edited_scene()->get_scene_file_path(); selection.sort_custom(); for (Node *node : selection) { HashMap duplimap; Node *dup = node->duplicate_from_editor(duplimap); ERR_CONTINUE(!dup); // Preserve ownership relations ready for pasting. List owned; Node *owner = node; while (owner) { List cur_owned; node->get_owned_by(owner, &cur_owned); owner = owner->get_owner(); for (Node *F : cur_owned) { owned.push_back(F); } } for (Node *F : owned) { if (!duplimap.has(F) || F == node) { continue; } Node *d = duplimap[F]; // Only use nullptr as a marker that ownership may need to be assigned when pasting. // The ownership is subsequently tracked in the node_clipboard_edited_scene_owned list. d->set_owner(nullptr); node_clipboard_edited_scene_owned.insert(d); } node_clipboard.push_back(dup); } if (p_tool == TOOL_CUT) { _delete_confirm(true); } if (was_empty) { _update_create_root_dialog(); } } break; case TOOL_PASTE: { paste_nodes(false); } break; case TOOL_PASTE_AS_SIBLING: { paste_nodes(true); } break; case TOOL_REPLACE: { if (!profile_allow_editing) { break; } if (!_validate_no_foreign()) { break; } if (!_validate_no_instance()) { break; } if (reset_create_dialog) { create_dialog->set_base_type("Node"); reset_create_dialog = false; } Node *selected = scene_tree->get_selected(); if (!selected && !editor_selection->get_selected_node_list().is_empty()) { selected = editor_selection->get_selected_node_list().front()->get(); } if (selected) { create_dialog->popup_create(false, true, selected->get_class(), selected->get_name()); } } break; case TOOL_EXTEND_SCRIPT: { attach_script_to_selected(true); } break; case TOOL_ATTACH_SCRIPT: { attach_script_to_selected(false); } break; case TOOL_DETACH_SCRIPT: { if (!profile_allow_script_editing) { break; } Array selection = editor_selection->get_selected_nodes(); if (selection.is_empty()) { return; } EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Detach Script"), UndoRedo::MERGE_DISABLE, EditorNode::get_singleton()->get_edited_scene()); undo_redo->add_do_method(EditorNode::get_singleton(), "push_item", (Script *)nullptr); for (int i = 0; i < selection.size(); i++) { Node *n = Object::cast_to(selection[i]); Ref