Path: blob/master/editor/project_manager/project_dialog.cpp
9903 views
/**************************************************************************/1/* project_dialog.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "project_dialog.h"3132#include "core/config/project_settings.h"33#include "core/io/dir_access.h"34#include "core/io/zip_io.h"35#include "core/version.h"36#include "editor/editor_string_names.h"37#include "editor/gui/editor_file_dialog.h"38#include "editor/settings/editor_settings.h"39#include "editor/themes/editor_icons.h"40#include "editor/themes/editor_scale.h"41#include "editor/version_control/editor_vcs_interface.h"42#include "scene/gui/check_box.h"43#include "scene/gui/check_button.h"44#include "scene/gui/line_edit.h"45#include "scene/gui/option_button.h"46#include "scene/gui/separator.h"47#include "scene/gui/texture_rect.h"4849void ProjectDialog::_set_message(const String &p_msg, MessageType p_type, InputType p_input_type) {50msg->set_text(p_msg);51get_ok_button()->set_disabled(p_type == MESSAGE_ERROR);5253Ref<Texture2D> new_icon;54switch (p_type) {55case MESSAGE_ERROR: {56msg->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor)));57new_icon = get_editor_theme_icon(SNAME("StatusError"));58} break;59case MESSAGE_WARNING: {60msg->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));61new_icon = get_editor_theme_icon(SNAME("StatusWarning"));62} break;63case MESSAGE_SUCCESS: {64msg->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("success_color"), EditorStringName(Editor)));65new_icon = get_editor_theme_icon(SNAME("StatusSuccess"));66} break;67}6869if (p_input_type == PROJECT_PATH) {70project_status_rect->set_texture(new_icon);71} else if (p_input_type == INSTALL_PATH) {72install_status_rect->set_texture(new_icon);73}74}7576static bool is_zip_file(Ref<DirAccess> p_d, const String &p_path) {77return p_path.get_extension() == "zip" && p_d->file_exists(p_path);78}7980void ProjectDialog::_validate_path() {81_set_message("", MESSAGE_SUCCESS, PROJECT_PATH);82_set_message("", MESSAGE_SUCCESS, INSTALL_PATH);8384if (project_name->get_text().strip_edges().is_empty()) {85_set_message(TTRC("It would be a good idea to name your project."), MESSAGE_ERROR);86return;87}8889Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);90String path = project_path->get_text().simplify_path();9192String target_path = path;93InputType target_path_input_type = PROJECT_PATH;9495if (mode == MODE_IMPORT) {96if (path.get_file().strip_edges() == "project.godot") {97path = path.get_base_dir();98project_path->set_text(path);99}100101if (is_zip_file(d, path)) {102zip_path = path;103} else if (is_zip_file(d, path.strip_edges())) {104zip_path = path.strip_edges();105} else {106zip_path = "";107}108109if (!zip_path.is_empty()) {110target_path = install_path->get_text().simplify_path();111target_path_input_type = INSTALL_PATH;112113create_dir->show();114install_path_container->show();115116Ref<FileAccess> io_fa;117zlib_filefunc_def io = zipio_create_io(&io_fa);118119unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io);120if (!pkg) {121_set_message(TTRC("Invalid \".zip\" project file; it is not in ZIP format."), MESSAGE_ERROR);122unzClose(pkg);123return;124}125126int ret = unzGoToFirstFile(pkg);127while (ret == UNZ_OK) {128unz_file_info info;129char fname[16384];130ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);131ERR_FAIL_COND_MSG(ret != UNZ_OK, "Failed to get current file info.");132133String name = String::utf8(fname);134135// Skip the __MACOSX directory created by macOS's built-in file zipper.136if (name.begins_with("__MACOSX")) {137ret = unzGoToNextFile(pkg);138continue;139}140141if (name.get_file() == "project.godot") {142break; // ret == UNZ_OK.143}144145ret = unzGoToNextFile(pkg);146}147148if (ret == UNZ_END_OF_LIST_OF_FILE) {149_set_message(TTRC("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR);150unzClose(pkg);151return;152}153154unzClose(pkg);155} else if (d->dir_exists(path) && d->file_exists(path.path_join("project.godot"))) {156zip_path = "";157158create_dir->hide();159install_path_container->hide();160161_set_message(TTRC("Valid project found at path."), MESSAGE_SUCCESS);162} else {163create_dir->hide();164install_path_container->hide();165166_set_message(TTRC("Please choose a \"project.godot\", a directory with one, or a \".zip\" file."), MESSAGE_ERROR);167return;168}169}170171if (target_path.is_relative_path()) {172_set_message(TTRC("The path specified is invalid."), MESSAGE_ERROR, target_path_input_type);173return;174}175176if (target_path.get_file() != OS::get_singleton()->get_safe_dir_name(target_path.get_file())) {177_set_message(TTRC("The directory name specified contains invalid characters or trailing whitespace."), MESSAGE_ERROR, target_path_input_type);178return;179}180181String working_dir = d->get_current_dir();182String executable_dir = OS::get_singleton()->get_executable_path().get_base_dir();183if (target_path == working_dir || target_path == executable_dir) {184_set_message(TTRC("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);185return;186}187188// 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.189#ifdef WINDOWS_ENABLED190String home_dir = OS::get_singleton()->get_environment("USERPROFILE");191#else192String home_dir = OS::get_singleton()->get_environment("HOME");193#endif194String documents_dir = OS::get_singleton()->get_system_dir(OS::SYSTEM_DIR_DOCUMENTS);195if (target_path == home_dir || target_path == documents_dir) {196_set_message(TTRC("You cannot save a project at the selected path. Please create a subfolder or choose a new path."), MESSAGE_ERROR, target_path_input_type);197return;198}199200is_folder_empty = true;201if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE || (mode == MODE_IMPORT && target_path_input_type == InputType::INSTALL_PATH)) {202if (create_dir->is_pressed()) {203if (!d->dir_exists(target_path.get_base_dir())) {204_set_message(TTRC("The parent directory of the path specified doesn't exist."), MESSAGE_ERROR, target_path_input_type);205return;206}207208if (d->dir_exists(target_path)) {209// The path is not necessarily empty here, but we will update the message later if it isn't.210_set_message(TTRC("The project folder already exists and is empty."), MESSAGE_SUCCESS, target_path_input_type);211} else {212_set_message(TTRC("The project folder will be automatically created."), MESSAGE_SUCCESS, target_path_input_type);213}214} else {215if (!d->dir_exists(target_path)) {216_set_message(TTRC("The path specified doesn't exist."), MESSAGE_ERROR, target_path_input_type);217return;218}219220// The path is not necessarily empty here, but we will update the message later if it isn't.221_set_message(TTRC("The project folder exists and is empty."), MESSAGE_SUCCESS, target_path_input_type);222}223224// Check if the directory is empty. Not an error, but we want to warn the user.225if (d->change_dir(target_path) == OK) {226d->list_dir_begin();227String n = d->get_next();228while (!n.is_empty()) {229if (n[0] != '.') {230// Allow `.`, `..` (reserved current/parent folder names)231// and hidden files/folders to be present.232// For instance, this lets users initialize a Git repository233// and still be able to create a project in the directory afterwards.234is_folder_empty = false;235break;236}237n = d->get_next();238}239d->list_dir_end();240241if (!is_folder_empty) {242_set_message(TTRC("The selected path is not empty. Choosing an empty folder is highly recommended."), MESSAGE_WARNING, target_path_input_type);243}244}245}246}247248String ProjectDialog::_get_target_path() {249if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {250return project_path->get_text();251} else if (mode == MODE_IMPORT) {252return install_path->get_text();253} else {254ERR_FAIL_V("");255}256}257void ProjectDialog::_set_target_path(const String &p_text) {258if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {259project_path->set_text(p_text);260} else if (mode == MODE_IMPORT) {261install_path->set_text(p_text);262} else {263ERR_FAIL();264}265}266267void ProjectDialog::_update_target_auto_dir() {268String new_auto_dir;269if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {270new_auto_dir = project_name->get_text();271} else if (mode == MODE_IMPORT) {272new_auto_dir = project_path->get_text().get_file().get_basename();273}274int naming_convention = (int)EDITOR_GET("project_manager/directory_naming_convention");275switch (naming_convention) {276case 0: // No convention277break;278case 1: // kebab-case279new_auto_dir = new_auto_dir.to_kebab_case();280break;281case 2: // snake_case282new_auto_dir = new_auto_dir.to_snake_case();283break;284case 3: // camelCase285new_auto_dir = new_auto_dir.to_camel_case();286break;287case 4: // PascalCase288new_auto_dir = new_auto_dir.to_pascal_case();289break;290case 5: // Title Case291new_auto_dir = new_auto_dir.capitalize();292break;293default:294ERR_FAIL_MSG("Invalid directory naming convention.");295break;296}297new_auto_dir = OS::get_singleton()->get_safe_dir_name(new_auto_dir);298299if (create_dir->is_pressed()) {300String target_path = _get_target_path();301302if (target_path.get_file() == auto_dir) {303// Update target dir name to new project name / ZIP name.304target_path = target_path.get_base_dir().path_join(new_auto_dir);305}306307_set_target_path(target_path);308}309310auto_dir = new_auto_dir;311}312313void ProjectDialog::_create_dir_toggled(bool p_pressed) {314String target_path = _get_target_path();315316if (create_dir->is_pressed()) {317// (Re-)append target dir name.318if (last_custom_target_dir.is_empty()) {319target_path = target_path.path_join(auto_dir);320} else {321target_path = target_path.path_join(last_custom_target_dir);322}323} else {324// Strip any trailing slash.325target_path = target_path.rstrip("/\\");326// Save and remove target dir name.327if (target_path.get_file() == auto_dir) {328last_custom_target_dir = "";329} else {330last_custom_target_dir = target_path.get_file();331}332target_path = target_path.get_base_dir();333}334335_set_target_path(target_path);336_validate_path();337}338339void ProjectDialog::_project_name_changed() {340if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {341_update_target_auto_dir();342}343344_validate_path();345}346347void ProjectDialog::_project_path_changed() {348if (mode == MODE_IMPORT) {349_update_target_auto_dir();350}351352_validate_path();353}354355void ProjectDialog::_install_path_changed() {356_validate_path();357}358359void ProjectDialog::_browse_project_path() {360String path = project_path->get_text();361if (path.is_relative_path()) {362path = EDITOR_GET("filesystem/directories/default_project_path");363}364if (mode == MODE_IMPORT && install_path->is_visible_in_tree()) {365// Select last ZIP file.366fdialog_project->set_current_path(path);367} else if ((mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) && create_dir->is_pressed()) {368// Select parent directory of project path.369fdialog_project->set_current_dir(path.get_base_dir());370} else {371// Select project path.372fdialog_project->set_current_dir(path);373}374375if (mode == MODE_IMPORT) {376fdialog_project->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_ANY);377fdialog_project->clear_filters();378fdialog_project->add_filter("project.godot", vformat("%s %s", GODOT_VERSION_NAME, TTR("Project")));379fdialog_project->add_filter("*.zip", TTR("ZIP File"));380} else {381fdialog_project->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);382}383384hide();385fdialog_project->popup_file_dialog();386}387388void ProjectDialog::_browse_install_path() {389ERR_FAIL_COND_MSG(mode != MODE_IMPORT, "Install path is only used for MODE_IMPORT.");390391String path = install_path->get_text();392if (path.is_relative_path() || !DirAccess::dir_exists_absolute(path)) {393path = EDITOR_GET("filesystem/directories/default_project_path");394}395if (create_dir->is_pressed()) {396// Select parent directory of install path.397fdialog_install->set_current_dir(path.get_base_dir());398} else {399// Select install path.400fdialog_install->set_current_dir(path);401}402403fdialog_install->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);404fdialog_install->popup_file_dialog();405}406407void ProjectDialog::_project_path_selected(const String &p_path) {408show_dialog(false);409410if (create_dir->is_pressed() && (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE)) {411// Replace parent directory, but keep target dir name.412project_path->set_text(p_path.path_join(project_path->get_text().get_file()));413} else {414project_path->set_text(p_path);415}416417_project_path_changed();418419if (install_path->is_visible_in_tree()) {420// ZIP is selected; focus install path.421install_path->grab_focus();422} else {423get_ok_button()->grab_focus();424}425}426427void ProjectDialog::_install_path_selected(const String &p_path) {428ERR_FAIL_COND_MSG(mode != MODE_IMPORT, "Install path is only used for MODE_IMPORT.");429430if (create_dir->is_pressed()) {431// Replace parent directory, but keep target dir name.432install_path->set_text(p_path.path_join(install_path->get_text().get_file()));433} else {434install_path->set_text(p_path);435}436437_install_path_changed();438439get_ok_button()->grab_focus();440}441442void ProjectDialog::_reset_name() {443project_name->set_text(TTR("New Game Project"));444}445446void ProjectDialog::_renderer_selected() {447ERR_FAIL_NULL(renderer_button_group->get_pressed_button());448449String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));450451bool rd_error = false;452453if (renderer_type == "forward_plus") {454renderer_info->set_text(455String::utf8("• ") + TTR("Supports desktop platforms only.") +456String::utf8("\n• ") + TTR("Advanced 3D graphics available.") +457String::utf8("\n• ") + TTR("Can scale to large complex scenes.") +458String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +459String::utf8("\n• ") + TTR("Slower rendering of simple scenes."));460rd_error = !rendering_device_supported;461} else if (renderer_type == "mobile") {462renderer_info->set_text(463String::utf8("• ") + TTR("Supports desktop + mobile platforms.") +464String::utf8("\n• ") + TTR("Less advanced 3D graphics.") +465String::utf8("\n• ") + TTR("Less scalable for complex scenes.") +466String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +467String::utf8("\n• ") + TTR("Fast rendering of simple scenes."));468rd_error = !rendering_device_supported;469} else if (renderer_type == "gl_compatibility") {470renderer_info->set_text(471String::utf8("• ") + TTR("Supports desktop, mobile + web platforms.") +472String::utf8("\n• ") + TTR("Least advanced 3D graphics.") +473String::utf8("\n• ") + TTR("Intended for low-end/older devices.") +474String::utf8("\n• ") + TTR("Uses OpenGL 3 backend (OpenGL 3.3/ES 3.0/WebGL2).") +475String::utf8("\n• ") + TTR("Fastest rendering of simple scenes."));476} else {477WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");478}479480rd_not_supported->set_visible(rd_error);481get_ok_button()->set_disabled(rd_error);482if (rd_error) {483// Needs to be set here since theme colors aren't available at startup.484rd_not_supported->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor)));485}486}487488void ProjectDialog::_nonempty_confirmation_ok_pressed() {489is_folder_empty = true;490ok_pressed();491}492493void ProjectDialog::ok_pressed() {494// Before we create a project, check that the target folder is empty.495// If not, we need to ask the user if they're sure they want to do this.496if (!is_folder_empty) {497if (!nonempty_confirmation) {498nonempty_confirmation = memnew(ConfirmationDialog);499nonempty_confirmation->set_title(TTRC("Warning: This folder is not empty"));500nonempty_confirmation->set_text(TTRC("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?"));501nonempty_confirmation->get_ok_button()->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_nonempty_confirmation_ok_pressed));502add_child(nonempty_confirmation);503}504nonempty_confirmation->popup_centered();505return;506}507508String path = project_path->get_text();509510if (mode == MODE_NEW) {511if (create_dir->is_pressed()) {512Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);513if (!d->dir_exists(path) && d->make_dir(path) != OK) {514_set_message(TTRC("Couldn't create project directory, check permissions."), MESSAGE_ERROR);515return;516}517}518519PackedStringArray project_features = ProjectSettings::get_required_features();520ProjectSettings::CustomMap initial_settings;521522// Be sure to change this code if/when renderers are changed.523// Default values are "forward_plus" for the main setting, "mobile" for the mobile override,524// and "gl_compatibility" for the web override.525String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));526initial_settings["rendering/renderer/rendering_method"] = renderer_type;527528EditorSettings::get_singleton()->set("project_manager/default_renderer", renderer_type);529EditorSettings::get_singleton()->save();530531if (renderer_type == "forward_plus") {532project_features.push_back("Forward Plus");533} else if (renderer_type == "mobile") {534project_features.push_back("Mobile");535} else if (renderer_type == "gl_compatibility") {536project_features.push_back("GL Compatibility");537// Also change the default rendering method for the mobile override.538initial_settings["rendering/renderer/rendering_method.mobile"] = "gl_compatibility";539} else {540WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");541}542543project_features.sort();544initial_settings["application/config/features"] = project_features;545initial_settings["application/config/name"] = project_name->get_text().strip_edges();546initial_settings["application/config/icon"] = "res://icon.svg";547548Error err = ProjectSettings::get_singleton()->save_custom(path.path_join("project.godot"), initial_settings, Vector<String>(), false);549if (err != OK) {550_set_message(TTRC("Couldn't create project.godot in project path."), MESSAGE_ERROR);551return;552}553554// Store default project icon in SVG format.555Ref<FileAccess> fa_icon = FileAccess::open(path.path_join("icon.svg"), FileAccess::WRITE, &err);556if (err != OK) {557_set_message(TTRC("Couldn't create icon.svg in project path."), MESSAGE_ERROR);558return;559}560fa_icon->store_string(get_default_project_icon());561562EditorVCSInterface::create_vcs_metadata_files(EditorVCSInterface::VCSMetadata(vcs_metadata_selection->get_selected()), path);563564// Ensures external editors and IDEs use UTF-8 encoding.565const String editor_config_path = path.path_join(".editorconfig");566Ref<FileAccess> f = FileAccess::open(editor_config_path, FileAccess::WRITE);567if (f.is_null()) {568// .editorconfig isn't so critical.569ERR_PRINT("Couldn't create .editorconfig in project path.");570} else {571f->store_line("root = true");572f->store_line("");573f->store_line("[*]");574f->store_line("charset = utf-8");575f->close();576FileAccess::set_hidden_attribute(editor_config_path, true);577}578}579580// Two cases for importing a ZIP.581switch (mode) {582case MODE_IMPORT: {583if (zip_path.is_empty()) {584break;585}586587path = install_path->get_text().simplify_path();588[[fallthrough]];589}590case MODE_INSTALL: {591ERR_FAIL_COND(zip_path.is_empty());592593Ref<FileAccess> io_fa;594zlib_filefunc_def io = zipio_create_io(&io_fa);595596unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io);597if (!pkg) {598dialog_error->set_text(TTRC("Error opening package file, not in ZIP format."));599dialog_error->popup_centered();600return;601}602603// Find the first directory with a "project.godot".604String zip_root;605int ret = unzGoToFirstFile(pkg);606while (ret == UNZ_OK) {607unz_file_info info;608char fname[16384];609unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);610ERR_FAIL_COND_MSG(ret != UNZ_OK, "Failed to get current file info.");611612String name = String::utf8(fname);613614// Skip the __MACOSX directory created by macOS's built-in file zipper.615if (name.begins_with("__MACOSX")) {616ret = unzGoToNextFile(pkg);617continue;618}619620if (name.get_file() == "project.godot") {621zip_root = name.get_base_dir();622break;623}624625ret = unzGoToNextFile(pkg);626}627628if (ret == UNZ_END_OF_LIST_OF_FILE) {629_set_message(TTRC("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR);630unzClose(pkg);631return;632}633634if (create_dir->is_pressed()) {635Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);636if (!d->dir_exists(path) && d->make_dir(path) != OK) {637_set_message(TTRC("Couldn't create project directory, check permissions."), MESSAGE_ERROR);638return;639}640}641642ret = unzGoToFirstFile(pkg);643644Vector<String> failed_files;645while (ret == UNZ_OK) {646//get filename647unz_file_info info;648char fname[16384];649ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);650ERR_FAIL_COND_MSG(ret != UNZ_OK, "Failed to get current file info.");651652String name = String::utf8(fname);653654// Skip the __MACOSX directory created by macOS's built-in file zipper.655if (name.begins_with("__MACOSX")) {656ret = unzGoToNextFile(pkg);657continue;658}659660String rel_path = name.trim_prefix(zip_root);661if (rel_path.is_empty()) { // Root.662} else if (rel_path.ends_with("/")) { // Directory.663Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);664da->make_dir(path.path_join(rel_path));665} else { // File.666Vector<uint8_t> uncomp_data;667uncomp_data.resize(info.uncompressed_size);668669unzOpenCurrentFile(pkg);670ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size());671ERR_BREAK_MSG(ret < 0, vformat("An error occurred while attempting to read from file: %s. This file will not be used.", rel_path));672unzCloseCurrentFile(pkg);673674Ref<FileAccess> f = FileAccess::open(path.path_join(rel_path), FileAccess::WRITE);675if (f.is_valid()) {676f->store_buffer(uncomp_data.ptr(), uncomp_data.size());677} else {678failed_files.push_back(rel_path);679}680}681682ret = unzGoToNextFile(pkg);683}684685unzClose(pkg);686687if (failed_files.size()) {688String err_msg = TTR("The following files failed extraction from package:") + "\n\n";689for (int i = 0; i < failed_files.size(); i++) {690if (i > 15) {691err_msg += "\nAnd " + itos(failed_files.size() - i) + " more files.";692break;693}694err_msg += failed_files[i] + "\n";695}696697dialog_error->set_text(err_msg);698dialog_error->popup_centered();699return;700}701} break;702default: {703} break;704}705706if (mode == MODE_DUPLICATE) {707Ref<DirAccess> dir = DirAccess::open(original_project_path);708Error err = FAILED;709if (dir.is_valid()) {710err = dir->copy_dir(".", path, -1, true);711}712if (err != OK) {713dialog_error->set_text(vformat(TTR("Couldn't duplicate project (error %d)."), err));714dialog_error->popup_centered();715return;716}717}718719if (mode == MODE_RENAME || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {720// Load project.godot as ConfigFile to set the new name.721ConfigFile cfg;722String project_godot = path.path_join("project.godot");723Error err = cfg.load(project_godot);724if (err != OK) {725dialog_error->set_text(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err));726dialog_error->popup_centered();727return;728}729cfg.set_value("application", "config/name", project_name->get_text().strip_edges());730err = cfg.save(project_godot);731if (err != OK) {732dialog_error->set_text(vformat(TTR("Couldn't save project at '%s' (error %d)."), project_godot, err));733dialog_error->popup_centered();734return;735}736}737738hide();739if (mode == MODE_NEW || mode == MODE_IMPORT || mode == MODE_INSTALL) {740#ifdef ANDROID_ENABLED741// Create a .nomedia file to hide assets from media apps on Android.742// Android 11 has some issues with nomedia files, so it's disabled there. See GH-106479, GH-105399 for details.743// NOTE: Nomedia file is also handled during the first filesystem scan. See editor_file_system.cpp -> EditorFileSystem::scan().744String sdk_version = OS::get_singleton()->get_version().get_slicec('.', 0);745if (sdk_version != "30") {746const String nomedia_file_path = path.path_join(".nomedia");747Ref<FileAccess> f2 = FileAccess::open(nomedia_file_path, FileAccess::WRITE);748if (f2.is_null()) {749// .nomedia isn't so critical.750ERR_PRINT("Couldn't create .nomedia in project path.");751} else {752f2->close();753}754}755#endif756emit_signal(SNAME("project_created"), path, edit_check_box->is_pressed());757} else if (mode == MODE_DUPLICATE) {758emit_signal(SNAME("project_duplicated"), original_project_path, path, edit_check_box->is_visible() && edit_check_box->is_pressed());759} else if (mode == MODE_RENAME) {760emit_signal(SNAME("projects_updated"));761}762}763764void ProjectDialog::set_zip_path(const String &p_path) {765zip_path = p_path;766}767768void ProjectDialog::set_zip_title(const String &p_title) {769zip_title = p_title;770}771772void ProjectDialog::set_original_project_path(const String &p_path) {773original_project_path = p_path;774}775776void ProjectDialog::set_duplicate_can_edit(bool p_duplicate_can_edit) {777duplicate_can_edit = p_duplicate_can_edit;778}779780void ProjectDialog::set_mode(Mode p_mode) {781mode = p_mode;782}783784void ProjectDialog::set_project_name(const String &p_name) {785project_name->set_text(p_name);786}787788void ProjectDialog::set_project_path(const String &p_path) {789project_path->set_text(p_path);790}791792void ProjectDialog::ask_for_path_and_show() {793_reset_name();794_browse_project_path();795}796797void ProjectDialog::show_dialog(bool p_reset_name) {798if (mode == MODE_RENAME) {799// Name and path are set in `ProjectManager::_rename_project`.800project_path->set_editable(false);801802set_title(TTRC("Rename Project"));803set_ok_button_text(TTRC("Rename"));804805create_dir->hide();806project_status_rect->hide();807project_browse->hide();808edit_check_box->hide();809810name_container->show();811install_path_container->hide();812renderer_container->hide();813default_files_container->hide();814815callable_mp((Control *)project_name, &Control::grab_focus).call_deferred();816callable_mp(project_name, &LineEdit::select_all).call_deferred();817} else {818if (p_reset_name) {819_reset_name();820}821project_path->set_editable(true);822823if (mode == MODE_DUPLICATE) {824String original_dir = original_project_path.get_base_dir();825project_path->set_text(original_dir);826install_path->set_text(original_dir);827fdialog_project->set_current_dir(original_dir);828} else {829String fav_dir = EDITOR_GET("filesystem/directories/default_project_path");830fav_dir = fav_dir.simplify_path();831if (!fav_dir.is_empty()) {832project_path->set_text(fav_dir);833install_path->set_text(fav_dir);834fdialog_project->set_current_dir(fav_dir);835} else {836Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);837project_path->set_text(d->get_current_dir());838install_path->set_text(d->get_current_dir());839fdialog_project->set_current_dir(d->get_current_dir());840}841}842843create_dir->show();844project_status_rect->show();845project_browse->show();846edit_check_box->show();847848if (mode == MODE_IMPORT) {849set_title(TTRC("Import Existing Project"));850set_ok_button_text(TTRC("Import"));851852name_container->hide();853install_path_container->hide();854renderer_container->hide();855default_files_container->hide();856857// Project path dialog is also opened; no need to change focus.858} else if (mode == MODE_NEW) {859set_title(TTRC("Create New Project"));860set_ok_button_text(TTRC("Create"));861862name_container->show();863install_path_container->hide();864renderer_container->show();865default_files_container->show();866867callable_mp((Control *)project_name, &Control::grab_focus).call_deferred();868callable_mp(project_name, &LineEdit::select_all).call_deferred();869} else if (mode == MODE_INSTALL) {870set_title(TTR("Install Project:") + " " + zip_title);871set_ok_button_text(TTRC("Install"));872873project_name->set_text(zip_title);874875name_container->show();876install_path_container->hide();877renderer_container->hide();878default_files_container->hide();879880callable_mp((Control *)project_path, &Control::grab_focus).call_deferred();881} else if (mode == MODE_DUPLICATE) {882set_title(TTRC("Duplicate Project"));883set_ok_button_text(TTRC("Duplicate"));884885name_container->show();886install_path_container->hide();887renderer_container->hide();888default_files_container->hide();889if (!duplicate_can_edit) {890edit_check_box->hide();891}892893callable_mp((Control *)project_name, &Control::grab_focus).call_deferred();894callable_mp(project_name, &LineEdit::select_all).call_deferred();895}896897auto_dir = "";898last_custom_target_dir = "";899_update_target_auto_dir();900if (create_dir->is_pressed()) {901// Append `auto_dir` to target path.902_create_dir_toggled(true);903}904}905906_validate_path();907908popup_centered(Size2(500, 0) * EDSCALE);909}910911void ProjectDialog::_notification(int p_what) {912switch (p_what) {913case NOTIFICATION_TRANSLATION_CHANGED: {914_renderer_selected();915} break;916917case NOTIFICATION_THEME_CHANGED: {918create_dir->set_button_icon(get_editor_theme_icon(SNAME("FolderCreate")));919project_browse->set_button_icon(get_editor_theme_icon(SNAME("FolderBrowse")));920install_browse->set_button_icon(get_editor_theme_icon(SNAME("FolderBrowse")));921} break;922case NOTIFICATION_READY: {923fdialog_project = memnew(EditorFileDialog);924fdialog_project->set_previews_enabled(false); // Crucial, otherwise the engine crashes.925fdialog_project->set_access(EditorFileDialog::ACCESS_FILESYSTEM);926fdialog_project->connect("dir_selected", callable_mp(this, &ProjectDialog::_project_path_selected));927fdialog_project->connect("file_selected", callable_mp(this, &ProjectDialog::_project_path_selected));928fdialog_project->connect("canceled", callable_mp(this, &ProjectDialog::show_dialog).bind(false), CONNECT_DEFERRED);929callable_mp((Node *)this, &Node::add_sibling).call_deferred(fdialog_project, false);930} break;931}932}933934void ProjectDialog::_bind_methods() {935ADD_SIGNAL(MethodInfo("project_created"));936ADD_SIGNAL(MethodInfo("project_duplicated"));937ADD_SIGNAL(MethodInfo("projects_updated"));938}939940ProjectDialog::ProjectDialog() {941VBoxContainer *vb = memnew(VBoxContainer);942add_child(vb);943944name_container = memnew(VBoxContainer);945vb->add_child(name_container);946947Label *l = memnew(Label);948l->set_text(TTRC("Project Name:"));949name_container->add_child(l);950951project_name = memnew(LineEdit);952project_name->set_virtual_keyboard_show_on_focus(false);953project_name->set_h_size_flags(Control::SIZE_EXPAND_FILL);954name_container->add_child(project_name);955956project_path_container = memnew(VBoxContainer);957vb->add_child(project_path_container);958959HBoxContainer *pphb_label = memnew(HBoxContainer);960project_path_container->add_child(pphb_label);961962l = memnew(Label);963l->set_text(TTRC("Project Path:"));964l->set_h_size_flags(Control::SIZE_EXPAND_FILL);965pphb_label->add_child(l);966967create_dir = memnew(CheckButton);968create_dir->set_text(TTRC("Create Folder"));969create_dir->set_pressed(true);970pphb_label->add_child(create_dir);971create_dir->connect(SceneStringName(toggled), callable_mp(this, &ProjectDialog::_create_dir_toggled));972973HBoxContainer *pphb = memnew(HBoxContainer);974project_path_container->add_child(pphb);975976project_path = memnew(LineEdit);977project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);978project_path->set_accessibility_name(TTRC("Project Path:"));979project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);980pphb->add_child(project_path);981982install_path_container = memnew(VBoxContainer);983vb->add_child(install_path_container);984985l = memnew(Label);986l->set_text(TTRC("Project Installation Path:"));987install_path_container->add_child(l);988989HBoxContainer *iphb = memnew(HBoxContainer);990install_path_container->add_child(iphb);991992install_path = memnew(LineEdit);993install_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);994install_path->set_accessibility_name(TTRC("Project Installation Path:"));995install_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);996iphb->add_child(install_path);997998// status icon999project_status_rect = memnew(TextureRect);1000project_status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);1001pphb->add_child(project_status_rect);10021003project_browse = memnew(Button);1004project_browse->set_text(TTRC("Browse"));1005project_browse->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_browse_project_path));1006pphb->add_child(project_browse);10071008// install status icon1009install_status_rect = memnew(TextureRect);1010install_status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);1011iphb->add_child(install_status_rect);10121013install_browse = memnew(Button);1014install_browse->set_text(TTRC("Browse"));1015install_browse->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_browse_install_path));1016iphb->add_child(install_browse);10171018msg = memnew(Label);1019msg->set_focus_mode(Control::FOCUS_ACCESSIBILITY);1020msg->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);1021msg->set_custom_minimum_size(Size2(200, 0) * EDSCALE);1022msg->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);1023vb->add_child(msg);10241025// Renderer selection.1026renderer_container = memnew(VBoxContainer);1027vb->add_child(renderer_container);1028l = memnew(Label);1029l->set_text(TTRC("Renderer:"));1030renderer_container->add_child(l);1031HBoxContainer *rshc = memnew(HBoxContainer);1032renderer_container->add_child(rshc);1033renderer_button_group.instantiate();10341035// Left hand side, used for checkboxes to select renderer.1036Container *rvb = memnew(VBoxContainer);1037rshc->add_child(rvb);10381039String default_renderer_type = "forward_plus";1040if (EditorSettings::get_singleton()->has_setting("project_manager/default_renderer")) {1041default_renderer_type = EditorSettings::get_singleton()->get_setting("project_manager/default_renderer");1042}10431044rendering_device_supported = DisplayServer::is_rendering_device_supported();10451046if (!rendering_device_supported) {1047default_renderer_type = "gl_compatibility";1048}10491050Button *rs_button = memnew(CheckBox);1051rs_button->set_button_group(renderer_button_group);1052rs_button->set_text(TTRC("Forward+"));1053#ifndef RD_ENABLED1054rs_button->set_disabled(true);1055#endif1056rs_button->set_meta(SNAME("rendering_method"), "forward_plus");1057rs_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_renderer_selected));1058rvb->add_child(rs_button);1059if (default_renderer_type == "forward_plus") {1060rs_button->set_pressed(true);1061}1062rs_button = memnew(CheckBox);1063rs_button->set_button_group(renderer_button_group);1064rs_button->set_text(TTRC("Mobile"));1065#ifndef RD_ENABLED1066rs_button->set_disabled(true);1067#endif1068rs_button->set_meta(SNAME("rendering_method"), "mobile");1069rs_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_renderer_selected));1070rvb->add_child(rs_button);1071if (default_renderer_type == "mobile") {1072rs_button->set_pressed(true);1073}1074rs_button = memnew(CheckBox);1075rs_button->set_button_group(renderer_button_group);1076rs_button->set_text(TTRC("Compatibility"));1077#if !defined(GLES3_ENABLED)1078rs_button->set_disabled(true);1079#endif1080rs_button->set_meta(SNAME("rendering_method"), "gl_compatibility");1081rs_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_renderer_selected));1082rvb->add_child(rs_button);1083#if defined(GLES3_ENABLED)1084if (default_renderer_type == "gl_compatibility") {1085rs_button->set_pressed(true);1086}1087#endif1088rshc->add_child(memnew(VSeparator));10891090// Right hand side, used for text explaining each choice.1091rvb = memnew(VBoxContainer);1092rvb->set_h_size_flags(Control::SIZE_EXPAND_FILL);1093rshc->add_child(rvb);1094renderer_info = memnew(Label);1095renderer_info->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);1096renderer_info->set_focus_mode(Control::FOCUS_ACCESSIBILITY);1097renderer_info->set_modulate(Color(1, 1, 1, 0.7));1098rvb->add_child(renderer_info);10991100rd_not_supported = memnew(Label);1101rd_not_supported->set_focus_mode(Control::FOCUS_ACCESSIBILITY);1102rd_not_supported->set_text(vformat(TTRC("RenderingDevice-based methods not available on this GPU:\n%s\nPlease use the Compatibility renderer."), RenderingServer::get_singleton()->get_video_adapter_name()));1103rd_not_supported->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);1104rd_not_supported->set_custom_minimum_size(Size2(200, 0) * EDSCALE);1105rd_not_supported->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);1106rd_not_supported->set_visible(false);1107renderer_container->add_child(rd_not_supported);11081109_renderer_selected();11101111l = memnew(Label);1112l->set_focus_mode(Control::FOCUS_ACCESSIBILITY);1113l->set_text(TTRC("The renderer can be changed later, but scenes may need to be adjusted."));1114// Add some extra spacing to separate it from the list above and the buttons below.1115l->set_custom_minimum_size(Size2(0, 40) * EDSCALE);1116l->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);1117l->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);1118l->set_modulate(Color(1, 1, 1, 0.7));1119renderer_container->add_child(l);11201121default_files_container = memnew(HBoxContainer);1122vb->add_child(default_files_container);1123l = memnew(Label);1124l->set_text(TTRC("Version Control Metadata:"));1125default_files_container->add_child(l);1126vcs_metadata_selection = memnew(OptionButton);1127vcs_metadata_selection->set_custom_minimum_size(Size2(100, 20));1128vcs_metadata_selection->add_item(TTRC("None"), (int)EditorVCSInterface::VCSMetadata::NONE);1129vcs_metadata_selection->add_item(TTRC("Git"), (int)EditorVCSInterface::VCSMetadata::GIT);1130vcs_metadata_selection->select((int)EditorVCSInterface::VCSMetadata::GIT);1131vcs_metadata_selection->set_accessibility_name(TTRC("Version Control Metadata:"));1132default_files_container->add_child(vcs_metadata_selection);1133Control *spacer = memnew(Control);1134spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);1135default_files_container->add_child(spacer);1136fdialog_install = memnew(EditorFileDialog);1137fdialog_install->set_previews_enabled(false); //Crucial, otherwise the engine crashes.1138fdialog_install->set_access(EditorFileDialog::ACCESS_FILESYSTEM);1139add_child(fdialog_install);11401141Control *spacer2 = memnew(Control);1142spacer2->set_v_size_flags(Control::SIZE_EXPAND_FILL);1143vb->add_child(spacer2);11441145edit_check_box = memnew(CheckBox);1146edit_check_box->set_text(TTRC("Edit Now"));1147edit_check_box->set_h_size_flags(Control::SIZE_SHRINK_CENTER);1148edit_check_box->set_pressed(true);1149vb->add_child(edit_check_box);11501151project_name->connect(SceneStringName(text_changed), callable_mp(this, &ProjectDialog::_project_name_changed).unbind(1));1152project_name->connect(SceneStringName(text_submitted), callable_mp(this, &ProjectDialog::ok_pressed).unbind(1));11531154project_path->connect(SceneStringName(text_changed), callable_mp(this, &ProjectDialog::_project_path_changed).unbind(1));1155project_path->connect(SceneStringName(text_submitted), callable_mp(this, &ProjectDialog::ok_pressed).unbind(1));11561157install_path->connect(SceneStringName(text_changed), callable_mp(this, &ProjectDialog::_install_path_changed).unbind(1));1158install_path->connect(SceneStringName(text_submitted), callable_mp(this, &ProjectDialog::ok_pressed).unbind(1));11591160fdialog_install->connect("dir_selected", callable_mp(this, &ProjectDialog::_install_path_selected));1161fdialog_install->connect("file_selected", callable_mp(this, &ProjectDialog::_install_path_selected));11621163set_hide_on_ok(false);11641165dialog_error = memnew(AcceptDialog);1166add_child(dialog_error);1167}116811691170