diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 1b5e17fce4..7dfa9fc913 100644 --- a/editor/project_manager.cpp +++ b/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 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 &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); diff --git a/editor/project_manager.h b/editor/project_manager.h index 96a35b7ad4..438b62561f 100644 --- a/editor/project_manager.h +++ b/editor/project_manager.h @@ -65,6 +65,14 @@ class ProjectManager : public Control { void _build_icon_type_cache(Ref 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; @@ -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; diff --git a/editor/project_manager/project_dialog.cpp b/editor/project_manager/project_dialog.cpp index 0a18174a7e..4734112df4 100644 --- a/editor/project_manager/project_dialog.cpp +++ b/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 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 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 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")); } diff --git a/editor/project_manager/project_dialog.h b/editor/project_manager/project_dialog.h index 8f98a98580..1f48cd6228 100644 --- a/editor/project_manager/project_dialog.h +++ b/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);