virtualx-engine/editor/project_manager/project_dialog.cpp
Grublady 5583ac5218 Ignore trailing slashes in new project’s path when disabling Create Folder
In the “Create New Project” dialog, disabling the “Create Folder” slider with a trailing slash in the project path would cause it to incorrectly identify the last path component as the substring following the slash, in other words, an empty string.

By first discarding any trailing slashes when disabling the slider, the project dialog will now identify and pop the last path component as usual even when a trailing slash is present.
2024-07-08 17:10:12 -04:00

992 lines
35 KiB
C++

/**************************************************************************/
/* project_dialog.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "project_dialog.h"
#include "core/config/project_settings.h"
#include "core/io/dir_access.h"
#include "core/io/zip_io.h"
#include "core/version.h"
#include "editor/editor_settings.h"
#include "editor/editor_string_names.h"
#include "editor/editor_vcs_interface.h"
#include "editor/gui/editor_file_dialog.h"
#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 p_input_type) {
msg->set_text(p_msg);
get_ok_button()->set_disabled(p_type == MESSAGE_ERROR);
Ref<Texture2D> new_icon;
switch (p_type) {
case MESSAGE_ERROR: {
msg->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
new_icon = get_editor_theme_icon(SNAME("StatusError"));
} break;
case MESSAGE_WARNING: {
msg->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
new_icon = get_editor_theme_icon(SNAME("StatusWarning"));
} break;
case MESSAGE_SUCCESS: {
msg->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("success_color"), EditorStringName(Editor)));
new_icon = get_editor_theme_icon(SNAME("StatusSuccess"));
} break;
}
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<DirAccess> p_d, const String &p_path) {
return p_path.get_extension() == "zip" && p_d->file_exists(p_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<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
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 (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 (!zip_path.is_empty()) {
target_path = install_path->get_text().simplify_path();
target_path_input_type = INSTALL_PATH;
create_dir->show();
install_path_container->show();
Ref<FileAccess> io_fa;
zlib_filefunc_def io = zipio_create_io(&io_fa);
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;
}
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.");
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;
}
unzClose(pkg);
} else if (d->dir_exists(path) && d->file_exists(path.path_join("project.godot"))) {
zip_path = "";
create_dir->hide();
install_path_container->hide();
_set_message(TTR("Valid project found at path."), MESSAGE_SUCCESS);
} else {
create_dir->hide();
install_path_container->hide();
_set_message(TTR("Please choose a \"project.godot\", a directory with one, or a \".zip\" file."), MESSAGE_ERROR);
return;
}
}
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 (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;
}
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;
}
// 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;
}
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 (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;
}
// 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();
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);
}
}
}
}
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::_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 {
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);
}
auto_dir = new_auto_dir;
}
void ProjectDialog::_create_dir_toggled(bool p_pressed) {
String target_path = _get_target_path();
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 {
target_path = target_path.path_join(last_custom_target_dir);
}
} else {
// Strip any trailing slash.
target_path = target_path.rstrip("/\\");
// 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();
}
_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::_project_path_changed() {
if (mode == MODE_IMPORT) {
_update_target_auto_dir();
}
_validate_path();
}
void ProjectDialog::_install_path_changed() {
_validate_path();
}
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_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_project->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
}
fdialog_project->popup_file_dialog();
}
void ProjectDialog::_browse_install_path() {
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::_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);
}
_project_path_changed();
if (install_path->is_visible_in_tree()) {
// ZIP is selected; focus install path.
install_path->grab_focus();
} else {
get_ok_button()->grab_focus();
}
}
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 (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);
}
_install_path_changed();
get_ok_button()->grab_focus();
}
void ProjectDialog::_renderer_selected() {
ERR_FAIL_NULL(renderer_button_group->get_pressed_button());
String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));
if (renderer_type == "forward_plus") {
renderer_info->set_text(
String::utf8("") + TTR("Supports desktop platforms only.") +
String::utf8("\n") + TTR("Advanced 3D graphics available.") +
String::utf8("\n") + TTR("Can scale to large complex scenes.") +
String::utf8("\n") + TTR("Uses RenderingDevice backend.") +
String::utf8("\n") + TTR("Slower rendering of simple scenes."));
} else if (renderer_type == "mobile") {
renderer_info->set_text(
String::utf8("") + TTR("Supports desktop + mobile platforms.") +
String::utf8("\n") + TTR("Less advanced 3D graphics.") +
String::utf8("\n") + TTR("Less scalable for complex scenes.") +
String::utf8("\n") + TTR("Uses RenderingDevice backend.") +
String::utf8("\n") + TTR("Fast rendering of simple scenes."));
} else if (renderer_type == "gl_compatibility") {
renderer_info->set_text(
String::utf8("") + TTR("Supports desktop, mobile + web platforms.") +
String::utf8("\n") + TTR("Least advanced 3D graphics (currently work-in-progress).") +
String::utf8("\n") + TTR("Intended for low-end/older devices.") +
String::utf8("\n") + TTR("Uses OpenGL 3 backend (OpenGL 3.3/ES 3.0/WebGL2).") +
String::utf8("\n") + TTR("Fastest rendering of simple scenes."));
} else {
WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
}
}
void ProjectDialog::_nonempty_confirmation_ok_pressed() {
is_folder_empty = true;
ok_pressed();
}
void ProjectDialog::ok_pressed() {
// 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(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_nonempty_confirmation_ok_pressed));
get_parent()->add_child(cd);
cd->popup_centered();
return;
}
String path = project_path->get_text();
if (mode == MODE_NEW) {
if (create_dir->is_pressed()) {
Ref<DirAccess> 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<String>(), false);
if (err != OK) {
_set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR);
return;
}
// Store default project icon in SVG format.
Ref<FileAccess> fa_icon = FileAccess::open(path.path_join("icon.svg"), FileAccess::WRITE, &err);
if (err != OK) {
_set_message(TTR("Couldn't create icon.svg in project path."), MESSAGE_ERROR);
return;
}
fa_icon->store_string(get_default_project_icon());
EditorVCSInterface::create_vcs_metadata_files(EditorVCSInterface::VCSMetadata(vcs_metadata_selection->get_selected()), path);
}
// 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<FileAccess> 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;
}
// 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;
}
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;
}
if (create_dir->is_pressed()) {
Ref<DirAccess> 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;
}
}
ret = unzGoToFirstFile(pkg);
Vector<String> 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<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
da->make_dir(path.path_join(rel_path));
} else { // File.
Vector<uint8_t> 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<FileAccess> 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);
}
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();
return;
}
} break;
default: {
} break;
}
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;
}
}
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"));
}
}
void ProjectDialog::set_zip_path(const String &p_path) {
zip_path = p_path;
}
void ProjectDialog::set_zip_title(const String &p_title) {
zip_title = p_title;
}
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);
}
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();
_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);
set_title(TTR("Rename Project"));
set_ok_button_text(TTR("Rename"));
create_dir->hide();
project_status_rect->hide();
project_browse->hide();
name_container->show();
install_path_container->hide();
renderer_container->hide();
default_files_container->hide();
callable_mp((Control *)project_name, &Control::grab_focus).call_deferred();
callable_mp(project_name, &LineEdit::select_all).call_deferred();
} else {
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);
install_path->set_text(fav_dir);
fdialog_project->set_current_dir(fav_dir);
} else {
Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
project_path->set_text(d->get_current_dir());
install_path->set_text(d->get_current_dir());
fdialog_project->set_current_dir(d->get_current_dir());
}
create_dir->show();
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 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();
callable_mp((Control *)project_path, &Control::grab_focus).call_deferred();
}
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);
}
}
_validate_path();
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")));
project_browse->set_icon(get_editor_theme_icon(SNAME("FolderBrowse")));
install_browse->set_icon(get_editor_theme_icon(SNAME("FolderBrowse")));
} break;
}
}
void ProjectDialog::_bind_methods() {
ADD_SIGNAL(MethodInfo("project_created"));
ADD_SIGNAL(MethodInfo("projects_updated"));
}
ProjectDialog::ProjectDialog() {
VBoxContainer *vb = memnew(VBoxContainer);
add_child(vb);
name_container = memnew(VBoxContainer);
vb->add_child(name_container);
Label *l = memnew(Label);
l->set_text(TTR("Project Name:"));
name_container->add_child(l);
project_name = memnew(LineEdit);
project_name->set_h_size_flags(Control::SIZE_EXPAND_FILL);
name_container->add_child(project_name);
project_path_container = memnew(VBoxContainer);
vb->add_child(project_path_container);
HBoxContainer *pphb_label = memnew(HBoxContainer);
project_path_container->add_child(pphb_label);
l = memnew(Label);
l->set_text(TTR("Project Path:"));
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);
project_path_container->add_child(pphb);
project_path = memnew(LineEdit);
project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
pphb->add_child(project_path);
install_path_container = memnew(VBoxContainer);
vb->add_child(install_path_container);
l = memnew(Label);
l->set_text(TTR("Project Installation Path:"));
install_path_container->add_child(l);
HBoxContainer *iphb = memnew(HBoxContainer);
install_path_container->add_child(iphb);
install_path = memnew(LineEdit);
install_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
install_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
iphb->add_child(install_path);
// status icon
project_status_rect = memnew(TextureRect);
project_status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
pphb->add_child(project_status_rect);
project_browse = memnew(Button);
project_browse->set_text(TTR("Browse"));
project_browse->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_browse_project_path));
pphb->add_child(project_browse);
// install status icon
install_status_rect = memnew(TextureRect);
install_status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
iphb->add_child(install_status_rect);
install_browse = memnew(Button);
install_browse->set_text(TTR("Browse"));
install_browse->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_browse_install_path));
iphb->add_child(install_browse);
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.
renderer_container = memnew(VBoxContainer);
vb->add_child(renderer_container);
l = memnew(Label);
l->set_text(TTR("Renderer:"));
renderer_container->add_child(l);
HBoxContainer *rshc = memnew(HBoxContainer);
renderer_container->add_child(rshc);
renderer_button_group.instantiate();
// Left hand side, used for checkboxes to select renderer.
Container *rvb = memnew(VBoxContainer);
rshc->add_child(rvb);
String default_renderer_type = "forward_plus";
if (EditorSettings::get_singleton()->has_setting("project_manager/default_renderer")) {
default_renderer_type = EditorSettings::get_singleton()->get_setting("project_manager/default_renderer");
}
Button *rs_button = memnew(CheckBox);
rs_button->set_button_group(renderer_button_group);
rs_button->set_text(TTR("Forward+"));
#if defined(WEB_ENABLED)
rs_button->set_disabled(true);
#endif
rs_button->set_meta(SNAME("rendering_method"), "forward_plus");
rs_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_renderer_selected));
rvb->add_child(rs_button);
if (default_renderer_type == "forward_plus") {
rs_button->set_pressed(true);
}
rs_button = memnew(CheckBox);
rs_button->set_button_group(renderer_button_group);
rs_button->set_text(TTR("Mobile"));
#if defined(WEB_ENABLED)
rs_button->set_disabled(true);
#endif
rs_button->set_meta(SNAME("rendering_method"), "mobile");
rs_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_renderer_selected));
rvb->add_child(rs_button);
if (default_renderer_type == "mobile") {
rs_button->set_pressed(true);
}
rs_button = memnew(CheckBox);
rs_button->set_button_group(renderer_button_group);
rs_button->set_text(TTR("Compatibility"));
#if !defined(GLES3_ENABLED)
rs_button->set_disabled(true);
#endif
rs_button->set_meta(SNAME("rendering_method"), "gl_compatibility");
rs_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectDialog::_renderer_selected));
rvb->add_child(rs_button);
#if defined(GLES3_ENABLED)
if (default_renderer_type == "gl_compatibility") {
rs_button->set_pressed(true);
}
#endif
rshc->add_child(memnew(VSeparator));
// Right hand side, used for text explaining each choice.
rvb = memnew(VBoxContainer);
rvb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
rshc->add_child(rvb);
renderer_info = memnew(Label);
renderer_info->set_modulate(Color(1, 1, 1, 0.7));
rvb->add_child(renderer_info);
_renderer_selected();
l = memnew(Label);
l->set_text(TTR("The renderer can be changed later, but scenes may need to be adjusted."));
// Add some extra spacing to separate it from the list above and the buttons below.
l->set_custom_minimum_size(Size2(0, 40) * EDSCALE);
l->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
l->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
l->set_modulate(Color(1, 1, 1, 0.7));
renderer_container->add_child(l);
default_files_container = memnew(HBoxContainer);
vb->add_child(default_files_container);
l = memnew(Label);
l->set_text(TTR("Version Control Metadata:"));
default_files_container->add_child(l);
vcs_metadata_selection = memnew(OptionButton);
vcs_metadata_selection->set_custom_minimum_size(Size2(100, 20));
vcs_metadata_selection->add_item(TTR("None"), (int)EditorVCSInterface::VCSMetadata::NONE);
vcs_metadata_selection->add_item(TTR("Git"), (int)EditorVCSInterface::VCSMetadata::GIT);
vcs_metadata_selection->select((int)EditorVCSInterface::VCSMetadata::GIT);
default_files_container->add_child(vcs_metadata_selection);
Control *spacer = memnew(Control);
spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
default_files_container->add_child(spacer);
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_project);
add_child(fdialog_install);
project_name->connect(SceneStringName(text_changed), callable_mp(this, &ProjectDialog::_project_name_changed).unbind(1));
project_path->connect(SceneStringName(text_changed), callable_mp(this, &ProjectDialog::_project_path_changed).unbind(1));
install_path->connect(SceneStringName(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));
set_hide_on_ok(false);
dialog_error = memnew(AcceptDialog);
add_child(dialog_error);
}