Browse Source

Project manager: Add option to backup project when it will be changed

David Snopek 5 months ago
parent
commit
66b40a710f

+ 88 - 7
editor/project_manager.cpp

@@ -52,6 +52,7 @@
 #include "editor/themes/editor_scale.h"
 #include "editor/themes/editor_theme_manager.h"
 #include "main/main.h"
+#include "scene/gui/check_box.h"
 #include "scene/gui/flow_container.h"
 #include "scene/gui/line_edit.h"
 #include "scene/gui/margin_container.h"
@@ -262,6 +263,7 @@ void ProjectManager::_update_theme(bool p_skip_creation) {
 			open_options_btn->set_button_icon(get_editor_theme_icon(SNAME("Collapse")));
 			run_btn->set_button_icon(get_editor_theme_icon(SNAME("Play")));
 			rename_btn->set_button_icon(get_editor_theme_icon(SNAME("Rename")));
+			duplicate_btn->set_button_icon(get_editor_theme_icon(SNAME("Duplicate")));
 			manage_tags_btn->set_button_icon(get_editor_theme_icon("Script"));
 			erase_btn->set_button_icon(get_editor_theme_icon(SNAME("Remove")));
 			erase_missing_btn->set_button_icon(get_editor_theme_icon(SNAME("Clear")));
@@ -276,6 +278,7 @@ void ProjectManager::_update_theme(bool p_skip_creation) {
 			open_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
 			run_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
 			rename_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
+			duplicate_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
 			manage_tags_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
 			erase_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
 			erase_missing_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
@@ -561,15 +564,16 @@ void ProjectManager::_open_selected_projects_check_warnings() {
 	const int config_version = project.version;
 	PackedStringArray unsupported_features = project.unsupported_features;
 
-	Label *ask_update_label = ask_update_settings->get_label();
 	ask_update_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_LEFT); // Reset in case of previous center align.
+	ask_update_backup->set_pressed(false);
 	full_convert_button->hide();
+	ask_update_backup->hide();
 
 	ask_update_settings->get_ok_button()->set_text("OK");
 
 	// Check if the config_version property was empty or 0.
 	if (config_version == 0) {
-		ask_update_settings->set_text(vformat(TTR("The selected project \"%s\" does not specify its supported Godot version in its configuration file (\"project.godot\").\n\nProject path: %s\n\nIf you proceed with opening it, it will be converted to Godot's current configuration file format.\n\nWarning: You won't be able to open the project with previous versions of the engine anymore."), project.project_name, project.path));
+		ask_update_label->set_text(vformat(TTR("The selected project \"%s\" does not specify its supported Godot version in its configuration file (\"project.godot\").\n\nProject path: %s\n\nIf you proceed with opening it, it will be converted to Godot's current configuration file format.\n\nWarning: You won't be able to open the project with previous versions of the engine anymore."), project.project_name, project.path));
 		ask_update_settings->popup_centered(popup_min_size);
 		return;
 	}
@@ -577,10 +581,12 @@ void ProjectManager::_open_selected_projects_check_warnings() {
 	if (config_version < ProjectSettings::CONFIG_VERSION) {
 		if (config_version == GODOT4_CONFIG_VERSION - 1 && ProjectSettings::CONFIG_VERSION == GODOT4_CONFIG_VERSION) { // Conversion from Godot 3 to 4.
 			full_convert_button->show();
-			ask_update_settings->set_text(vformat(TTR("The selected project \"%s\" was generated by Godot 3.x, and needs to be converted for Godot 4.x.\n\nProject path: %s\n\nYou have three options:\n- Convert only the configuration file (\"project.godot\"). Use this to open the project without attempting to convert its scenes, resources and scripts.\n- Convert the entire project including its scenes, resources and scripts (recommended if you are upgrading).\n- Do nothing and go back.\n\nWarning: If you select a conversion option, you won't be able to open the project with previous versions of the engine anymore."), project.project_name, project.path));
+			ask_update_backup->show();
+			ask_update_label->set_text(vformat(TTR("The selected project \"%s\" was generated by Godot 3.x, and needs to be converted for Godot 4.x.\n\nProject path: %s\n\nYou have three options:\n- Convert only the configuration file (\"project.godot\"). Use this to open the project without attempting to convert its scenes, resources and scripts.\n- Convert the entire project including its scenes, resources and scripts (recommended if you are upgrading).\n- Do nothing and go back.\n\nWarning: If you select a conversion option, you won't be able to open the project with previous versions of the engine anymore."), project.project_name, project.path));
 			ask_update_settings->get_ok_button()->set_text(TTRC("Convert project.godot Only"));
 		} else {
-			ask_update_settings->set_text(vformat(TTR("The selected project \"%s\" was generated by an older engine version, and needs to be converted for this version.\n\nProject path: %s\n\nDo you want to convert it?\n\nWarning: You won't be able to open the project with previous versions of the engine anymore."), project.project_name, project.path));
+			ask_update_backup->show();
+			ask_update_label->set_text(vformat(TTR("The selected project \"%s\" was generated by an older engine version, and needs to be converted for this version.\n\nProject path: %s\n\nDo you want to convert it?\n\nWarning: You won't be able to open the project with previous versions of the engine anymore."), project.project_name, project.path));
 			ask_update_settings->get_ok_button()->set_text(TTRC("Convert project.godot"));
 		}
 		ask_update_settings->popup_centered(popup_min_size);
@@ -598,6 +604,7 @@ void ProjectManager::_open_selected_projects_check_warnings() {
 		for (int i = 0; i < unsupported_features.size(); i++) {
 			const String &feature = unsupported_features[i];
 			if (feature == "Double Precision") {
+				ask_update_backup->show();
 				warning_message += TTR("Warning: This project uses double precision floats, but this version of\nGodot uses single precision floats. Opening this project may cause data loss.\n\n");
 				unsupported_features.remove_at(i);
 				i--;
@@ -606,6 +613,7 @@ void ProjectManager::_open_selected_projects_check_warnings() {
 				unsupported_features.remove_at(i);
 				i--;
 			} else if (ProjectList::project_feature_looks_like_version(feature)) {
+				ask_update_backup->show();
 				version_convert_feature = feature;
 				warning_message += vformat(TTR("Warning: This project was last edited in Godot %s. Opening will change it to Godot %s.\n\n"), Variant(feature), Variant(GODOT_VERSION_BRANCH));
 				unsupported_features.remove_at(i);
@@ -618,7 +626,7 @@ void ProjectManager::_open_selected_projects_check_warnings() {
 		}
 		warning_message += TTR("Open anyway? Project will be modified.");
 		ask_update_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
-		ask_update_settings->set_text(warning_message);
+		ask_update_label->set_text(warning_message);
 		ask_update_settings->popup_centered(popup_min_size);
 		return;
 	}
@@ -651,6 +659,14 @@ void ProjectManager::_open_selected_projects_check_recovery_mode() {
 }
 
 void ProjectManager::_open_selected_projects_with_migration() {
+	if (ask_update_backup->is_pressed() && project_list->get_selected_projects().size() == 1) {
+		ask_update_settings->hide();
+		ask_update_backup->set_pressed(false);
+
+		_duplicate_project_with_action(POST_DUPLICATE_ACTION_OPEN);
+		return;
+	}
+
 #ifndef DISABLE_DEPRECATED
 	if (project_list->get_selected_projects().size() == 1) {
 		// Only migrate if a single project is opened.
@@ -692,6 +708,27 @@ void ProjectManager::_rename_project() {
 	}
 }
 
+void ProjectManager::_duplicate_project() {
+	_duplicate_project_with_action(POST_DUPLICATE_ACTION_NONE);
+}
+
+void ProjectManager::_duplicate_project_with_action(PostDuplicateAction p_post_action) {
+	Vector<ProjectList::Item> selected_projects = project_list->get_selected_projects();
+	if (selected_projects.is_empty()) {
+		return;
+	}
+
+	post_duplicate_action = p_post_action;
+
+	const ProjectList::Item &project = selected_projects[0];
+
+	project_dialog->set_mode(ProjectDialog::MODE_DUPLICATE);
+	project_dialog->set_project_name(vformat("%s (%s)", project.project_name, p_post_action == POST_DUPLICATE_ACTION_NONE ? "Copy" : project.project_version));
+	project_dialog->set_original_project_path(project.path);
+	project_dialog->set_duplicate_can_edit(p_post_action == POST_DUPLICATE_ACTION_NONE);
+	project_dialog->show_dialog(false);
+}
+
 void ProjectManager::_erase_project() {
 	const HashSet<String> &selected_list = project_list->get_selected_project_keys();
 
@@ -744,6 +781,7 @@ void ProjectManager::_update_project_buttons() {
 	open_btn->set_disabled(empty_selection || is_missing_project_selected);
 	open_options_btn->set_disabled(empty_selection || is_missing_project_selected);
 	rename_btn->set_disabled(empty_selection || is_missing_project_selected);
+	duplicate_btn->set_disabled(empty_selection || is_missing_project_selected);
 	manage_tags_btn->set_disabled(empty_selection || is_missing_project_selected || selected_projects.size() > 1);
 	run_btn->set_disabled(empty_selection || is_missing_project_selected);
 
@@ -836,6 +874,25 @@ void ProjectManager::_on_project_created(const String &dir, bool edit) {
 	project_list->update_dock_menu();
 }
 
+void ProjectManager::_on_project_duplicated(const String &p_original_path, const String &p_duplicate_path, bool p_edit) {
+	if (post_duplicate_action == POST_DUPLICATE_ACTION_NONE) {
+		_on_project_created(p_duplicate_path, p_edit);
+	} else {
+		project_list->add_project(p_duplicate_path, false);
+		project_list->save_config();
+
+		if (post_duplicate_action == POST_DUPLICATE_ACTION_OPEN) {
+			_open_selected_projects_with_migration();
+		} else if (post_duplicate_action == POST_DUPLICATE_ACTION_FULL_CONVERSION) {
+			_full_convert_button_pressed();
+		}
+
+		project_list->update_dock_menu();
+	}
+
+	post_duplicate_action = POST_DUPLICATE_ACTION_NONE;
+}
+
 void ProjectManager::_on_order_option_changed(int p_idx) {
 	if (is_inside_tree()) {
 		project_list->set_order_option(p_idx);
@@ -1019,6 +1076,14 @@ void ProjectManager::_minor_project_migrate() {
 
 void ProjectManager::_full_convert_button_pressed() {
 	ask_update_settings->hide();
+
+	if (ask_update_backup->is_pressed()) {
+		ask_update_backup->set_pressed(false);
+
+		_duplicate_project_with_action(POST_DUPLICATE_ACTION_FULL_CONVERSION);
+		return;
+	}
+
 	ask_full_convert_dialog->popup_centered(Size2i(600.0 * EDSCALE, 0));
 	ask_full_convert_dialog->get_cancel_button()->grab_focus();
 }
@@ -1525,6 +1590,11 @@ ProjectManager::ProjectManager() {
 			rename_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_rename_project));
 			project_list_sidebar->add_child(rename_btn);
 
+			duplicate_btn = memnew(Button);
+			duplicate_btn->set_text(TTRC("Duplicate"));
+			duplicate_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_duplicate_project));
+			project_list_sidebar->add_child(duplicate_btn);
+
 			manage_tags_btn = memnew(Button);
 			manage_tags_btn->set_text(TTRC("Manage Tags"));
 			manage_tags_btn->set_shortcut(ED_SHORTCUT("project_manager/project_tags", TTRC("Manage Tags"), KeyModifierMask::CMD_OR_CTRL | Key::T));
@@ -1636,7 +1706,18 @@ ProjectManager::ProjectManager() {
 		add_child(open_recovery_mode_ask);
 
 		ask_update_settings = memnew(ConfirmationDialog);
-		ask_update_settings->set_autowrap(true);
+		add_child(ask_update_settings);
+		ask_update_vb = memnew(VBoxContainer);
+		ask_update_settings->add_child(ask_update_vb);
+		ask_update_label = memnew(Label);
+		ask_update_label->set_custom_minimum_size(Size2(300 * EDSCALE, 1));
+		ask_update_label->set_autowrap_mode(TextServer::AUTOWRAP_WORD);
+		ask_update_label->set_v_size_flags(SIZE_EXPAND_FILL);
+		ask_update_vb->add_child(ask_update_label);
+		ask_update_backup = memnew(CheckBox);
+		ask_update_backup->set_text(TTRC("Backup project first"));
+		ask_update_backup->set_h_size_flags(SIZE_SHRINK_CENTER);
+		ask_update_vb->add_child(ask_update_backup);
 		ask_update_settings->get_ok_button()->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_open_selected_projects_with_migration));
 		int ed_swap_cancel_ok = EDITOR_GET("interface/editor/accept_dialog_cancel_ok_buttons");
 		if (ed_swap_cancel_ok == 0) {
@@ -1644,7 +1725,6 @@ ProjectManager::ProjectManager() {
 		}
 		full_convert_button = ask_update_settings->add_button(TTRC("Convert Full Project"), ed_swap_cancel_ok != 2);
 		full_convert_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_full_convert_button_pressed));
-		add_child(ask_update_settings);
 
 		ask_full_convert_dialog = memnew(ConfirmationDialog);
 		ask_full_convert_dialog->set_autowrap(true);
@@ -1655,6 +1735,7 @@ ProjectManager::ProjectManager() {
 		project_dialog = memnew(ProjectDialog);
 		project_dialog->connect("projects_updated", callable_mp(this, &ProjectManager::_on_projects_updated));
 		project_dialog->connect("project_created", callable_mp(this, &ProjectManager::_on_project_created));
+		project_dialog->connect("project_duplicated", callable_mp(this, &ProjectManager::_on_project_duplicated));
 		add_child(project_dialog);
 
 		error_dialog = memnew(AcceptDialog);

+ 15 - 0
editor/project_manager.h

@@ -65,6 +65,14 @@ class ProjectManager : public Control {
 
 	void _build_icon_type_cache(Ref<Theme> p_theme);
 
+	enum PostDuplicateAction {
+		POST_DUPLICATE_ACTION_NONE,
+		POST_DUPLICATE_ACTION_OPEN,
+		POST_DUPLICATE_ACTION_FULL_CONVERSION,
+	};
+
+	PostDuplicateAction post_duplicate_action = POST_DUPLICATE_ACTION_NONE;
+
 	// Main layout.
 
 	Ref<Theme> theme;
@@ -150,6 +158,7 @@ class ProjectManager : public Control {
 	Button *open_options_btn = nullptr;
 	Button *run_btn = nullptr;
 	Button *rename_btn = nullptr;
+	Button *duplicate_btn = nullptr;
 	Button *manage_tags_btn = nullptr;
 	Button *erase_btn = nullptr;
 	Button *erase_missing_btn = nullptr;
@@ -183,6 +192,8 @@ class ProjectManager : public Control {
 	void _import_project();
 	void _new_project();
 	void _rename_project();
+	void _duplicate_project();
+	void _duplicate_project_with_action(PostDuplicateAction p_action);
 	void _erase_project();
 	void _erase_missing_projects();
 	void _erase_project_confirm();
@@ -192,6 +203,7 @@ class ProjectManager : public Control {
 	void _open_recovery_mode_ask(bool manual = false);
 
 	void _on_project_created(const String &dir, bool edit);
+	void _on_project_duplicated(const String &p_original_path, const String &p_duplicate_path, bool p_edit);
 	void _on_projects_updated();
 	void _on_open_options_selected(int p_option);
 	void _on_recovery_mode_popup_open_normal();
@@ -228,6 +240,9 @@ class ProjectManager : public Control {
 
 	ConfirmationDialog *ask_full_convert_dialog = nullptr;
 	ConfirmationDialog *ask_update_settings = nullptr;
+	VBoxContainer *ask_update_vb = nullptr;
+	Label *ask_update_label = nullptr;
+	CheckBox *ask_update_backup = nullptr;
 	Button *full_convert_button = nullptr;
 
 	String version_convert_feature;

+ 63 - 18
editor/project_manager/project_dialog.cpp

@@ -199,7 +199,7 @@ void ProjectDialog::_validate_path() {
 	}
 
 	is_folder_empty = true;
-	if (mode == MODE_NEW || mode == MODE_INSTALL || (mode == MODE_IMPORT && target_path_input_type == InputType::INSTALL_PATH)) {
+	if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE || (mode == MODE_IMPORT && target_path_input_type == InputType::INSTALL_PATH)) {
 		if (create_dir->is_pressed()) {
 			if (!d->dir_exists(target_path.get_base_dir())) {
 				_set_message(TTRC("The parent directory of the path specified doesn't exist."), MESSAGE_ERROR, target_path_input_type);
@@ -247,7 +247,7 @@ void ProjectDialog::_validate_path() {
 }
 
 String ProjectDialog::_get_target_path() {
-	if (mode == MODE_NEW || mode == MODE_INSTALL) {
+	if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {
 		return project_path->get_text();
 	} else if (mode == MODE_IMPORT) {
 		return install_path->get_text();
@@ -256,7 +256,7 @@ String ProjectDialog::_get_target_path() {
 	}
 }
 void ProjectDialog::_set_target_path(const String &p_text) {
-	if (mode == MODE_NEW || mode == MODE_INSTALL) {
+	if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {
 		project_path->set_text(p_text);
 	} else if (mode == MODE_IMPORT) {
 		install_path->set_text(p_text);
@@ -267,7 +267,7 @@ void ProjectDialog::_set_target_path(const String &p_text) {
 
 void ProjectDialog::_update_target_auto_dir() {
 	String new_auto_dir;
-	if (mode == MODE_NEW || mode == MODE_INSTALL) {
+	if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {
 		new_auto_dir = project_name->get_text();
 	} else if (mode == MODE_IMPORT) {
 		new_auto_dir = project_path->get_text().get_file().get_basename();
@@ -338,7 +338,7 @@ void ProjectDialog::_create_dir_toggled(bool p_pressed) {
 }
 
 void ProjectDialog::_project_name_changed() {
-	if (mode == MODE_NEW || mode == MODE_INSTALL) {
+	if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {
 		_update_target_auto_dir();
 	}
 
@@ -365,7 +365,7 @@ void ProjectDialog::_browse_project_path() {
 	if (mode == MODE_IMPORT && install_path->is_visible_in_tree()) {
 		// Select last ZIP file.
 		fdialog_project->set_current_path(path);
-	} else if ((mode == MODE_NEW || mode == MODE_INSTALL) && create_dir->is_pressed()) {
+	} else if ((mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) && create_dir->is_pressed()) {
 		// Select parent directory of project path.
 		fdialog_project->set_current_dir(path.get_base_dir());
 	} else {
@@ -408,7 +408,7 @@ void ProjectDialog::_browse_install_path() {
 void ProjectDialog::_project_path_selected(const String &p_path) {
 	show_dialog(false);
 
-	if (create_dir->is_pressed() && (mode == MODE_NEW || mode == MODE_INSTALL)) {
+	if (create_dir->is_pressed() && (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE)) {
 		// Replace parent directory, but keep target dir name.
 		project_path->set_text(p_path.path_join(project_path->get_text().get_file()));
 	} else {
@@ -704,7 +704,20 @@ void ProjectDialog::ok_pressed() {
 		} break;
 	}
 
-	if (mode == MODE_RENAME || mode == MODE_INSTALL) {
+	if (mode == MODE_DUPLICATE) {
+		Ref<DirAccess> dir = DirAccess::open(original_project_path);
+		Error err = FAILED;
+		if (dir.is_valid()) {
+			err = dir->copy_dir(".", path, -1, true);
+		}
+		if (err != OK) {
+			dialog_error->set_text(vformat(TTR("Couldn't duplicate project (error %d)."), err));
+			dialog_error->popup_centered();
+			return;
+		}
+	}
+
+	if (mode == MODE_RENAME || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {
 		// Load project.godot as ConfigFile to set the new name.
 		ConfigFile cfg;
 		String project_godot = path.path_join("project.godot");
@@ -737,6 +750,8 @@ void ProjectDialog::ok_pressed() {
 		}
 #endif
 		emit_signal(SNAME("project_created"), path, edit_check_box->is_pressed());
+	} else if (mode == MODE_DUPLICATE) {
+		emit_signal(SNAME("project_duplicated"), original_project_path, path, edit_check_box->is_visible() && edit_check_box->is_pressed());
 	} else if (mode == MODE_RENAME) {
 		emit_signal(SNAME("projects_updated"));
 	}
@@ -750,6 +765,14 @@ void ProjectDialog::set_zip_title(const String &p_title) {
 	zip_title = p_title;
 }
 
+void ProjectDialog::set_original_project_path(const String &p_path) {
+	original_project_path = p_path;
+}
+
+void ProjectDialog::set_duplicate_can_edit(bool p_duplicate_can_edit) {
+	duplicate_can_edit = p_duplicate_can_edit;
+}
+
 void ProjectDialog::set_mode(Mode p_mode) {
 	mode = p_mode;
 }
@@ -793,17 +816,24 @@ void ProjectDialog::show_dialog(bool p_reset_name) {
 		}
 		project_path->set_editable(true);
 
-		String fav_dir = EDITOR_GET("filesystem/directories/default_project_path");
-		fav_dir = fav_dir.simplify_path();
-		if (!fav_dir.is_empty()) {
-			project_path->set_text(fav_dir);
-			install_path->set_text(fav_dir);
-			fdialog_project->set_current_dir(fav_dir);
+		if (mode == MODE_DUPLICATE) {
+			String original_dir = original_project_path.get_base_dir();
+			project_path->set_text(original_dir);
+			install_path->set_text(original_dir);
+			fdialog_project->set_current_dir(original_dir);
 		} else {
-			Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
-			project_path->set_text(d->get_current_dir());
-			install_path->set_text(d->get_current_dir());
-			fdialog_project->set_current_dir(d->get_current_dir());
+			String fav_dir = EDITOR_GET("filesystem/directories/default_project_path");
+			fav_dir = fav_dir.simplify_path();
+			if (!fav_dir.is_empty()) {
+				project_path->set_text(fav_dir);
+				install_path->set_text(fav_dir);
+				fdialog_project->set_current_dir(fav_dir);
+			} else {
+				Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+				project_path->set_text(d->get_current_dir());
+				install_path->set_text(d->get_current_dir());
+				fdialog_project->set_current_dir(d->get_current_dir());
+			}
 		}
 
 		create_dir->show();
@@ -844,6 +874,20 @@ void ProjectDialog::show_dialog(bool p_reset_name) {
 			default_files_container->hide();
 
 			callable_mp((Control *)project_path, &Control::grab_focus).call_deferred();
+		} else if (mode == MODE_DUPLICATE) {
+			set_title(TTRC("Duplicate Project"));
+			set_ok_button_text(TTRC("Duplicate"));
+
+			name_container->show();
+			install_path_container->hide();
+			renderer_container->hide();
+			default_files_container->hide();
+			if (!duplicate_can_edit) {
+				edit_check_box->hide();
+			}
+
+			callable_mp((Control *)project_name, &Control::grab_focus).call_deferred();
+			callable_mp(project_name, &LineEdit::select_all).call_deferred();
 		}
 
 		auto_dir = "";
@@ -885,6 +929,7 @@ void ProjectDialog::_notification(int p_what) {
 
 void ProjectDialog::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("project_created"));
+	ADD_SIGNAL(MethodInfo("project_duplicated"));
 	ADD_SIGNAL(MethodInfo("projects_updated"));
 }
 

+ 6 - 0
editor/project_manager/project_dialog.h

@@ -49,6 +49,7 @@ public:
 		MODE_IMPORT,
 		MODE_INSTALL,
 		MODE_RENAME,
+		MODE_DUPLICATE,
 	};
 
 private:
@@ -99,6 +100,9 @@ private:
 	String zip_path;
 	String zip_title;
 
+	String original_project_path;
+	bool duplicate_can_edit = false;
+
 	void _set_message(const String &p_msg, MessageType p_type, InputType input_type = PROJECT_PATH);
 	void _validate_path();
 
@@ -143,6 +147,8 @@ public:
 	void set_project_path(const String &p_path);
 	void set_zip_path(const String &p_path);
 	void set_zip_title(const String &p_title);
+	void set_original_project_path(const String &p_path);
+	void set_duplicate_can_edit(bool p_duplicate_can_edit);
 
 	void ask_for_path_and_show();
 	void show_dialog(bool p_reset_name = true);