diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index 11f44720dae3..c1edeeeb0ed7 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -87,6 +87,12 @@ void SceneTreeDock::_unhandled_key_input(Ref p_event) { _tool_selected(TOOL_INSTANCE); } 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/change_node_type", p_event)) { _tool_selected(TOOL_REPLACE); } else if (ED_IS_SHORTCUT("scene_tree/duplicate", p_event)) { @@ -397,6 +403,114 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) { tree->ensure_cursor_is_visible(); } break; + case TOOL_CUT: + case TOOL_COPY: { + if (!edited_scene || !_validate_no_foreign()) { + break; + } + + List selection = editor_selection->get_selected_node_list(); + if (selection.size() == 0) { + break; + } + + if (!node_clipboard.is_empty()) { + _clear_clipboard(); + } + clipboard_source_scene = editor->get_edited_scene()->get_filename(); + + selection.sort_custom(); + + for (List::Element *E = selection.front(); E; E = E->next()) { + Node *node = E->get(); + Map duplimap; + Node *dup = node->duplicate_from_editor(duplimap); + + ERR_CONTINUE(!dup); + + node_clipboard.push_back(dup); + } + + if (p_tool == TOOL_CUT) { + _delete_confirm(true); + } + } break; + case TOOL_PASTE: { + if (node_clipboard.is_empty() || !edited_scene) { + break; + } + + bool has_cycle = false; + if (edited_scene->get_filename() != String()) { + for (List::Element *E = node_clipboard.front(); E; E = E->next()) { + if (edited_scene->get_filename() == E->get()->get_filename()) { + has_cycle = true; + break; + } + } + } + + if (has_cycle) { + current_option = -1; + accept->set_text(TTR("Can't paste root node into the same scene.")); + accept->popup_centered(); + break; + } + + Node *paste_parent = edited_scene; + List selection = editor_selection->get_selected_node_list(); + if (selection.size() > 0) { + paste_parent = selection.back()->get(); + } + + Node *owner = paste_parent->get_owner(); + if (!owner) { + owner = paste_parent; + } + + editor_data->get_undo_redo().create_action(TTR("Paste Node(s)")); + editor_data->get_undo_redo().add_do_method(editor_selection, "clear"); + + Map resource_remap; + String target_scene = editor->get_edited_scene()->get_filename(); + if (target_scene != clipboard_source_scene) { + if (!clipboard_resource_remap.has(target_scene)) { + Map remap; + for (List::Element *E = node_clipboard.front(); E; E = E->next()) { + _create_remap_for_node(E->get(), remap); + } + clipboard_resource_remap[target_scene] = remap; + } + resource_remap = clipboard_resource_remap[target_scene]; + } + + for (List::Element *E = node_clipboard.front(); E; E = E->next()) { + Node *node = E->get(); + Map duplimap; + + Node *dup = node->duplicate_from_editor(duplimap, resource_remap); + + ERR_CONTINUE(!dup); + + editor_data->get_undo_redo().add_do_method(paste_parent, "add_child", dup); + + for (Map::Element *E2 = duplimap.front(); E2; E2 = E2->next()) { + Node *d = E2->value(); + editor_data->get_undo_redo().add_do_method(d, "set_owner", owner); + } + + editor_data->get_undo_redo().add_do_method(dup, "set_owner", owner); + editor_data->get_undo_redo().add_do_method(editor_selection, "add_node", dup); + editor_data->get_undo_redo().add_undo_method(paste_parent, "remove_child", dup); + editor_data->get_undo_redo().add_do_reference(dup); + + if (node_clipboard.size() == 1) { + editor_data->get_undo_redo().add_do_method(editor, "push_item", dup); + } + } + + editor_data->get_undo_redo().commit_action(); + } break; case TOOL_REPLACE: { if (!profile_allow_editing) { break; @@ -1795,7 +1909,7 @@ void SceneTreeDock::_toggle_editable_children(Node *p_node) { } } -void SceneTreeDock::_delete_confirm() { +void SceneTreeDock::_delete_confirm(bool p_cut) { List remove_list = editor_selection->get_selected_node_list(); if (remove_list.is_empty()) { @@ -1804,7 +1918,11 @@ void SceneTreeDock::_delete_confirm() { editor->get_editor_plugins_over()->make_visible(false); - editor_data->get_undo_redo().create_action(TTR("Remove Node(s)")); + if (p_cut) { + editor_data->get_undo_redo().create_action(TTR("Cut Node(s)")); + } else { + editor_data->get_undo_redo().create_action(TTR("Remove Node(s)")); + } bool entire_scene = false; @@ -2444,6 +2562,13 @@ void SceneTreeDock::_tree_rmb(const Vector2 &p_menu_pos) { } if (profile_allow_script_editing) { + menu->add_shortcut(ED_GET_SHORTCUT("scene_tree/cut_node"), TOOL_CUT); + menu->add_shortcut(ED_GET_SHORTCUT("scene_tree/copy_node"), TOOL_COPY); + if (selection.size() == 1 && !node_clipboard.is_empty()) { + menu->add_shortcut(ED_GET_SHORTCUT("scene_tree/paste_node"), TOOL_PASTE); + } + menu->add_separator(); + bool add_separator = false; if (full_selection.size() == 1) { @@ -2775,6 +2900,62 @@ void SceneTreeDock::_feature_profile_changed() { _update_script_button(); } +void SceneTreeDock::_clear_clipboard() { + for (List::Element *E = node_clipboard.front(); E; E = E->next()) { + memdelete(E->get()); + } + node_clipboard.clear(); + clipboard_resource_remap.clear(); +} + +void SceneTreeDock::_create_remap_for_node(Node *p_node, Map &r_remap) { + List props; + p_node->get_property_list(&props); + + for (List::Element *E = props.front(); E; E = E->next()) { + if (!(E->get().usage & PROPERTY_USAGE_STORAGE)) { + continue; + } + + Variant v = p_node->get(E->get().name); + if (v.is_ref()) { + RES res = v; + if (res.is_valid()) { + if (res->get_path() == "" && !r_remap.has(res)) { + _create_remap_for_resource(res, r_remap); + } + } + } + } + + for (int i = 0; i < p_node->get_child_count(); i++) { + _create_remap_for_node(p_node->get_child(i), r_remap); + } +} + +void SceneTreeDock::_create_remap_for_resource(RES p_resource, Map &r_remap) { + r_remap[p_resource] = p_resource->duplicate(); + + List props; + p_resource->get_property_list(&props); + + for (List::Element *E = props.front(); E; E = E->next()) { + if (!(E->get().usage & PROPERTY_USAGE_STORAGE)) { + continue; + } + + Variant v = p_resource->get(E->get().name); + if (v.is_ref()) { + RES res = v; + if (res.is_valid()) { + if (res->get_path() == "" && !r_remap.has(res)) { + _create_remap_for_resource(res, r_remap); + } + } + } + } +} + void SceneTreeDock::_bind_methods() { ClassDB::bind_method(D_METHOD("_set_owners"), &SceneTreeDock::_set_owners); ClassDB::bind_method(D_METHOD("_unhandled_key_input"), &SceneTreeDock::_unhandled_key_input); @@ -2806,6 +2987,9 @@ SceneTreeDock::SceneTreeDock(EditorNode *p_editor, Node *p_scene_root, EditorSel ED_SHORTCUT("scene_tree/add_child_node", TTR("Add Child Node"), KEY_MASK_CMD | KEY_A); ED_SHORTCUT("scene_tree/instance_scene", TTR("Instance Child Scene")); ED_SHORTCUT("scene_tree/expand_collapse_all", TTR("Expand/Collapse All")); + ED_SHORTCUT("scene_tree/cut_node", TTR("Cut"), KEY_MASK_CMD | KEY_X); + ED_SHORTCUT("scene_tree/copy_node", TTR("Copy"), KEY_MASK_CMD | KEY_C); + ED_SHORTCUT("scene_tree/paste_node", TTR("Paste"), KEY_MASK_CMD | KEY_V); ED_SHORTCUT("scene_tree/change_node_type", TTR("Change Type")); ED_SHORTCUT("scene_tree/attach_script", TTR("Attach Script")); ED_SHORTCUT("scene_tree/extend_script", TTR("Extend Script")); @@ -2818,7 +3002,7 @@ SceneTreeDock::SceneTreeDock(EditorNode *p_editor, Node *p_scene_root, EditorSel ED_SHORTCUT("scene_tree/make_root", TTR("Make Scene Root")); ED_SHORTCUT("scene_tree/merge_from_scene", TTR("Merge From Scene")); ED_SHORTCUT("scene_tree/save_branch_as_scene", TTR("Save Branch as Scene")); - ED_SHORTCUT("scene_tree/copy_node_path", TTR("Copy Node Path"), KEY_MASK_CMD | KEY_C); + ED_SHORTCUT("scene_tree/copy_node_path", TTR("Copy Node Path"), KEY_MASK_CMD | KEY_MASK_SHIFT | KEY_C); ED_SHORTCUT("scene_tree/delete_no_confirm", TTR("Delete (No Confirm)"), KEY_MASK_SHIFT | KEY_DELETE); ED_SHORTCUT("scene_tree/delete", TTR("Delete"), KEY_DELETE); @@ -2936,7 +3120,7 @@ SceneTreeDock::SceneTreeDock(EditorNode *p_editor, Node *p_scene_root, EditorSel delete_dialog = memnew(ConfirmationDialog); add_child(delete_dialog); - delete_dialog->connect("confirmed", callable_mp(this, &SceneTreeDock::_delete_confirm)); + delete_dialog->connect("confirmed", callable_mp(this, &SceneTreeDock::_delete_confirm), varray(false)); editable_instance_remove_dialog = memnew(ConfirmationDialog); add_child(editable_instance_remove_dialog); @@ -2981,3 +3165,9 @@ SceneTreeDock::SceneTreeDock(EditorNode *p_editor, Node *p_scene_root, EditorSel EDITOR_DEF("interface/editors/derive_script_globals_by_name", true); EDITOR_DEF("_use_favorites_root_selection", false); } + +SceneTreeDock::~SceneTreeDock() { + if (!node_clipboard.is_empty()) { + _clear_clipboard(); + } +} diff --git a/editor/scene_tree_dock.h b/editor/scene_tree_dock.h index 4f8d85f07cbd..a042188be6d5 100644 --- a/editor/scene_tree_dock.h +++ b/editor/scene_tree_dock.h @@ -58,6 +58,9 @@ class SceneTreeDock : public VBoxContainer { TOOL_NEW, TOOL_INSTANCE, TOOL_EXPAND_COLLAPSE, + TOOL_CUT, + TOOL_COPY, + TOOL_PASTE, TOOL_RENAME, TOOL_BATCH_RENAME, TOOL_REPLACE, @@ -126,6 +129,10 @@ class SceneTreeDock : public VBoxContainer { EditorData *editor_data; EditorSelection *editor_selection; + List node_clipboard; + String clipboard_source_scene; + HashMap> clipboard_resource_remap; + ScriptCreateDialog *script_create_dialog; AcceptDialog *accept; ConfirmationDialog *delete_dialog; @@ -183,7 +190,7 @@ class SceneTreeDock : public VBoxContainer { void _script_created(Ref