Path: blob/master/editor/project_manager/project_dialog.cpp
21055 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_node.h"37#include "editor/editor_string_names.h"38#include "editor/gui/editor_file_dialog.h"39#include "editor/settings/editor_settings.h"40#include "editor/themes/editor_icons.h"41#include "editor/themes/editor_scale.h"42#include "editor/version_control/editor_vcs_interface.h"43#include "scene/gui/check_box.h"44#include "scene/gui/check_button.h"45#include "scene/gui/line_edit.h"46#include "scene/gui/link_button.h"47#include "scene/gui/option_button.h"48#include "scene/gui/separator.h"49#include "scene/gui/texture_rect.h"5051void ProjectDialog::_set_message(const String &p_msg, MessageType p_type, InputType p_input_type) {52msg->set_text(p_msg);5354if (p_type == MESSAGE_ERROR) {55invalid_state_flags.set_flag(InvalidStateFlag::INVALID_STATE_FLAG_PATH_INPUT);56} else {57invalid_state_flags.clear_flag(InvalidStateFlag::INVALID_STATE_FLAG_PATH_INPUT);58}5960Ref<Texture2D> new_icon;61switch (p_type) {62case MESSAGE_ERROR: {63msg->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor)));64new_icon = get_editor_theme_icon(SNAME("StatusError"));65} break;66case MESSAGE_WARNING: {67msg->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));68new_icon = get_editor_theme_icon(SNAME("StatusWarning"));69} break;70case MESSAGE_SUCCESS: {71msg->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("success_color"), EditorStringName(Editor)));72new_icon = get_editor_theme_icon(SNAME("StatusSuccess"));73} break;74}7576if (p_input_type == PROJECT_PATH) {77project_status_rect->set_texture(new_icon);78} else if (p_input_type == INSTALL_PATH) {79install_status_rect->set_texture(new_icon);80}8182_update_ok_button();83}8485void ProjectDialog::_update_ok_button() {86get_ok_button()->set_disabled(!invalid_state_flags.is_empty());87}8889static bool is_zip_file(Ref<DirAccess> p_d, const String &p_path) {90return p_path.get_extension() == "zip" && p_d->file_exists(p_path);91}9293void ProjectDialog::_validate_path() {94_set_message("", MESSAGE_SUCCESS, PROJECT_PATH);95_set_message("", MESSAGE_SUCCESS, INSTALL_PATH);9697if (project_name->get_text().strip_edges().is_empty()) {98_set_message(TTRC("It would be a good idea to name your project."), MESSAGE_ERROR);99return;100}101102Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);103String path = project_path->get_text().simplify_path();104105String target_path = path;106InputType target_path_input_type = PROJECT_PATH;107108if (mode == MODE_IMPORT) {109if (path.get_file().strip_edges() == "project.godot") {110path = path.get_base_dir();111project_path->set_text(path);112}113114if (is_zip_file(d, path)) {115zip_path = path;116} else if (is_zip_file(d, path.strip_edges())) {117zip_path = path.strip_edges();118} else {119zip_path = "";120}121122if (!zip_path.is_empty()) {123target_path = install_path->get_text().simplify_path();124target_path_input_type = INSTALL_PATH;125126create_dir->show();127install_path_container->show();128129Ref<FileAccess> io_fa;130zlib_filefunc_def io = zipio_create_io(&io_fa);131132unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io);133if (!pkg) {134_set_message(TTRC("Invalid \".zip\" project file; it is not in ZIP format."), MESSAGE_ERROR);135unzClose(pkg);136return;137}138139int ret = unzGoToFirstFile(pkg);140while (ret == UNZ_OK) {141unz_file_info info;142char fname[16384];143ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);144ERR_FAIL_COND_MSG(ret != UNZ_OK, "Failed to get current file info.");145146String name = String::utf8(fname);147148// Skip the __MACOSX directory created by macOS's built-in file zipper.149if (name.begins_with("__MACOSX")) {150ret = unzGoToNextFile(pkg);151continue;152}153154if (name.get_file() == "project.godot") {155break; // ret == UNZ_OK.156}157158ret = unzGoToNextFile(pkg);159}160161if (ret == UNZ_END_OF_LIST_OF_FILE) {162_set_message(TTRC("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR);163unzClose(pkg);164return;165}166167unzClose(pkg);168} else if (d->dir_exists(path) && d->file_exists(path.path_join("project.godot"))) {169zip_path = "";170171create_dir->hide();172install_path_container->hide();173174_set_message(TTRC("Valid project found at path."), MESSAGE_SUCCESS);175} else {176create_dir->hide();177install_path_container->hide();178179_set_message(TTRC("Please choose a \"project.godot\", a directory with one, or a \".zip\" file."), MESSAGE_ERROR);180return;181}182}183184if (target_path.is_relative_path()) {185_set_message(TTRC("The path specified is invalid."), MESSAGE_ERROR, target_path_input_type);186return;187}188189if (target_path.get_file() != OS::get_singleton()->get_safe_dir_name(target_path.get_file())) {190_set_message(TTRC("The directory name specified contains invalid characters or trailing whitespace."), MESSAGE_ERROR, target_path_input_type);191return;192}193194String working_dir = d->get_current_dir();195String executable_dir = OS::get_singleton()->get_executable_path().get_base_dir();196if (target_path == working_dir || target_path == executable_dir) {197_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);198return;199}200201// 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.202#ifdef WINDOWS_ENABLED203String home_dir = OS::get_singleton()->get_environment("USERPROFILE");204#else205String home_dir = OS::get_singleton()->get_environment("HOME");206#endif207String documents_dir = OS::get_singleton()->get_system_dir(OS::SYSTEM_DIR_DOCUMENTS);208if (target_path == home_dir || target_path == documents_dir) {209_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);210return;211}212213is_folder_empty = true;214if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE || (mode == MODE_IMPORT && target_path_input_type == InputType::INSTALL_PATH)) {215if (create_dir->is_pressed()) {216if (!d->dir_exists(target_path.get_base_dir())) {217_set_message(TTRC("The parent directory of the path specified doesn't exist."), MESSAGE_ERROR, target_path_input_type);218return;219}220221if (d->dir_exists(target_path)) {222// The path is not necessarily empty here, but we will update the message later if it isn't.223_set_message(TTRC("The project folder already exists and is empty."), MESSAGE_SUCCESS, target_path_input_type);224} else {225_set_message(TTRC("The project folder will be automatically created."), MESSAGE_SUCCESS, target_path_input_type);226}227} else {228if (!d->dir_exists(target_path)) {229_set_message(TTRC("The path specified doesn't exist."), MESSAGE_ERROR, target_path_input_type);230return;231}232233// The path is not necessarily empty here, but we will update the message later if it isn't.234_set_message(TTRC("The project folder exists and is empty."), MESSAGE_SUCCESS, target_path_input_type);235}236237// Check if the directory is empty. Not an error, but we want to warn the user.238if (d->change_dir(target_path) == OK) {239d->list_dir_begin();240String n = d->get_next();241while (!n.is_empty()) {242if (n[0] != '.') {243// Allow `.`, `..` (reserved current/parent folder names)244// and hidden files/folders to be present.245// For instance, this lets users initialize a Git repository246// and still be able to create a project in the directory afterwards.247is_folder_empty = false;248break;249}250n = d->get_next();251}252d->list_dir_end();253254if (!is_folder_empty) {255_set_message(TTRC("The selected path is not empty. Choosing an empty folder is highly recommended."), MESSAGE_WARNING, target_path_input_type);256}257}258}259260// Check if the target path is a subdirectory of original when duplicating261if (mode == MODE_DUPLICATE) {262String base_path = original_project_path;263String duplicate_target = target_path;264265// Ensure the paths end with a slash266if (!base_path.ends_with("/")) {267base_path += "/";268}269270if (!duplicate_target.ends_with("/")) {271duplicate_target += "/";272}273274bool is_subdirectory_or_equal;275276if (d->is_case_sensitive(base_path) || d->is_case_sensitive(duplicate_target)) {277is_subdirectory_or_equal = duplicate_target.begins_with(base_path);278} else {279base_path = base_path.to_lower();280String target_lower = duplicate_target.to_lower();281is_subdirectory_or_equal = target_lower.begins_with(base_path);282}283284if (is_subdirectory_or_equal) {285_set_message(TTRC("Cannot duplicate a project into itself."), MESSAGE_ERROR, target_path_input_type);286}287}288}289290String ProjectDialog::_get_target_path() {291if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {292return project_path->get_text();293} else if (mode == MODE_IMPORT) {294return install_path->get_text();295} else {296ERR_FAIL_V("");297}298}299void ProjectDialog::_set_target_path(const String &p_text) {300if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {301project_path->set_text(p_text);302} else if (mode == MODE_IMPORT) {303install_path->set_text(p_text);304} else {305ERR_FAIL();306}307}308309void ProjectDialog::_update_target_auto_dir() {310String new_auto_dir;311if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {312new_auto_dir = project_name->get_text();313} else if (mode == MODE_IMPORT) {314new_auto_dir = project_path->get_text().get_file().get_basename();315}316int naming_convention = (int)EDITOR_GET("project_manager/directory_naming_convention");317switch (naming_convention) {318case 0: // No Convention319break;320case 1: // kebab-case321new_auto_dir = new_auto_dir.to_kebab_case();322break;323case 2: // snake_case324new_auto_dir = new_auto_dir.to_snake_case();325break;326case 3: // camelCase327new_auto_dir = new_auto_dir.to_camel_case();328break;329case 4: // PascalCase330new_auto_dir = new_auto_dir.to_pascal_case();331break;332case 5: // Title Case333new_auto_dir = new_auto_dir.capitalize();334break;335default:336ERR_FAIL_MSG("Invalid directory naming convention.");337break;338}339new_auto_dir = OS::get_singleton()->get_safe_dir_name(new_auto_dir);340341if (create_dir->is_pressed()) {342String target_path = _get_target_path();343344if (target_path.get_file() == auto_dir) {345// Update target dir name to new project name / ZIP name.346target_path = target_path.get_base_dir().path_join(new_auto_dir);347}348349_set_target_path(target_path);350}351352auto_dir = new_auto_dir;353}354355void ProjectDialog::_create_dir_toggled(bool p_pressed) {356String target_path = _get_target_path();357358if (create_dir->is_pressed()) {359// (Re-)append target dir name.360if (last_custom_target_dir.is_empty()) {361target_path = target_path.path_join(auto_dir);362} else {363target_path = target_path.path_join(last_custom_target_dir);364}365} else {366// Strip any trailing slash.367target_path = target_path.rstrip("/\\");368// Save and remove target dir name.369if (target_path.get_file() == auto_dir) {370last_custom_target_dir = "";371} else {372last_custom_target_dir = target_path.get_file();373}374target_path = target_path.get_base_dir();375}376377_set_target_path(target_path);378_validate_path();379}380381void ProjectDialog::_project_name_changed() {382if (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {383_update_target_auto_dir();384}385386_validate_path();387}388389void ProjectDialog::_project_path_changed() {390if (mode == MODE_IMPORT) {391_update_target_auto_dir();392}393394_validate_path();395}396397void ProjectDialog::_install_path_changed() {398_validate_path();399}400401void ProjectDialog::_browse_project_path() {402String path = project_path->get_text();403if (path.is_relative_path()) {404path = EDITOR_GET("filesystem/directories/default_project_path");405}406if (mode == MODE_IMPORT && install_path->is_visible_in_tree()) {407// Select last ZIP file.408fdialog_project->set_current_path(path);409} else if ((mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE) && create_dir->is_pressed()) {410// Select parent directory of project path.411fdialog_project->set_current_dir(path.get_base_dir());412} else {413// Select project path.414fdialog_project->set_current_dir(path);415}416417if (mode == MODE_IMPORT) {418fdialog_project->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_ANY);419fdialog_project->clear_filters();420fdialog_project->add_filter("project.godot", vformat("%s %s", GODOT_VERSION_NAME, TTR("Project")));421fdialog_project->add_filter("*.zip", TTR("ZIP File"));422} else {423fdialog_project->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);424}425426hide();427fdialog_project->popup_file_dialog();428}429430void ProjectDialog::_browse_install_path() {431ERR_FAIL_COND_MSG(mode != MODE_IMPORT, "Install path is only used for MODE_IMPORT.");432433String path = install_path->get_text();434if (path.is_relative_path() || !DirAccess::dir_exists_absolute(path)) {435path = EDITOR_GET("filesystem/directories/default_project_path");436}437if (create_dir->is_pressed()) {438// Select parent directory of install path.439fdialog_install->set_current_dir(path.get_base_dir());440} else {441// Select install path.442fdialog_install->set_current_dir(path);443}444445fdialog_install->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);446fdialog_install->popup_file_dialog();447}448449void ProjectDialog::_project_path_selected(const String &p_path) {450show_dialog(false);451452if (create_dir->is_pressed() && (mode == MODE_NEW || mode == MODE_INSTALL || mode == MODE_DUPLICATE)) {453// Replace parent directory, but keep target dir name.454project_path->set_text(p_path.path_join(project_path->get_text().get_file()));455} else {456project_path->set_text(p_path);457}458459_project_path_changed();460461if (install_path->is_visible_in_tree()) {462// ZIP is selected; focus install path.463install_path->grab_focus();464} else {465get_ok_button()->grab_focus();466}467}468469void ProjectDialog::_install_path_selected(const String &p_path) {470ERR_FAIL_COND_MSG(mode != MODE_IMPORT, "Install path is only used for MODE_IMPORT.");471472if (create_dir->is_pressed()) {473// Replace parent directory, but keep target dir name.474install_path->set_text(p_path.path_join(install_path->get_text().get_file()));475} else {476install_path->set_text(p_path);477}478479_install_path_changed();480481get_ok_button()->grab_focus();482}483484void ProjectDialog::_reset_name() {485project_name->set_text(TTR("New Game Project"));486}487488void ProjectDialog::_renderer_selected() {489ERR_FAIL_NULL(renderer_button_group->get_pressed_button());490491String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));492493bool rd_error = false;494495if (renderer_type == "forward_plus") {496renderer_info->set_text(497String::utf8("• ") + TTR("Supports desktop platforms only.") +498String::utf8("\n• ") + TTR("Advanced 3D graphics available.") +499String::utf8("\n• ") + TTR("Can scale to large complex scenes.") +500String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +501String::utf8("\n• ") + TTR("Slower rendering of simple scenes."));502rd_error = !rendering_device_supported;503} else if (renderer_type == "mobile") {504renderer_info->set_text(505String::utf8("• ") + TTR("Supports desktop + mobile platforms.") +506String::utf8("\n• ") + TTR("Less advanced 3D graphics.") +507String::utf8("\n• ") + TTR("Less scalable for complex scenes.") +508String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +509String::utf8("\n• ") + TTR("Fast rendering of simple scenes."));510rd_error = !rendering_device_supported;511} else if (renderer_type == "gl_compatibility") {512renderer_info->set_text(513String::utf8("• ") + TTR("Supports desktop, mobile + web platforms.") +514String::utf8("\n• ") + TTR("Least advanced 3D graphics.") +515String::utf8("\n• ") + TTR("Intended for low-end/older devices.") +516String::utf8("\n• ") + TTR("Uses OpenGL 3 backend (OpenGL 3.3/ES 3.0/WebGL2).") +517String::utf8("\n• ") + TTR("Fastest rendering of simple scenes."));518} else {519WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");520}521522rd_not_supported->set_visible(rd_error);523if (rd_error) {524// Needs to be set here since theme colors aren't available at startup.525rd_not_supported->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor)));526invalid_state_flags.set_flag(InvalidStateFlag::INVALID_STATE_FLAG_RENDERER_SELECT);527} else {528invalid_state_flags.clear_flag(InvalidStateFlag::INVALID_STATE_FLAG_RENDERER_SELECT);529}530531_update_ok_button();532}533534void ProjectDialog::_nonempty_confirmation_ok_pressed() {535is_folder_empty = true;536ok_pressed();537}538539void ProjectDialog::ok_pressed() {540// Before we create a project, check that the target folder is empty.541// If not, we need to ask the user if they're sure they want to do this.542if (!is_folder_empty) {543if (!nonempty_confirmation) {544nonempty_confirmation = memnew(ConfirmationDialog);545nonempty_confirmation->set_title(TTRC("Warning: This folder is not empty"));546nonempty_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?"));547nonempty_confirmation->get_ok_button()->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_nonempty_confirmation_ok_pressed));548add_child(nonempty_confirmation);549}550nonempty_confirmation->popup_centered();551return;552}553554String path = project_path->get_text();555556if (mode == MODE_NEW) {557if (create_dir->is_pressed()) {558Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);559if (!d->dir_exists(path) && d->make_dir(path) != OK) {560_set_message(TTRC("Couldn't create project directory, check permissions."), MESSAGE_ERROR);561return;562}563}564565PackedStringArray project_features = ProjectSettings::get_required_features();566ProjectSettings::CustomMap initial_settings;567568// Be sure to change this code if/when renderers are changed.569// Default values are "forward_plus" for the main setting, "mobile" for the mobile override,570// and "gl_compatibility" for the web override.571String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));572initial_settings["rendering/renderer/rendering_method"] = renderer_type;573574EditorSettings::get_singleton()->set("project_manager/default_renderer", renderer_type);575EditorSettings::get_singleton()->save();576577if (renderer_type == "forward_plus") {578project_features.push_back("Forward Plus");579} else if (renderer_type == "mobile") {580project_features.push_back("Mobile");581} else if (renderer_type == "gl_compatibility") {582project_features.push_back("GL Compatibility");583// Also change the default renderer for the mobile override.584initial_settings["rendering/renderer/rendering_method.mobile"] = "gl_compatibility";585} else {586WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");587}588589project_features.sort();590initial_settings["application/config/features"] = project_features;591initial_settings["application/config/name"] = project_name->get_text().strip_edges();592initial_settings["application/config/icon"] = "res://icon.svg";593ProjectSettings::CustomMap extra_settings = EditorNode::get_initial_settings();594for (const KeyValue<String, Variant> &extra_setting : extra_settings) {595// Merge with other initial settings defined above.596initial_settings[extra_setting.key] = extra_setting.value;597}598599Error err = ProjectSettings::get_singleton()->save_custom(path.path_join("project.godot"), initial_settings, Vector<String>(), false);600if (err != OK) {601_set_message(TTRC("Couldn't create project.godot in project path."), MESSAGE_ERROR);602return;603}604605// Store default project icon in SVG format.606Ref<FileAccess> fa_icon = FileAccess::open(path.path_join("icon.svg"), FileAccess::WRITE, &err);607if (err != OK) {608_set_message(TTRC("Couldn't create icon.svg in project path."), MESSAGE_ERROR);609return;610}611fa_icon->store_string(get_default_project_icon());612613EditorVCSInterface::create_vcs_metadata_files(EditorVCSInterface::VCSMetadata(vcs_metadata_selection->get_selected()), path);614615// Ensures external editors and IDEs use UTF-8 encoding.616const String editor_config_path = path.path_join(".editorconfig");617Ref<FileAccess> f = FileAccess::open(editor_config_path, FileAccess::WRITE);618if (f.is_null()) {619// .editorconfig isn't so critical.620ERR_PRINT("Couldn't create .editorconfig in project path.");621} else {622f->store_line("root = true");623f->store_line("");624f->store_line("[*]");625f->store_line("charset = utf-8");626f->close();627FileAccess::set_hidden_attribute(editor_config_path, true);628}629}630631// Two cases for importing a ZIP.632switch (mode) {633case MODE_IMPORT: {634if (zip_path.is_empty()) {635break;636}637638path = install_path->get_text().simplify_path();639[[fallthrough]];640}641case MODE_INSTALL: {642ERR_FAIL_COND(zip_path.is_empty());643644Ref<FileAccess> io_fa;645zlib_filefunc_def io = zipio_create_io(&io_fa);646647unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io);648if (!pkg) {649dialog_error->set_text(TTRC("Error opening package file, not in ZIP format."));650dialog_error->popup_centered();651return;652}653654// Find the first directory with a "project.godot".655String zip_root;656int ret = unzGoToFirstFile(pkg);657while (ret == UNZ_OK) {658unz_file_info info;659char fname[16384];660unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);661ERR_FAIL_COND_MSG(ret != UNZ_OK, "Failed to get current file info.");662663String name = String::utf8(fname);664665// Skip the __MACOSX directory created by macOS's built-in file zipper.666if (name.begins_with("__MACOSX")) {667ret = unzGoToNextFile(pkg);668continue;669}670671if (name.get_file() == "project.godot") {672zip_root = name.get_base_dir();673break;674}675676ret = unzGoToNextFile(pkg);677}678679if (ret == UNZ_END_OF_LIST_OF_FILE) {680_set_message(TTRC("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR);681unzClose(pkg);682return;683}684685if (create_dir->is_pressed()) {686Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);687if (!d->dir_exists(path) && d->make_dir(path) != OK) {688_set_message(TTRC("Couldn't create project directory, check permissions."), MESSAGE_ERROR);689return;690}691}692693ret = unzGoToFirstFile(pkg);694695Vector<String> failed_files;696while (ret == UNZ_OK) {697//get filename698unz_file_info info;699char fname[16384];700ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);701ERR_FAIL_COND_MSG(ret != UNZ_OK, "Failed to get current file info.");702703String name = String::utf8(fname);704705// Skip the __MACOSX directory created by macOS's built-in file zipper.706if (name.begins_with("__MACOSX")) {707ret = unzGoToNextFile(pkg);708continue;709}710711String rel_path = name.trim_prefix(zip_root);712if (rel_path.is_empty()) { // Root.713} else if (rel_path.ends_with("/")) { // Directory.714Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);715da->make_dir(path.path_join(rel_path));716} else { // File.717Vector<uint8_t> uncomp_data;718uncomp_data.resize(info.uncompressed_size);719720unzOpenCurrentFile(pkg);721ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size());722ERR_BREAK_MSG(ret < 0, vformat("An error occurred while attempting to read from file: %s. This file will not be used.", rel_path));723unzCloseCurrentFile(pkg);724725Ref<FileAccess> f = FileAccess::open(path.path_join(rel_path), FileAccess::WRITE);726if (f.is_valid()) {727f->store_buffer(uncomp_data.ptr(), uncomp_data.size());728} else {729failed_files.push_back(rel_path);730}731}732733ret = unzGoToNextFile(pkg);734}735736unzClose(pkg);737738if (failed_files.size()) {739String err_msg = TTR("The following files failed extraction from package:") + "\n\n";740for (int i = 0; i < failed_files.size(); i++) {741if (i > 15) {742err_msg += "\nAnd " + itos(failed_files.size() - i) + " more files.";743break;744}745err_msg += failed_files[i] + "\n";746}747748dialog_error->set_text(err_msg);749dialog_error->popup_centered();750return;751}752} break;753default: {754} break;755}756757if (mode == MODE_DUPLICATE) {758Ref<DirAccess> dir = DirAccess::open(original_project_path);759Error err = FAILED;760if (dir.is_valid()) {761err = dir->copy_dir(".", path, -1, true);762}763if (err != OK) {764dialog_error->set_text(vformat(TTR("Couldn't duplicate project (error %d)."), err));765dialog_error->popup_centered();766return;767}768}769770if (mode == MODE_RENAME || mode == MODE_INSTALL || mode == MODE_DUPLICATE) {771// Load project.godot as ConfigFile to set the new name.772ConfigFile cfg;773String project_godot = path.path_join("project.godot");774Error err = cfg.load(project_godot);775if (err != OK) {776dialog_error->set_text(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err));777dialog_error->popup_centered();778return;779}780cfg.set_value("application", "config/name", project_name->get_text().strip_edges());781err = cfg.save(project_godot);782if (err != OK) {783dialog_error->set_text(vformat(TTR("Couldn't save project at '%s' (error %d)."), project_godot, err));784dialog_error->popup_centered();785return;786}787}788789hide();790if (mode == MODE_NEW || mode == MODE_IMPORT || mode == MODE_INSTALL) {791#ifdef ANDROID_ENABLED792// Create a .nomedia file to hide assets from media apps on Android.793// Android 11 has some issues with nomedia files, so it's disabled there. See GH-106479, GH-105399 for details.794// NOTE: Nomedia file is also handled during the first filesystem scan. See editor_file_system.cpp -> EditorFileSystem::scan().795String sdk_version = OS::get_singleton()->get_version().get_slicec('.', 0);796if (sdk_version != "30") {797const String nomedia_file_path = path.path_join(".nomedia");798Ref<FileAccess> f2 = FileAccess::open(nomedia_file_path, FileAccess::WRITE);799if (f2.is_null()) {800// .nomedia isn't so critical.801ERR_PRINT("Couldn't create .nomedia in project path.");802} else {803f2->close();804}805}806#endif807emit_signal(SNAME("project_created"), path, edit_check_box->is_pressed());808} else if (mode == MODE_DUPLICATE) {809emit_signal(SNAME("project_duplicated"), original_project_path, path, edit_check_box->is_visible() && edit_check_box->is_pressed());810} else if (mode == MODE_RENAME) {811emit_signal(SNAME("projects_updated"));812}813}814815void ProjectDialog::set_zip_path(const String &p_path) {816zip_path = p_path;817}818819void ProjectDialog::set_zip_title(const String &p_title) {820zip_title = p_title;821}822823void ProjectDialog::set_original_project_path(const String &p_path) {824original_project_path = p_path;825}826827void ProjectDialog::set_duplicate_can_edit(bool p_duplicate_can_edit) {828duplicate_can_edit = p_duplicate_can_edit;829}830831void ProjectDialog::set_mode(Mode p_mode) {832mode = p_mode;833}834835void ProjectDialog::set_project_name(const String &p_name) {836project_name->set_text(p_name);837}838839void ProjectDialog::set_project_path(const String &p_path) {840project_path->set_text(p_path);841}842843void ProjectDialog::ask_for_path_and_show() {844_reset_name();845_browse_project_path();846}847848void ProjectDialog::show_dialog(bool p_reset_name, bool p_is_confirmed) {849_update_ok_button();850851if (mode == MODE_IMPORT && !p_is_confirmed) {852return;853}854if (mode == MODE_RENAME) {855// Name and path are set in `ProjectManager::_rename_project`.856project_path->set_editable(false);857858set_title(TTRC("Rename Project"));859set_ok_button_text(TTRC("Rename"));860861create_dir->hide();862project_status_rect->hide();863project_browse->hide();864edit_check_box->hide();865866name_container->show();867install_path_container->hide();868renderer_container->hide();869default_files_container->hide();870871callable_mp((Control *)project_name, &Control::grab_focus).call_deferred(false);872callable_mp(project_name, &LineEdit::select_all).call_deferred();873} else {874if (p_reset_name) {875_reset_name();876}877project_path->set_editable(true);878879if (mode == MODE_DUPLICATE) {880String original_dir = original_project_path.get_base_dir();881project_path->set_text(original_dir);882install_path->set_text(original_dir);883fdialog_project->set_current_dir(original_dir);884} else {885String fav_dir = EDITOR_GET("filesystem/directories/default_project_path");886fav_dir = fav_dir.simplify_path();887if (!fav_dir.is_empty()) {888project_path->set_text(fav_dir);889install_path->set_text(fav_dir);890fdialog_project->set_current_dir(fav_dir);891} else {892Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);893project_path->set_text(d->get_current_dir());894install_path->set_text(d->get_current_dir());895fdialog_project->set_current_dir(d->get_current_dir());896}897}898899create_dir->show();900project_status_rect->show();901project_browse->show();902edit_check_box->show();903904if (mode == MODE_IMPORT) {905set_title(TTRC("Import Existing Project"));906set_ok_button_text(TTRC("Import"));907908name_container->hide();909install_path_container->hide();910renderer_container->hide();911default_files_container->hide();912913// Project path dialog is also opened; no need to change focus.914} else if (mode == MODE_NEW) {915set_title(TTRC("Create New Project"));916set_ok_button_text(TTRC("Create"));917918if (!rendering_device_checked) {919rendering_device_supported = DisplayServer::is_rendering_device_supported();920921if (!rendering_device_supported) {922List<BaseButton *> buttons;923renderer_button_group->get_buttons(&buttons);924for (BaseButton *button : buttons) {925if (button->get_meta(SNAME("rendering_method")) == "gl_compatibility") {926button->set_pressed(true);927break;928}929}930}931_renderer_selected();932rendering_device_checked = true;933}934935name_container->show();936install_path_container->hide();937renderer_container->show();938default_files_container->show();939940callable_mp((Control *)project_name, &Control::grab_focus).call_deferred(false);941callable_mp(project_name, &LineEdit::select_all).call_deferred();942} else if (mode == MODE_INSTALL) {943set_title(TTR("Install Project:") + " " + zip_title);944set_ok_button_text(TTRC("Install"));945946project_name->set_text(zip_title);947948name_container->show();949install_path_container->hide();950renderer_container->hide();951default_files_container->hide();952953callable_mp((Control *)project_path, &Control::grab_focus).call_deferred(false);954} else if (mode == MODE_DUPLICATE) {955set_title(TTRC("Duplicate Project"));956set_ok_button_text(TTRC("Duplicate"));957958name_container->show();959install_path_container->hide();960renderer_container->hide();961default_files_container->hide();962if (!duplicate_can_edit) {963edit_check_box->hide();964}965966callable_mp((Control *)project_name, &Control::grab_focus).call_deferred(false);967callable_mp(project_name, &LineEdit::select_all).call_deferred();968}969970auto_dir = "";971last_custom_target_dir = "";972_update_target_auto_dir();973if (create_dir->is_pressed()) {974// Append `auto_dir` to target path.975_create_dir_toggled(true);976}977}978979_validate_path();980981popup_centered(Size2(500, 0) * EDSCALE);982}983984void ProjectDialog::_notification(int p_what) {985switch (p_what) {986case NOTIFICATION_TRANSLATION_CHANGED: {987if (rendering_device_checked) {988_renderer_selected();989}990} break;991992case NOTIFICATION_THEME_CHANGED: {993create_dir->set_button_icon(get_editor_theme_icon(SNAME("FolderCreate")));994project_browse->set_button_icon(get_editor_theme_icon(SNAME("FolderBrowse")));995install_browse->set_button_icon(get_editor_theme_icon(SNAME("FolderBrowse")));996} break;997case NOTIFICATION_READY: {998fdialog_project = memnew(EditorFileDialog);999fdialog_project->set_access(EditorFileDialog::ACCESS_FILESYSTEM);1000fdialog_project->connect("dir_selected", callable_mp(this, &ProjectDialog::_project_path_selected));1001fdialog_project->connect("file_selected", callable_mp(this, &ProjectDialog::_project_path_selected));1002fdialog_project->connect("canceled", callable_mp(this, &ProjectDialog::show_dialog).bind(false, false), CONNECT_DEFERRED);1003callable_mp((Node *)this, &Node::add_sibling).call_deferred(fdialog_project, false);1004} break;1005}1006}10071008void ProjectDialog::_bind_methods() {1009ADD_SIGNAL(MethodInfo("project_created"));1010ADD_SIGNAL(MethodInfo("project_duplicated"));1011ADD_SIGNAL(MethodInfo("projects_updated"));1012}10131014ProjectDialog::ProjectDialog() {1015VBoxContainer *vb = memnew(VBoxContainer);1016add_child(vb);10171018name_container = memnew(VBoxContainer);1019vb->add_child(name_container);10201021Label *l = memnew(Label);1022l->set_text(TTRC("Project Name:"));1023name_container->add_child(l);10241025project_name = memnew(LineEdit);1026project_name->set_virtual_keyboard_show_on_focus(false);1027project_name->set_h_size_flags(Control::SIZE_EXPAND_FILL);1028project_name->set_accessibility_name(TTRC("Project Name:"));1029name_container->add_child(project_name);10301031project_path_container = memnew(VBoxContainer);1032vb->add_child(project_path_container);10331034HBoxContainer *pphb_label = memnew(HBoxContainer);1035project_path_container->add_child(pphb_label);10361037l = memnew(Label);1038l->set_text(TTRC("Project Path:"));1039l->set_h_size_flags(Control::SIZE_EXPAND_FILL);1040pphb_label->add_child(l);10411042create_dir = memnew(CheckButton);1043create_dir->set_text(TTRC("Create Folder"));1044create_dir->set_pressed(true);1045pphb_label->add_child(create_dir);1046create_dir->connect(SceneStringName(toggled), callable_mp(this, &ProjectDialog::_create_dir_toggled));10471048HBoxContainer *pphb = memnew(HBoxContainer);1049project_path_container->add_child(pphb);10501051project_path = memnew(LineEdit);1052project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);1053project_path->set_accessibility_name(TTRC("Project Path:"));1054project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);1055pphb->add_child(project_path);10561057install_path_container = memnew(VBoxContainer);1058vb->add_child(install_path_container);10591060l = memnew(Label);1061l->set_text(TTRC("Project Installation Path:"));1062install_path_container->add_child(l);10631064HBoxContainer *iphb = memnew(HBoxContainer);1065install_path_container->add_child(iphb);10661067install_path = memnew(LineEdit);1068install_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);1069install_path->set_accessibility_name(TTRC("Project Installation Path:"));1070install_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);1071iphb->add_child(install_path);10721073// status icon1074project_status_rect = memnew(TextureRect);1075project_status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);1076pphb->add_child(project_status_rect);10771078project_browse = memnew(Button);1079project_browse->set_text(TTRC("Browse"));1080project_browse->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_browse_project_path));1081pphb->add_child(project_browse);10821083// install status icon1084install_status_rect = memnew(TextureRect);1085install_status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);1086iphb->add_child(install_status_rect);10871088install_browse = memnew(Button);1089install_browse->set_text(TTRC("Browse"));1090install_browse->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_browse_install_path));1091iphb->add_child(install_browse);10921093msg = memnew(Label);1094msg->set_focus_mode(Control::FOCUS_ACCESSIBILITY);1095msg->set_accessibility_live(DisplayServer::LIVE_POLITE);1096msg->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);1097msg->set_custom_minimum_size(Size2(200, 0) * EDSCALE);1098msg->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);1099vb->add_child(msg);11001101// Renderer selection.1102renderer_container = memnew(VBoxContainer);1103vb->add_child(renderer_container);1104l = memnew(Label);1105l->set_text(TTRC("Renderer:"));1106renderer_container->add_child(l);1107HBoxContainer *rshc = memnew(HBoxContainer);1108renderer_container->add_child(rshc);1109renderer_button_group.instantiate();11101111// Left hand side, used for checkboxes to select renderer.1112Container *rvb = memnew(VBoxContainer);1113rshc->add_child(rvb);11141115String default_renderer_type = "forward_plus";1116if (EditorSettings::get_singleton()->has_setting("project_manager/default_renderer")) {1117default_renderer_type = EditorSettings::get_singleton()->get_setting("project_manager/default_renderer");1118}11191120Button *rs_button = memnew(CheckBox);1121rs_button->set_button_group(renderer_button_group);1122rs_button->set_text(TTRC("Forward+"));1123rs_button->set_accessibility_name(TTRC("Renderer:"));1124#ifndef RD_ENABLED1125rs_button->set_disabled(true);1126#endif1127rs_button->set_meta(SNAME("rendering_method"), "forward_plus");1128rs_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_renderer_selected));1129rvb->add_child(rs_button);1130if (default_renderer_type == "forward_plus") {1131rs_button->set_pressed(true);1132}1133rs_button = memnew(CheckBox);1134rs_button->set_button_group(renderer_button_group);1135rs_button->set_text(TTRC("Mobile"));1136rs_button->set_accessibility_name(TTRC("Renderer:"));1137#ifndef RD_ENABLED1138rs_button->set_disabled(true);1139#endif1140rs_button->set_meta(SNAME("rendering_method"), "mobile");1141rs_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_renderer_selected));1142rvb->add_child(rs_button);1143if (default_renderer_type == "mobile") {1144rs_button->set_pressed(true);1145}1146rs_button = memnew(CheckBox);1147rs_button->set_button_group(renderer_button_group);1148rs_button->set_text(TTRC("Compatibility"));1149rs_button->set_accessibility_name(TTRC("Renderer:"));1150#if !defined(GLES3_ENABLED)1151rs_button->set_disabled(true);1152#endif1153rs_button->set_meta(SNAME("rendering_method"), "gl_compatibility");1154rs_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_renderer_selected));1155rvb->add_child(rs_button);1156LinkButton *ri_link = memnew(LinkButton);1157ri_link->set_text(TTRC("More information"));1158ri_link->set_uri(GODOT_VERSION_DOCS_URL "/tutorials/rendering/renderers.html");1159ri_link->set_h_size_flags(Control::SIZE_SHRINK_CENTER);1160rvb->add_child(ri_link);1161#if defined(GLES3_ENABLED)1162if (default_renderer_type == "gl_compatibility") {1163rs_button->set_pressed(true);1164}1165#endif1166rshc->add_child(memnew(VSeparator));11671168// Right hand side, used for text explaining each choice.1169rvb = memnew(VBoxContainer);1170rvb->set_h_size_flags(Control::SIZE_EXPAND_FILL);1171rshc->add_child(rvb);1172renderer_info = memnew(Label);1173renderer_info->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);1174renderer_info->set_focus_mode(Control::FOCUS_ACCESSIBILITY);1175renderer_info->set_modulate(Color(1, 1, 1, 0.7));1176rvb->add_child(renderer_info);11771178rd_not_supported = memnew(Label);1179rd_not_supported->set_focus_mode(Control::FOCUS_ACCESSIBILITY);1180rd_not_supported->set_text(vformat(TTR("RenderingDevice-based methods not available on this GPU:\n%s\nPlease use the Compatibility renderer."), RenderingServer::get_singleton()->get_video_adapter_name()));1181rd_not_supported->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);1182rd_not_supported->set_custom_minimum_size(Size2(200, 0) * EDSCALE);1183rd_not_supported->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);1184rd_not_supported->set_visible(false);1185renderer_container->add_child(rd_not_supported);11861187l = memnew(Label);1188l->set_focus_mode(Control::FOCUS_ACCESSIBILITY);1189l->set_text(TTRC("The renderer can be changed later, but scenes may need to be adjusted."));1190// Add some extra spacing to separate it from the list above and the buttons below.1191l->set_custom_minimum_size(Size2(0, 40) * EDSCALE);1192l->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);1193l->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);1194l->set_modulate(Color(1, 1, 1, 0.7));1195renderer_container->add_child(l);11961197default_files_container = memnew(HBoxContainer);1198vb->add_child(default_files_container);1199l = memnew(Label);1200l->set_text(TTRC("Version Control Metadata:"));1201default_files_container->add_child(l);1202vcs_metadata_selection = memnew(OptionButton);1203vcs_metadata_selection->set_custom_minimum_size(Size2(100, 20));1204vcs_metadata_selection->add_item(TTRC("None"), (int)EditorVCSInterface::VCSMetadata::NONE);1205vcs_metadata_selection->add_item(TTRC("Git"), (int)EditorVCSInterface::VCSMetadata::GIT);1206vcs_metadata_selection->select((int)EditorVCSInterface::VCSMetadata::GIT);1207vcs_metadata_selection->set_accessibility_name(TTRC("Version Control Metadata:"));1208default_files_container->add_child(vcs_metadata_selection);1209Control *spacer = memnew(Control);1210spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);1211default_files_container->add_child(spacer);1212fdialog_install = memnew(EditorFileDialog);1213fdialog_install->set_access(EditorFileDialog::ACCESS_FILESYSTEM);1214add_child(fdialog_install);12151216Control *spacer2 = memnew(Control);1217spacer2->set_v_size_flags(Control::SIZE_EXPAND_FILL);1218vb->add_child(spacer2);12191220edit_check_box = memnew(CheckBox);1221edit_check_box->set_text(TTRC("Edit Now"));1222edit_check_box->set_h_size_flags(Control::SIZE_SHRINK_CENTER);1223edit_check_box->set_pressed(true);1224vb->add_child(edit_check_box);12251226project_name->connect(SceneStringName(text_changed), callable_mp(this, &ProjectDialog::_project_name_changed).unbind(1));1227project_name->connect(SceneStringName(text_submitted), callable_mp(this, &ProjectDialog::ok_pressed).unbind(1));12281229project_path->connect(SceneStringName(text_changed), callable_mp(this, &ProjectDialog::_project_path_changed).unbind(1));1230project_path->connect(SceneStringName(text_submitted), callable_mp(this, &ProjectDialog::ok_pressed).unbind(1));12311232install_path->connect(SceneStringName(text_changed), callable_mp(this, &ProjectDialog::_install_path_changed).unbind(1));1233install_path->connect(SceneStringName(text_submitted), callable_mp(this, &ProjectDialog::ok_pressed).unbind(1));12341235fdialog_install->connect("dir_selected", callable_mp(this, &ProjectDialog::_install_path_selected));1236fdialog_install->connect("file_selected", callable_mp(this, &ProjectDialog::_install_path_selected));12371238set_hide_on_ok(false);12391240dialog_error = memnew(AcceptDialog);1241add_child(dialog_error);1242}124312441245