From 2130f1121a4d4bbc8c812ac6bea78cae1203c7ad Mon Sep 17 00:00:00 2001 From: Nathan Franke Date: Thu, 22 Feb 2024 07:27:04 -0600 Subject: [PATCH] Automatically create folder in project manager create/import/install --- doc/classes/EditorSettings.xml | 3 + editor/editor_settings.cpp | 1 + editor/project_manager.cpp | 7 +- editor/project_manager/project_dialog.cpp | 1047 ++++++++++----------- editor/project_manager/project_dialog.h | 58 +- 5 files changed, 565 insertions(+), 551 deletions(-) diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index 923c22e871e5..1c69c4878652 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -883,6 +883,9 @@ The renderer type that will be checked off by default when creating a new project. Accepted strings are "forward_plus", "mobile" or "gl_compatibility". + + Directory naming convention for the project manager. Options are "No convention" (project name is directory name), "kebab-case" (default), "snake_case", "camelCase", "PascalCase", or "Title Case". + The sorting order to use in the project manager. When changing the sorting order in the project manager, this setting is set permanently in the editor settings. diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index d0b633b1362d..29652c04b55a 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -838,6 +838,7 @@ void EditorSettings::_load_defaults(Ref p_extra_config) { // TRANSLATORS: Project Manager here refers to the tool used to create/manage Godot projects. EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "project_manager/sorting_order", 0, "Last Edited,Name,Path") + EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "project_manager/directory_naming_convention", 1, "No convention,kebab-case,snake_case,camelCase,PascalCase,Title Case") #if defined(WEB_ENABLED) // Web platform only supports `gl_compatibility`. diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 4187bf5a325e..b951e9453e64 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -618,14 +618,15 @@ void ProjectManager::_new_project() { } void ProjectManager::_rename_project() { - const HashSet &selected_list = project_list->get_selected_project_keys(); + const Vector &selected_list = project_list->get_selected_projects(); if (selected_list.size() == 0) { return; } - for (const String &E : selected_list) { - project_dialog->set_project_path(E); + for (const ProjectList::Item &E : selected_list) { + project_dialog->set_project_name(E.project_name); + project_dialog->set_project_path(E.path); project_dialog->set_mode(ProjectDialog::MODE_RENAME); project_dialog->show_dialog(); } diff --git a/editor/project_manager/project_dialog.cpp b/editor/project_manager/project_dialog.cpp index 3af9509bbe22..350bb5bb9f93 100644 --- a/editor/project_manager/project_dialog.cpp +++ b/editor/project_manager/project_dialog.cpp @@ -41,349 +41,382 @@ #include "editor/themes/editor_icons.h" #include "editor/themes/editor_scale.h" #include "scene/gui/check_box.h" +#include "scene/gui/check_button.h" #include "scene/gui/line_edit.h" #include "scene/gui/option_button.h" #include "scene/gui/separator.h" #include "scene/gui/texture_rect.h" -void ProjectDialog::_set_message(const String &p_msg, MessageType p_type, InputType input_type) { +void ProjectDialog::_set_message(const String &p_msg, MessageType p_type, InputType p_input_type) { msg->set_text(p_msg); - Ref current_path_icon = status_rect->get_texture(); - Ref current_install_icon = install_status_rect->get_texture(); - Ref new_icon; + get_ok_button()->set_disabled(p_type == MESSAGE_ERROR); + Ref new_icon; switch (p_type) { case MESSAGE_ERROR: { msg->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), EditorStringName(Editor))); - msg->set_modulate(Color(1, 1, 1, 1)); new_icon = get_editor_theme_icon(SNAME("StatusError")); - } break; case MESSAGE_WARNING: { msg->add_theme_color_override("font_color", get_theme_color(SNAME("warning_color"), EditorStringName(Editor))); - msg->set_modulate(Color(1, 1, 1, 1)); new_icon = get_editor_theme_icon(SNAME("StatusWarning")); - } break; case MESSAGE_SUCCESS: { - msg->remove_theme_color_override("font_color"); - msg->set_modulate(Color(1, 1, 1, 0)); + msg->add_theme_color_override("font_color", get_theme_color(SNAME("success_color"), EditorStringName(Editor))); new_icon = get_editor_theme_icon(SNAME("StatusSuccess")); - } break; } - if (current_path_icon != new_icon && input_type == PROJECT_PATH) { - status_rect->set_texture(new_icon); - } else if (current_install_icon != new_icon && input_type == INSTALL_PATH) { + if (p_input_type == PROJECT_PATH) { + project_status_rect->set_texture(new_icon); + } else if (p_input_type == INSTALL_PATH) { install_status_rect->set_texture(new_icon); } } static bool is_zip_file(Ref p_d, const String &p_path) { - return p_path.ends_with(".zip") && p_d->file_exists(p_path); + return p_path.get_extension() == "zip" && p_d->file_exists(p_path); } -String ProjectDialog::_test_path() { +void ProjectDialog::_validate_path() { + _set_message("", MESSAGE_SUCCESS, PROJECT_PATH); + _set_message("", MESSAGE_SUCCESS, INSTALL_PATH); + + if (project_name->get_text().strip_edges().is_empty()) { + _set_message(TTR("It would be a good idea to name your project."), MESSAGE_ERROR); + return; + } + Ref d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - const String base_path = project_path->get_text(); - String valid_path, valid_install_path; - bool is_zip = false; - if (d->change_dir(base_path) == OK) { - valid_path = base_path; - } else if (is_zip_file(d, base_path)) { - valid_path = base_path; - is_zip = true; - } else if (d->change_dir(base_path.strip_edges()) == OK) { - valid_path = base_path.strip_edges(); - } else if (is_zip_file(d, base_path.strip_edges())) { - valid_path = base_path.strip_edges(); - is_zip = true; - } - - if (valid_path.is_empty()) { - _set_message(TTR("The path specified doesn't exist."), MESSAGE_ERROR); - get_ok_button()->set_disabled(true); - return ""; - } - - if (mode == MODE_IMPORT && is_zip) { - if (d->change_dir(install_path->get_text()) == OK) { - valid_install_path = install_path->get_text(); - } else if (d->change_dir(install_path->get_text().strip_edges()) == OK) { - valid_install_path = install_path->get_text().strip_edges(); + String path = project_path->get_text().simplify_path(); + + String target_path = path; + InputType target_path_input_type = PROJECT_PATH; + + if (mode == MODE_IMPORT) { + if (path.get_file().strip_edges() == "project.godot") { + path = path.get_base_dir(); + project_path->set_text(path); } - if (valid_install_path.is_empty()) { - _set_message(TTR("The install path specified doesn't exist."), MESSAGE_ERROR, INSTALL_PATH); - get_ok_button()->set_disabled(true); - return ""; + if (is_zip_file(d, path)) { + zip_path = path; + } else if (is_zip_file(d, path.strip_edges())) { + zip_path = path.strip_edges(); + } else { + zip_path = ""; } - } - if (mode == MODE_IMPORT || mode == MODE_RENAME) { - if (!d->file_exists("project.godot")) { - if (is_zip) { - Ref io_fa; - zlib_filefunc_def io = zipio_create_io(&io_fa); + if (!zip_path.is_empty()) { + target_path = install_path->get_text().simplify_path(); + target_path_input_type = INSTALL_PATH; - unzFile pkg = unzOpen2(valid_path.utf8().get_data(), &io); - if (!pkg) { - _set_message(TTR("Error opening package file (it's not in ZIP format)."), MESSAGE_ERROR); - get_ok_button()->set_disabled(true); - unzClose(pkg); - return ""; - } + create_dir->show(); + install_path_container->show(); - int ret = unzGoToFirstFile(pkg); - while (ret == UNZ_OK) { - unz_file_info info; - char fname[16384]; - ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); - if (ret != UNZ_OK) { - break; - } + Ref io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); - if (String::utf8(fname).ends_with("project.godot")) { - break; - } + unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io); + if (!pkg) { + _set_message(TTR("Invalid \".zip\" project file; it is not in ZIP format."), MESSAGE_ERROR); + unzClose(pkg); + return; + } - ret = unzGoToNextFile(pkg); - } + int ret = unzGoToFirstFile(pkg); + while (ret == UNZ_OK) { + unz_file_info info; + char fname[16384]; + ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); + ERR_FAIL_COND_MSG(ret != UNZ_OK, "Failed to get current file info."); - if (ret == UNZ_END_OF_LIST_OF_FILE) { - _set_message(TTR("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR); - get_ok_button()->set_disabled(true); - unzClose(pkg); - return ""; + String name = String::utf8(fname); + if (name.get_file() == "project.godot") { + break; // ret == UNZ_OK. } + ret = unzGoToNextFile(pkg); + } + + if (ret == UNZ_END_OF_LIST_OF_FILE) { + _set_message(TTR("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR); unzClose(pkg); + return; + } - // check if the specified install folder is empty, even though this is not an error, it is good to check here - d->list_dir_begin(); - is_folder_empty = true; - String n = d->get_next(); - while (!n.is_empty()) { - if (!n.begins_with(".")) { - // Allow `.`, `..` (reserved current/parent folder names) - // and hidden files/folders to be present. - // For instance, this lets users initialize a Git repository - // and still be able to create a project in the directory afterwards. - is_folder_empty = false; - break; - } - n = d->get_next(); - } - d->list_dir_end(); + unzClose(pkg); + } else if (d->dir_exists(path) && d->file_exists(path.path_join("project.godot"))) { + zip_path = ""; - if (!is_folder_empty) { - _set_message(TTR("Please choose an empty install folder."), MESSAGE_WARNING, INSTALL_PATH); - get_ok_button()->set_disabled(true); - return ""; - } + create_dir->hide(); + install_path_container->hide(); - } else { - _set_message(TTR("Please choose a \"project.godot\", a directory with it, or a \".zip\" file."), MESSAGE_ERROR); - install_path_container->hide(); - get_ok_button()->set_disabled(true); - return ""; - } + _set_message(TTR("Valid project found at path."), MESSAGE_SUCCESS); + } else { + create_dir->hide(); + install_path_container->hide(); - } else if (is_zip) { - _set_message(TTR("The install directory already contains a Godot project."), MESSAGE_ERROR, INSTALL_PATH); - get_ok_button()->set_disabled(true); - return ""; + _set_message(TTR("Please choose a \"project.godot\", a directory with one, or a \".zip\" file."), MESSAGE_ERROR); + return; } + } - } else { - // Check if the specified folder is empty, even though this is not an error, it is good to check here. - d->list_dir_begin(); - is_folder_empty = true; - String n = d->get_next(); - while (!n.is_empty()) { - if (!n.begins_with(".")) { - // Allow `.`, `..` (reserved current/parent folder names) - // and hidden files/folders to be present. - // For instance, this lets users initialize a Git repository - // and still be able to create a project in the directory afterwards. - is_folder_empty = false; - break; - } - n = d->get_next(); - } - d->list_dir_end(); + if (target_path.is_empty() || target_path.is_relative_path()) { + _set_message(TTR("The path specified is invalid."), MESSAGE_ERROR, target_path_input_type); + return; + } - if (!is_folder_empty) { - if (valid_path == OS::get_singleton()->get_environment("HOME") || valid_path == OS::get_singleton()->get_system_dir(OS::SYSTEM_DIR_DOCUMENTS) || valid_path == OS::get_singleton()->get_executable_path().get_base_dir()) { - _set_message(TTR("You cannot save a project in the selected path. Please make a new folder or choose a new path."), MESSAGE_ERROR); - get_ok_button()->set_disabled(true); - return ""; - } + if (target_path.get_file() != OS::get_singleton()->get_safe_dir_name(target_path.get_file())) { + _set_message(TTR("The directory name specified contains invalid characters or trailing whitespace."), MESSAGE_ERROR, target_path_input_type); + return; + } - _set_message(TTR("The selected path is not empty. Choosing an empty folder is highly recommended."), MESSAGE_WARNING); - get_ok_button()->set_disabled(false); - return valid_path; - } + String working_dir = d->get_current_dir(); + String executable_dir = OS::get_singleton()->get_executable_path().get_base_dir(); + if (target_path == working_dir || target_path == executable_dir) { + _set_message(TTR("Creating a project at the engine's working directory or executable directory is not allowed, as it would prevent the project manager from starting."), MESSAGE_ERROR, target_path_input_type); + return; } - _set_message(""); - _set_message("", MESSAGE_SUCCESS, INSTALL_PATH); - get_ok_button()->set_disabled(false); - return valid_path; -} + // TODO: The following 5 lines could be simplified if OS.get_user_home_dir() or SYSTEM_DIR_HOME is implemented. See: https://github.com/godotengine/godot-proposals/issues/4851. +#ifdef WINDOWS_ENABLED + String home_dir = OS::get_singleton()->get_environment("USERPROFILE"); +#else + String home_dir = OS::get_singleton()->get_environment("HOME"); +#endif + String documents_dir = OS::get_singleton()->get_system_dir(OS::SYSTEM_DIR_DOCUMENTS); + if (target_path == home_dir || target_path == documents_dir) { + _set_message(TTR("You cannot save a project at the selected path. Please create a subfolder or choose a new path."), MESSAGE_ERROR, target_path_input_type); + return; + } -void ProjectDialog::_update_path(const String &p_path) { - String sp = _test_path(); - if (!sp.is_empty()) { - // If the project name is empty or default, infer the project name from the selected folder name - if (project_name->get_text().strip_edges().is_empty() || project_name->get_text().strip_edges() == TTR("New Game Project")) { - sp = sp.replace("\\", "/"); - int lidx = sp.rfind("/"); + is_folder_empty = true; + if (mode == MODE_NEW || mode == MODE_INSTALL || (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(TTR("The parent directory of the path specified doesn't exist."), MESSAGE_ERROR, target_path_input_type); + return; + } - if (lidx != -1) { - sp = sp.substr(lidx + 1, sp.length()).capitalize(); + if (d->dir_exists(target_path)) { + // The path is not necessarily empty here, but we will update the message later if it isn't. + _set_message(TTR("The project folder already exists and is empty."), MESSAGE_SUCCESS, target_path_input_type); + } else { + _set_message(TTR("The project folder will be automatically created."), MESSAGE_SUCCESS, target_path_input_type); + } + } else { + if (!d->dir_exists(target_path)) { + _set_message(TTR("The path specified doesn't exist."), MESSAGE_ERROR, target_path_input_type); + return; } - if (sp.is_empty() && mode == MODE_IMPORT) { - sp = TTR("Imported Project"); + + // The path is not necessarily empty here, but we will update the message later if it isn't. + _set_message(TTR("The project folder exists and is empty."), MESSAGE_SUCCESS, target_path_input_type); + } + + // Check if the directory is empty. Not an error, but we want to warn the user. + if (d->change_dir(target_path) == OK) { + d->list_dir_begin(); + String n = d->get_next(); + while (!n.is_empty()) { + if (n[0] != '.') { + // Allow `.`, `..` (reserved current/parent folder names) + // and hidden files/folders to be present. + // For instance, this lets users initialize a Git repository + // and still be able to create a project in the directory afterwards. + is_folder_empty = false; + break; + } + n = d->get_next(); } + d->list_dir_end(); - project_name->set_text(sp); - _text_changed(sp); + if (!is_folder_empty) { + _set_message(TTR("The selected path is not empty. Choosing an empty folder is highly recommended."), MESSAGE_WARNING, target_path_input_type); + } } } +} - if (!created_folder_path.is_empty() && created_folder_path != p_path) { - _remove_created_folder(); +String ProjectDialog::_get_target_path() { + if (mode == MODE_NEW || mode == MODE_INSTALL) { + return project_path->get_text(); + } else if (mode == MODE_IMPORT) { + return install_path->get_text(); + } else { + ERR_FAIL_V(""); } } - -void ProjectDialog::_path_text_changed(const String &p_path) { - Ref d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (mode == MODE_IMPORT && is_zip_file(d, p_path)) { - install_path->set_text(p_path.get_base_dir()); - install_path_container->show(); - } else if (mode == MODE_IMPORT && is_zip_file(d, p_path.strip_edges())) { - install_path->set_text(p_path.strip_edges().get_base_dir()); - install_path_container->show(); +void ProjectDialog::_set_target_path(const String &p_text) { + if (mode == MODE_NEW || mode == MODE_INSTALL) { + project_path->set_text(p_text); + } else if (mode == MODE_IMPORT) { + install_path->set_text(p_text); } else { - install_path_container->hide(); + ERR_FAIL(); + } +} + +void ProjectDialog::_update_target_auto_dir() { + String new_auto_dir; + if (mode == MODE_NEW || mode == MODE_INSTALL) { + new_auto_dir = project_name->get_text(); + } else if (mode == MODE_IMPORT) { + new_auto_dir = project_path->get_text().get_file().get_basename(); + } + int naming_convention = (int)EDITOR_GET("project_manager/directory_naming_convention"); + switch (naming_convention) { + case 0: // No convention + break; + case 1: // kebab-case + new_auto_dir = new_auto_dir.to_lower().replace(" ", "-"); + break; + case 2: // snake_case + new_auto_dir = new_auto_dir.to_snake_case(); + break; + case 3: // camelCase + new_auto_dir = new_auto_dir.to_camel_case(); + break; + case 4: // PascalCase + new_auto_dir = new_auto_dir.to_pascal_case(); + break; + case 5: // Title Case + new_auto_dir = new_auto_dir.capitalize(); + break; + default: + ERR_FAIL_MSG("Invalid directory naming convention."); + break; + } + new_auto_dir = OS::get_singleton()->get_safe_dir_name(new_auto_dir); + + if (create_dir->is_pressed()) { + String target_path = _get_target_path(); + + if (target_path.get_file() == auto_dir) { + // Update target dir name to new project name / ZIP name. + target_path = target_path.get_base_dir().path_join(new_auto_dir); + } + + _set_target_path(target_path); } - _update_path(p_path.simplify_path()); + auto_dir = new_auto_dir; } -void ProjectDialog::_file_selected(const String &p_path) { - // If not already shown. - show_dialog(); +void ProjectDialog::_create_dir_toggled(bool p_pressed) { + String target_path = _get_target_path(); - String p = p_path; - if (mode == MODE_IMPORT) { - if (p.ends_with("project.godot")) { - p = p.get_base_dir(); - install_path_container->hide(); - get_ok_button()->set_disabled(false); - } else if (p.ends_with(".zip")) { - install_path->set_text(p.get_base_dir()); - install_path_container->show(); - get_ok_button()->set_disabled(false); + if (create_dir->is_pressed()) { + // (Re-)append target dir name. + if (last_custom_target_dir.is_empty()) { + target_path = target_path.path_join(auto_dir); } else { - _set_message(TTR("Please choose a \"project.godot\" or \".zip\" file."), MESSAGE_ERROR); - get_ok_button()->set_disabled(true); - return; + target_path = target_path.path_join(last_custom_target_dir); + } + } else { + // Save and remove target dir name. + if (target_path.get_file() == auto_dir) { + last_custom_target_dir = ""; + } else { + last_custom_target_dir = target_path.get_file(); } + target_path = target_path.get_base_dir(); } - String sp = p.simplify_path(); - project_path->set_text(sp); - _update_path(sp); - if (p.ends_with(".zip")) { - callable_mp((Control *)install_path, &Control::grab_focus).call_deferred(); - } else { - callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred(); + _set_target_path(target_path); + _validate_path(); +} + +void ProjectDialog::_project_name_changed() { + if (mode == MODE_NEW || mode == MODE_INSTALL) { + _update_target_auto_dir(); } + + _validate_path(); } -void ProjectDialog::_path_selected(const String &p_path) { - // If not already shown. - show_dialog(); +void ProjectDialog::_project_path_changed() { + if (mode == MODE_IMPORT) { + _update_target_auto_dir(); + } - String sp = p_path.simplify_path(); - project_path->set_text(sp); - _update_path(sp); - callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred(); + _validate_path(); } -void ProjectDialog::_install_path_selected(const String &p_path) { - String sp = p_path.simplify_path(); - install_path->set_text(sp); - _update_path(sp); - callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred(); +void ProjectDialog::_install_path_changed() { + _validate_path(); } -void ProjectDialog::_browse_path() { - fdialog->set_current_dir(project_path->get_text()); +void ProjectDialog::_browse_project_path() { + if (mode == MODE_IMPORT && install_path->is_visible_in_tree()) { + // Select last ZIP file. + fdialog_project->set_current_path(project_path->get_text()); + } else if ((mode == MODE_NEW || mode == MODE_INSTALL) && create_dir->is_pressed()) { + // Select parent directory of project path. + fdialog_project->set_current_dir(project_path->get_text().get_base_dir()); + } else { + // Select project path. + fdialog_project->set_current_dir(project_path->get_text()); + } if (mode == MODE_IMPORT) { - fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_ANY); - fdialog->clear_filters(); - fdialog->add_filter("project.godot", vformat("%s %s", VERSION_NAME, TTR("Project"))); - fdialog->add_filter("*.zip", TTR("ZIP File")); + fdialog_project->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_ANY); + fdialog_project->clear_filters(); + fdialog_project->add_filter("project.godot", vformat("%s %s", VERSION_NAME, TTR("Project"))); + fdialog_project->add_filter("*.zip", TTR("ZIP File")); } else { - fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR); + fdialog_project->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR); } - fdialog->popup_file_dialog(); + fdialog_project->popup_file_dialog(); } void ProjectDialog::_browse_install_path() { - fdialog_install->set_current_dir(install_path->get_text()); + ERR_FAIL_COND_MSG(mode != MODE_IMPORT, "Install path is only used for MODE_IMPORT."); + + if (create_dir->is_pressed()) { + // Select parent directory of install path. + fdialog_install->set_current_dir(install_path->get_text().get_base_dir()); + } else { + // Select install path. + fdialog_install->set_current_dir(install_path->get_text()); + } + fdialog_install->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR); fdialog_install->popup_file_dialog(); } -void ProjectDialog::_create_folder() { - const String project_name_no_edges = project_name->get_text().strip_edges(); - if (project_name_no_edges.is_empty() || !created_folder_path.is_empty() || project_name_no_edges.ends_with(".")) { - _set_message(TTR("Invalid project name."), MESSAGE_WARNING); - return; +void ProjectDialog::_project_path_selected(const String &p_path) { + if (create_dir->is_pressed() && (mode == MODE_NEW || mode == MODE_INSTALL)) { + // Replace parent directory, but keep target dir name. + project_path->set_text(p_path.path_join(project_path->get_text().get_file())); + } else { + project_path->set_text(p_path); } - Ref d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (d->change_dir(project_path->get_text()) == OK) { - if (!d->dir_exists(project_name_no_edges)) { - if (d->make_dir(project_name_no_edges) == OK) { - d->change_dir(project_name_no_edges); - String dir_str = d->get_current_dir(); - project_path->set_text(dir_str); - _update_path(dir_str); - created_folder_path = d->get_current_dir(); - create_dir->set_disabled(true); - } else { - dialog_error->set_text(TTR("Couldn't create folder.")); - dialog_error->popup_centered(); - } - } else { - dialog_error->set_text(TTR("There is already a folder in this path with the specified name.")); - dialog_error->popup_centered(); - } - } -} + _project_path_changed(); -void ProjectDialog::_text_changed(const String &p_text) { - if (mode != MODE_NEW) { - return; + if (install_path->is_visible_in_tree()) { + // ZIP is selected; focus install path. + install_path->grab_focus(); + } else { + get_ok_button()->grab_focus(); } +} - _test_path(); +void ProjectDialog::_install_path_selected(const String &p_path) { + ERR_FAIL_COND_MSG(mode != MODE_IMPORT, "Install path is only used for MODE_IMPORT."); - if (p_text.strip_edges().is_empty()) { - _set_message(TTR("It would be a good idea to name your project."), MESSAGE_ERROR); + if (create_dir->is_pressed()) { + // Replace parent directory, but keep target dir name. + install_path->set_text(p_path.path_join(install_path->get_text().get_file())); + } else { + install_path->set_text(p_path); } -} -void ProjectDialog::_nonempty_confirmation_ok_pressed() { - is_folder_empty = true; - ok_pressed(); + _install_path_changed(); + + get_ok_button()->grab_focus(); } void ProjectDialog::_renderer_selected() { @@ -417,233 +450,216 @@ void ProjectDialog::_renderer_selected() { } } -void ProjectDialog::_remove_created_folder() { - if (!created_folder_path.is_empty()) { - Ref d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - d->remove(created_folder_path); - - create_dir->set_disabled(false); - created_folder_path = ""; - } +void ProjectDialog::_nonempty_confirmation_ok_pressed() { + is_folder_empty = true; + ok_pressed(); } void ProjectDialog::ok_pressed() { - String dir = project_path->get_text(); + // Before we create a project, check that the target folder is empty. + // If not, we need to ask the user if they're sure they want to do this. + if (!is_folder_empty) { + ConfirmationDialog *cd = memnew(ConfirmationDialog); + cd->set_title(TTR("Warning: This folder is not empty")); + cd->set_text(TTR("You are about to create a Godot project in a non-empty folder.\nThe entire contents of this folder will be imported as project resources!\n\nAre you sure you wish to continue?")); + cd->get_ok_button()->connect("pressed", callable_mp(this, &ProjectDialog::_nonempty_confirmation_ok_pressed)); + get_parent()->add_child(cd); + cd->popup_centered(); + return; + } - if (mode == MODE_RENAME) { - String dir2 = _test_path(); - if (dir2.is_empty()) { - _set_message(TTR("Invalid project path (changed anything?)."), MESSAGE_ERROR); + String path = project_path->get_text(); + + if (mode == MODE_NEW) { + if (create_dir->is_pressed()) { + Ref d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (!d->dir_exists(path) && d->make_dir(path) != OK) { + _set_message(TTR("Couldn't create project directory, check permissions."), MESSAGE_ERROR); + return; + } + } + + PackedStringArray project_features = ProjectSettings::get_required_features(); + ProjectSettings::CustomMap initial_settings; + + // Be sure to change this code if/when renderers are changed. + // Default values are "forward_plus" for the main setting, "mobile" for the mobile override, + // and "gl_compatibility" for the web override. + String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method")); + initial_settings["rendering/renderer/rendering_method"] = renderer_type; + + EditorSettings::get_singleton()->set("project_manager/default_renderer", renderer_type); + EditorSettings::get_singleton()->save(); + + if (renderer_type == "forward_plus") { + project_features.push_back("Forward Plus"); + } else if (renderer_type == "mobile") { + project_features.push_back("Mobile"); + } else if (renderer_type == "gl_compatibility") { + project_features.push_back("GL Compatibility"); + // Also change the default rendering method for the mobile override. + initial_settings["rendering/renderer/rendering_method.mobile"] = "gl_compatibility"; + } else { + WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub."); + } + + project_features.sort(); + initial_settings["application/config/features"] = project_features; + initial_settings["application/config/name"] = project_name->get_text().strip_edges(); + initial_settings["application/config/icon"] = "res://icon.svg"; + + Error err = ProjectSettings::get_singleton()->save_custom(path.path_join("project.godot"), initial_settings, Vector(), false); + if (err != OK) { + _set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR); return; } - // Load project.godot as ConfigFile to set the new name. - ConfigFile cfg; - String project_godot = dir2.path_join("project.godot"); - Error err = cfg.load(project_godot); + // Store default project icon in SVG format. + Ref fa_icon = FileAccess::open(path.path_join("icon.svg"), FileAccess::WRITE, &err); if (err != OK) { - _set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR); - } else { - cfg.set_value("application", "config/name", project_name->get_text().strip_edges()); - err = cfg.save(project_godot); - if (err != OK) { - _set_message(vformat(TTR("Couldn't save project at '%s' (error %d)."), project_godot, err), MESSAGE_ERROR); - } + _set_message(TTR("Couldn't create icon.svg in project path."), MESSAGE_ERROR); + return; } + fa_icon->store_string(get_default_project_icon()); - hide(); - emit_signal(SNAME("projects_updated")); + EditorVCSInterface::create_vcs_metadata_files(EditorVCSInterface::VCSMetadata(vcs_metadata_selection->get_selected()), path); + } - } else { - if (mode == MODE_IMPORT) { - if (project_path->get_text().ends_with(".zip")) { - mode = MODE_INSTALL; - ok_pressed(); + // Two cases for importing a ZIP. + switch (mode) { + case MODE_IMPORT: { + if (zip_path.is_empty()) { + break; + } + path = install_path->get_text().simplify_path(); + [[fallthrough]]; + } + case MODE_INSTALL: { + ERR_FAIL_COND(zip_path.is_empty()); + + Ref io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); + + unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io); + if (!pkg) { + dialog_error->set_text(TTR("Error opening package file, not in ZIP format.")); + dialog_error->popup_centered(); return; } - } else { - if (mode == MODE_NEW) { - // Before we create a project, check that the target folder is empty. - // If not, we need to ask the user if they're sure they want to do this. - if (!is_folder_empty) { - ConfirmationDialog *cd = memnew(ConfirmationDialog); - cd->set_title(TTR("Warning: This folder is not empty")); - cd->set_text(TTR("You are about to create a Godot project in a non-empty folder.\nThe entire contents of this folder will be imported as project resources!\n\nAre you sure you wish to continue?")); - cd->get_ok_button()->connect("pressed", callable_mp(this, &ProjectDialog::_nonempty_confirmation_ok_pressed)); - get_parent()->add_child(cd); - cd->popup_centered(); - cd->grab_focus(); - return; + // Find the first directory with a "project.godot". + String zip_root; + int ret = unzGoToFirstFile(pkg); + while (ret == UNZ_OK) { + unz_file_info info; + char fname[16384]; + unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); + ERR_FAIL_COND_MSG(ret != UNZ_OK, "Failed to get current file info."); + + String name = String::utf8(fname); + if (name.get_file() == "project.godot") { + zip_root = name.get_base_dir(); + break; } - PackedStringArray project_features = ProjectSettings::get_required_features(); - ProjectSettings::CustomMap initial_settings; - - // Be sure to change this code if/when renderers are changed. - // Default values are "forward_plus" for the main setting, "mobile" for the mobile override, - // and "gl_compatibility" for the web override. - String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method")); - initial_settings["rendering/renderer/rendering_method"] = renderer_type; - - EditorSettings::get_singleton()->set("project_manager/default_renderer", renderer_type); - EditorSettings::get_singleton()->save(); - - if (renderer_type == "forward_plus") { - project_features.push_back("Forward Plus"); - } else if (renderer_type == "mobile") { - project_features.push_back("Mobile"); - } else if (renderer_type == "gl_compatibility") { - project_features.push_back("GL Compatibility"); - // Also change the default rendering method for the mobile override. - initial_settings["rendering/renderer/rendering_method.mobile"] = "gl_compatibility"; - } else { - WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub."); - } - - project_features.sort(); - initial_settings["application/config/features"] = project_features; - initial_settings["application/config/name"] = project_name->get_text().strip_edges(); - initial_settings["application/config/icon"] = "res://icon.svg"; - - if (ProjectSettings::get_singleton()->save_custom(dir.path_join("project.godot"), initial_settings, Vector(), false) != OK) { - _set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR); - } else { - // Store default project icon in SVG format. - Error err; - Ref fa_icon = FileAccess::open(dir.path_join("icon.svg"), FileAccess::WRITE, &err); - fa_icon->store_string(get_default_project_icon()); - - if (err != OK) { - _set_message(TTR("Couldn't create icon.svg in project path."), MESSAGE_ERROR); - } - EditorVCSInterface::create_vcs_metadata_files(EditorVCSInterface::VCSMetadata(vcs_metadata_selection->get_selected()), dir); - } - } else if (mode == MODE_INSTALL) { - if (project_path->get_text().ends_with(".zip")) { - dir = install_path->get_text(); - zip_path = project_path->get_text(); - } + ret = unzGoToNextFile(pkg); + } - Ref io_fa; - zlib_filefunc_def io = zipio_create_io(&io_fa); + if (ret == UNZ_END_OF_LIST_OF_FILE) { + _set_message(TTR("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR); + unzClose(pkg); + return; + } - unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io); - if (!pkg) { - dialog_error->set_text(TTR("Error opening package file, not in ZIP format.")); - dialog_error->popup_centered(); + if (create_dir->is_pressed()) { + Ref d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (!d->dir_exists(path) && d->make_dir(path) != OK) { + _set_message(TTR("Couldn't create project directory, check permissions."), MESSAGE_ERROR); return; } + } - // Find the zip_root - String zip_root; - int ret = unzGoToFirstFile(pkg); - while (ret == UNZ_OK) { - unz_file_info info; - char fname[16384]; - unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); - - String name = String::utf8(fname); - if (name.ends_with("project.godot")) { - zip_root = name.substr(0, name.rfind("project.godot")); - break; + ret = unzGoToFirstFile(pkg); + + Vector failed_files; + while (ret == UNZ_OK) { + //get filename + unz_file_info info; + char fname[16384]; + ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); + ERR_FAIL_COND_MSG(ret != UNZ_OK, "Failed to get current file info."); + + String rel_path = String::utf8(fname).trim_prefix(zip_root); + if (rel_path.is_empty()) { // Root. + } else if (rel_path.ends_with("/")) { // Directory. + Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + da->make_dir(path.path_join(rel_path)); + } else { // File. + Vector uncomp_data; + uncomp_data.resize(info.uncompressed_size); + + unzOpenCurrentFile(pkg); + ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size()); + ERR_BREAK_MSG(ret < 0, vformat("An error occurred while attempting to read from file: %s. This file will not be used.", rel_path)); + unzCloseCurrentFile(pkg); + + Ref f = FileAccess::open(path.path_join(rel_path), FileAccess::WRITE); + if (f.is_valid()) { + f->store_buffer(uncomp_data.ptr(), uncomp_data.size()); + } else { + failed_files.push_back(rel_path); } - - ret = unzGoToNextFile(pkg); } - ret = unzGoToFirstFile(pkg); + ret = unzGoToNextFile(pkg); + } - Vector failed_files; + unzClose(pkg); - while (ret == UNZ_OK) { - //get filename - unz_file_info info; - char fname[16384]; - ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); - if (ret != UNZ_OK) { + if (failed_files.size()) { + String err_msg = TTR("The following files failed extraction from package:") + "\n\n"; + for (int i = 0; i < failed_files.size(); i++) { + if (i > 15) { + err_msg += "\nAnd " + itos(failed_files.size() - i) + " more files."; break; } - - String path = String::utf8(fname); - - if (path.is_empty() || path == zip_root || !zip_root.is_subsequence_of(path)) { - // - } else if (path.ends_with("/")) { // a dir - path = path.substr(0, path.length() - 1); - String rel_path = path.substr(zip_root.length()); - - Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - da->make_dir(dir.path_join(rel_path)); - } else { - Vector uncomp_data; - uncomp_data.resize(info.uncompressed_size); - String rel_path = path.substr(zip_root.length()); - - //read - unzOpenCurrentFile(pkg); - ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size()); - ERR_BREAK_MSG(ret < 0, vformat("An error occurred while attempting to read from file: %s. This file will not be used.", rel_path)); - unzCloseCurrentFile(pkg); - - Ref f = FileAccess::open(dir.path_join(rel_path), FileAccess::WRITE); - if (f.is_valid()) { - f->store_buffer(uncomp_data.ptr(), uncomp_data.size()); - } else { - failed_files.push_back(rel_path); - } - } - - ret = unzGoToNextFile(pkg); + err_msg += failed_files[i] + "\n"; } - unzClose(pkg); - - if (failed_files.size()) { - String err_msg = TTR("The following files failed extraction from package:") + "\n\n"; - for (int i = 0; i < failed_files.size(); i++) { - if (i > 15) { - err_msg += "\nAnd " + itos(failed_files.size() - i) + " more files."; - break; - } - err_msg += failed_files[i] + "\n"; - } - - dialog_error->set_text(err_msg); - dialog_error->popup_centered(); - - } else if (!project_path->get_text().ends_with(".zip")) { - dialog_error->set_text(TTR("Package installed successfully!")); - dialog_error->popup_centered(); - } + dialog_error->set_text(err_msg); + dialog_error->popup_centered(); + return; } - } - - dir = dir.replace("\\", "/"); - if (dir.ends_with("/")) { - dir = dir.substr(0, dir.length() - 1); - } - - hide(); - emit_signal(SNAME("project_created"), dir); + } break; + default: { + } break; } -} - -void ProjectDialog::cancel_pressed() { - _remove_created_folder(); - - project_path->clear(); - _update_path(""); - project_name->clear(); - _text_changed(""); - if (status_rect->get_texture() == get_editor_theme_icon(SNAME("StatusError"))) { - msg->show(); + if (mode == MODE_RENAME || mode == MODE_INSTALL) { + // Load project.godot as ConfigFile to set the new name. + ConfigFile cfg; + String project_godot = path.path_join("project.godot"); + Error err = cfg.load(project_godot); + if (err != OK) { + dialog_error->set_text(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err)); + dialog_error->popup_centered(); + return; + } + cfg.set_value("application", "config/name", project_name->get_text().strip_edges()); + err = cfg.save(project_godot); + if (err != OK) { + dialog_error->set_text(vformat(TTR("Couldn't save project at '%s' (error %d)."), project_godot, err)); + dialog_error->popup_centered(); + return; + } } - if (install_status_rect->get_texture() == get_editor_theme_icon(SNAME("StatusError"))) { - msg->show(); + hide(); + if (mode == MODE_NEW || mode == MODE_IMPORT || mode == MODE_INSTALL) { + emit_signal(SNAME("project_created"), path); + } else if (mode == MODE_RENAME) { + emit_signal(SNAME("projects_updated")); } } @@ -659,6 +675,10 @@ void ProjectDialog::set_mode(Mode p_mode) { mode = p_mode; } +void ProjectDialog::set_project_name(const String &p_name) { + project_name->set_text(p_name); +} + void ProjectDialog::set_project_path(const String &p_path) { project_path->set_text(p_path); } @@ -666,120 +686,96 @@ void ProjectDialog::set_project_path(const String &p_path) { void ProjectDialog::ask_for_path_and_show() { // Workaround: for the file selection dialog content to be rendered we need to show its parent dialog. show_dialog(); - _set_message(""); - - _browse_path(); + _browse_project_path(); } void ProjectDialog::show_dialog() { if (mode == MODE_RENAME) { + // Name and path are set in `ProjectManager::_rename_project`. project_path->set_editable(false); - browse->hide(); - install_browse->hide(); set_title(TTR("Rename Project")); set_ok_button_text(TTR("Rename")); + + create_dir->hide(); + project_status_rect->hide(); + project_browse->hide(); + name_container->show(); - status_rect->hide(); - msg->hide(); install_path_container->hide(); - install_status_rect->hide(); renderer_container->hide(); default_files_container->hide(); - get_ok_button()->set_disabled(false); - - // Fetch current name from project.godot to prefill the text input. - ConfigFile cfg; - String project_godot = project_path->get_text().path_join("project.godot"); - Error err = cfg.load(project_godot); - if (err != OK) { - _set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR); - status_rect->show(); - msg->show(); - get_ok_button()->set_disabled(true); - } else { - String cur_name = cfg.get_value("application", "config/name", ""); - project_name->set_text(cur_name); - _text_changed(cur_name); - } callable_mp((Control *)project_name, &Control::grab_focus).call_deferred(); - - create_dir->hide(); - + callable_mp(project_name, &LineEdit::select_all).call_deferred(); } else { - fav_dir = EDITOR_GET("filesystem/directories/default_project_path"); + String proj = TTR("New Game Project"); + project_name->set_text(proj); + project_path->set_editable(true); + + String fav_dir = EDITOR_GET("filesystem/directories/default_project_path"); if (!fav_dir.is_empty()) { project_path->set_text(fav_dir); - fdialog->set_current_dir(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()); - fdialog->set_current_dir(d->get_current_dir()); - } - - if (project_name->get_text().is_empty()) { - String proj = TTR("New Game Project"); - project_name->set_text(proj); - _text_changed(proj); + install_path->set_text(d->get_current_dir()); + fdialog_project->set_current_dir(d->get_current_dir()); } - project_path->set_editable(true); - browse->set_disabled(false); - browse->show(); - install_browse->set_disabled(false); - install_browse->show(); create_dir->show(); - status_rect->show(); - install_status_rect->show(); - msg->show(); + project_status_rect->show(); + project_browse->show(); if (mode == MODE_IMPORT) { set_title(TTR("Import Existing Project")); set_ok_button_text(TTR("Import & Edit")); + name_container->hide(); install_path_container->hide(); renderer_container->hide(); default_files_container->hide(); - project_path->grab_focus(); + // Project path dialog is also opened; no need to change focus. } else if (mode == MODE_NEW) { set_title(TTR("Create New Project")); set_ok_button_text(TTR("Create & Edit")); + name_container->show(); install_path_container->hide(); renderer_container->show(); default_files_container->show(); + callable_mp((Control *)project_name, &Control::grab_focus).call_deferred(); callable_mp(project_name, &LineEdit::select_all).call_deferred(); - } else if (mode == MODE_INSTALL) { set_title(TTR("Install Project:") + " " + zip_title); set_ok_button_text(TTR("Install & Edit")); + project_name->set_text(zip_title); + name_container->show(); install_path_container->hide(); renderer_container->hide(); default_files_container->hide(); - project_path->grab_focus(); + + callable_mp((Control *)project_path, &Control::grab_focus).call_deferred(); } - _test_path(); + auto_dir = ""; + last_custom_target_dir = ""; + _update_target_auto_dir(); + if (create_dir->is_pressed()) { + // Append `auto_dir` to target path. + _create_dir_toggled(true); + } } - popup_centered(Size2(500, 0) * EDSCALE); -} - -void ProjectDialog::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_THEME_CHANGED: { - create_dir->set_icon(get_editor_theme_icon(SNAME("FolderCreate"))); - } break; + _validate_path(); - case NOTIFICATION_WM_CLOSE_REQUEST: { - _remove_created_folder(); - } break; - } + popup_centered(Size2(500, 0) * EDSCALE); } void ProjectDialog::_bind_methods() { @@ -798,27 +794,29 @@ ProjectDialog::ProjectDialog() { l->set_text(TTR("Project Name:")); name_container->add_child(l); - HBoxContainer *pnhb = memnew(HBoxContainer); - name_container->add_child(pnhb); - project_name = memnew(LineEdit); project_name->set_h_size_flags(Control::SIZE_EXPAND_FILL); - pnhb->add_child(project_name); + name_container->add_child(project_name); - create_dir = memnew(Button); - pnhb->add_child(create_dir); - create_dir->set_text(TTR("Create Folder")); - create_dir->connect("pressed", callable_mp(this, &ProjectDialog::_create_folder)); + project_path_container = memnew(VBoxContainer); + vb->add_child(project_path_container); - path_container = memnew(VBoxContainer); - vb->add_child(path_container); + HBoxContainer *pphb_label = memnew(HBoxContainer); + project_path_container->add_child(pphb_label); l = memnew(Label); l->set_text(TTR("Project Path:")); - path_container->add_child(l); + l->set_h_size_flags(Control::SIZE_EXPAND_FILL); + pphb_label->add_child(l); + + create_dir = memnew(CheckButton); + create_dir->set_text(TTR("Create Folder")); + create_dir->set_pressed(true); + pphb_label->add_child(create_dir); + create_dir->connect("toggled", callable_mp(this, &ProjectDialog::_create_dir_toggled)); HBoxContainer *pphb = memnew(HBoxContainer); - path_container->add_child(pphb); + project_path_container->add_child(pphb); project_path = memnew(LineEdit); project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL); @@ -841,14 +839,14 @@ ProjectDialog::ProjectDialog() { iphb->add_child(install_path); // status icon - status_rect = memnew(TextureRect); - status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED); - pphb->add_child(status_rect); + project_status_rect = memnew(TextureRect); + project_status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED); + pphb->add_child(project_status_rect); - browse = memnew(Button); - browse->set_text(TTR("Browse")); - browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_path)); - pphb->add_child(browse); + project_browse = memnew(Button); + project_browse->set_text(TTR("Browse")); + project_browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_project_path)); + pphb->add_child(project_browse); // install status icon install_status_rect = memnew(TextureRect); @@ -863,6 +861,7 @@ ProjectDialog::ProjectDialog() { msg = memnew(Label); msg->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); msg->set_custom_minimum_size(Size2(200, 0) * EDSCALE); + msg->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART); vb->add_child(msg); // Renderer selection. @@ -957,20 +956,20 @@ ProjectDialog::ProjectDialog() { spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL); default_files_container->add_child(spacer); - fdialog = memnew(EditorFileDialog); - fdialog->set_previews_enabled(false); //Crucial, otherwise the engine crashes. - fdialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM); + fdialog_project = memnew(EditorFileDialog); + fdialog_project->set_previews_enabled(false); //Crucial, otherwise the engine crashes. + fdialog_project->set_access(EditorFileDialog::ACCESS_FILESYSTEM); fdialog_install = memnew(EditorFileDialog); fdialog_install->set_previews_enabled(false); //Crucial, otherwise the engine crashes. fdialog_install->set_access(EditorFileDialog::ACCESS_FILESYSTEM); - add_child(fdialog); + add_child(fdialog_project); add_child(fdialog_install); - project_name->connect("text_changed", callable_mp(this, &ProjectDialog::_text_changed)); - project_path->connect("text_changed", callable_mp(this, &ProjectDialog::_path_text_changed)); - install_path->connect("text_changed", callable_mp(this, &ProjectDialog::_update_path)); - fdialog->connect("dir_selected", callable_mp(this, &ProjectDialog::_path_selected)); - fdialog->connect("file_selected", callable_mp(this, &ProjectDialog::_file_selected)); + project_name->connect("text_changed", callable_mp(this, &ProjectDialog::_project_name_changed).unbind(1)); + project_path->connect("text_changed", callable_mp(this, &ProjectDialog::_project_path_changed).unbind(1)); + install_path->connect("text_changed", callable_mp(this, &ProjectDialog::_install_path_changed).unbind(1)); + fdialog_project->connect("dir_selected", callable_mp(this, &ProjectDialog::_project_path_selected)); + fdialog_project->connect("file_selected", callable_mp(this, &ProjectDialog::_project_path_selected)); fdialog_install->connect("dir_selected", callable_mp(this, &ProjectDialog::_install_path_selected)); fdialog_install->connect("file_selected", callable_mp(this, &ProjectDialog::_install_path_selected)); diff --git a/editor/project_manager/project_dialog.h b/editor/project_manager/project_dialog.h index dcc5cf71f8f8..1418edc57fdf 100644 --- a/editor/project_manager/project_dialog.h +++ b/editor/project_manager/project_dialog.h @@ -34,6 +34,7 @@ #include "scene/gui/dialogs.h" class Button; +class CheckButton; class EditorFileDialog; class LineEdit; class OptionButton; @@ -65,11 +66,11 @@ class ProjectDialog : public ConfirmationDialog { Mode mode = MODE_NEW; bool is_folder_empty = true; - Button *browse = nullptr; + CheckButton *create_dir = nullptr; + Button *project_browse = nullptr; Button *install_browse = nullptr; - Button *create_dir = nullptr; VBoxContainer *name_container = nullptr; - VBoxContainer *path_container = nullptr; + VBoxContainer *project_path_container = nullptr; VBoxContainer *install_path_container = nullptr; VBoxContainer *renderer_container = nullptr; @@ -78,54 +79,63 @@ class ProjectDialog : public ConfirmationDialog { Ref renderer_button_group; Label *msg = nullptr; - LineEdit *project_path = nullptr; LineEdit *project_name = nullptr; + LineEdit *project_path = nullptr; LineEdit *install_path = nullptr; - TextureRect *status_rect = nullptr; + TextureRect *project_status_rect = nullptr; TextureRect *install_status_rect = nullptr; OptionButton *vcs_metadata_selection = nullptr; - EditorFileDialog *fdialog = nullptr; + EditorFileDialog *fdialog_project = nullptr; EditorFileDialog *fdialog_install = nullptr; AcceptDialog *dialog_error = nullptr; String zip_path; String zip_title; - String fav_dir; - String created_folder_path; + void _set_message(const String &p_msg, MessageType p_type, InputType input_type = PROJECT_PATH); + void _validate_path(); - void _set_message(const String &p_msg, MessageType p_type = MESSAGE_SUCCESS, InputType input_type = PROJECT_PATH); + // Project path for MODE_NEW and MODE_INSTALL. Install path for MODE_IMPORT. + // Install path is only visible when importing a ZIP. + String _get_target_path(); + void _set_target_path(const String &p_text); - String _test_path(); - void _update_path(const String &p_path); - void _path_text_changed(const String &p_path); - void _path_selected(const String &p_path); - void _file_selected(const String &p_path); - void _install_path_selected(const String &p_path); + // Calculated from project name / ZIP name. + String auto_dir; + + // Updates `auto_dir`. If the target path dir name is equal to `auto_dir` (the default state), the target path is also updated. + void _update_target_auto_dir(); - void _browse_path(); + // While `create_dir` is disabled, stores the last target path dir name, or an empty string if equal to `auto_dir`. + String last_custom_target_dir; + void _create_dir_toggled(bool p_pressed); + + void _project_name_changed(); + void _project_path_changed(); + void _install_path_changed(); + + void _browse_project_path(); void _browse_install_path(); - void _create_folder(); - void _text_changed(const String &p_text); - void _nonempty_confirmation_ok_pressed(); + void _project_path_selected(const String &p_path); + void _install_path_selected(const String &p_path); + void _renderer_selected(); - void _remove_created_folder(); + void _nonempty_confirmation_ok_pressed(); void ok_pressed() override; - void cancel_pressed() override; protected: - void _notification(int p_what); static void _bind_methods(); public: - void set_zip_path(const String &p_path); - void set_zip_title(const String &p_title); void set_mode(Mode p_mode); + void set_project_name(const String &p_name); 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 ask_for_path_and_show(); void show_dialog();