diff options
Diffstat (limited to 'editor/project_manager.cpp')
-rw-r--r-- | editor/project_manager.cpp | 1042 |
1 files changed, 542 insertions, 500 deletions
diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index dc5ff6a5eb..50a763f05a 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -31,14 +31,14 @@ #include "project_manager.h" #include "core/io/config_file.h" +#include "core/io/dir_access.h" +#include "core/io/file_access.h" #include "core/io/resource_saver.h" #include "core/io/stream_peer_ssl.h" #include "core/io/zip_io.h" -#include "core/os/dir_access.h" -#include "core/os/file_access.h" #include "core/os/keyboard.h" #include "core/os/os.h" -#include "core/translation.h" +#include "core/string/translation.h" #include "core/version.h" #include "core/version_hash.gen.h" #include "editor_scale.h" @@ -50,7 +50,6 @@ #include "scene/gui/panel_container.h" #include "scene/gui/separator.h" #include "scene/gui/texture_rect.h" -#include "scene/gui/tool_button.h" #include "scene/main/window.h" #include "servers/display_server.h" @@ -62,6 +61,7 @@ class ProjectDialog : public ConfirmationDialog { GDCLASS(ProjectDialog, ConfirmationDialog); public: + bool is_folder_empty = true; enum Mode { MODE_NEW, MODE_IMPORT, @@ -160,7 +160,7 @@ private: if (valid_path == "") { set_message(TTR("The path specified doesn't exist."), MESSAGE_ERROR); memdelete(d); - get_ok()->set_disabled(true); + get_ok_button()->set_disabled(true); return ""; } @@ -174,7 +174,7 @@ private: if (valid_install_path == "") { set_message(TTR("The path specified doesn't exist."), MESSAGE_ERROR, INSTALL_PATH); memdelete(d); - get_ok()->set_disabled(true); + get_ok_button()->set_disabled(true); return ""; } } @@ -189,7 +189,7 @@ private: if (!pkg) { set_message(TTR("Error opening package file (it's not in ZIP format)."), MESSAGE_ERROR); memdelete(d); - get_ok()->set_disabled(true); + get_ok_button()->set_disabled(true); unzClose(pkg); return ""; } @@ -210,7 +210,7 @@ private: 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); memdelete(d); - get_ok()->set_disabled(true); + get_ok_button()->set_disabled(true); unzClose(pkg); return ""; } @@ -219,7 +219,7 @@ private: // check if the specified install folder is empty, even though this is not an error, it is good to check here d->list_dir_begin(); - bool is_empty = true; + is_folder_empty = true; String n = d->get_next(); while (n != String()) { if (!n.begins_with(".")) { @@ -227,17 +227,17 @@ private: // 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_empty = false; + is_folder_empty = false; break; } n = d->get_next(); } d->list_dir_end(); - if (!is_empty) { + if (!is_folder_empty) { set_message(TTR("Please choose an empty folder."), MESSAGE_WARNING, INSTALL_PATH); memdelete(d); - get_ok()->set_disabled(true); + get_ok_button()->set_disabled(true); return ""; } @@ -245,21 +245,21 @@ private: set_message(TTR("Please choose a \"project.godot\" or \".zip\" file."), MESSAGE_ERROR); memdelete(d); install_path_container->hide(); - get_ok()->set_disabled(true); + get_ok_button()->set_disabled(true); return ""; } } else if (valid_path.ends_with("zip")) { set_message(TTR("This directory already contains a Godot project."), MESSAGE_ERROR, INSTALL_PATH); memdelete(d); - get_ok()->set_disabled(true); + get_ok_button()->set_disabled(true); return ""; } } else { // check if the specified folder is empty, even though this is not an error, it is good to check here d->list_dir_begin(); - bool is_empty = true; + is_folder_empty = true; String n = d->get_next(); while (n != String()) { if (!n.begins_with(".")) { @@ -267,25 +267,25 @@ private: // 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_empty = false; + is_folder_empty = false; break; } n = d->get_next(); } d->list_dir_end(); - if (!is_empty) { - set_message(TTR("Please choose an empty folder."), MESSAGE_ERROR); + if (!is_folder_empty) { + set_message(TTR("The selected path is not empty. Choosing an empty folder is highly recommended."), MESSAGE_WARNING); memdelete(d); - get_ok()->set_disabled(true); - return ""; + get_ok_button()->set_disabled(false); + return valid_path; } } set_message(""); set_message("", MESSAGE_SUCCESS, INSTALL_PATH); memdelete(d); - get_ok()->set_disabled(false); + get_ok_button()->set_disabled(false); return valid_path; } @@ -293,9 +293,9 @@ private: String sp = _test_path(); if (sp != "") { // If the project name is empty or default, infer the project name from the selected folder name - if (project_name->get_text() == "" || project_name->get_text() == TTR("New Game Project")) { + if (project_name->get_text().strip_edges() == "" || project_name->get_text().strip_edges() == TTR("New Game Project")) { sp = sp.replace("\\", "/"); - int lidx = sp.find_last("/"); + int lidx = sp.rfind("/"); if (lidx != -1) { sp = sp.substr(lidx + 1, sp.length()).capitalize(); @@ -320,24 +320,25 @@ private: if (p.ends_with("project.godot")) { p = p.get_base_dir(); install_path_container->hide(); - get_ok()->set_disabled(false); + get_ok_button()->set_disabled(false); } else if (p.ends_with(".zip")) { install_path->set_text(p.get_base_dir()); install_path_container->show(); - get_ok()->set_disabled(false); + get_ok_button()->set_disabled(false); } else { set_message(TTR("Please choose a \"project.godot\" or \".zip\" file."), MESSAGE_ERROR); - get_ok()->set_disabled(true); + get_ok_button()->set_disabled(true); return; } } + String sp = p.simplify_path(); project_path->set_text(sp); _path_text_changed(sp); if (p.ends_with(".zip")) { install_path->call_deferred("grab_focus"); } else { - get_ok()->call_deferred("grab_focus"); + get_ok_button()->call_deferred("grab_focus"); } } @@ -345,14 +346,14 @@ private: String sp = p_path.simplify_path(); project_path->set_text(sp); _path_text_changed(sp); - get_ok()->call_deferred("grab_focus"); + get_ok_button()->call_deferred("grab_focus"); } void _install_path_selected(const String &p_path) { String sp = p_path.simplify_path(); install_path->set_text(sp); _path_text_changed(sp); - get_ok()->call_deferred("grab_focus"); + get_ok_button()->call_deferred("grab_focus"); } void _browse_path() { @@ -366,26 +367,27 @@ private: } else { fdialog->set_file_mode(FileDialog::FILE_MODE_OPEN_DIR); } - fdialog->popup_centered_ratio(); + fdialog->popup_file_dialog(); } void _browse_install_path() { fdialog_install->set_current_dir(install_path->get_text()); fdialog_install->set_file_mode(FileDialog::FILE_MODE_OPEN_DIR); - fdialog_install->popup_centered_ratio(); + fdialog_install->popup_file_dialog(); } void _create_folder() { - if (project_name->get_text() == "" || created_folder_path != "" || project_name->get_text().ends_with(".") || project_name->get_text().ends_with(" ")) { - set_message(TTR("Invalid Project Name."), MESSAGE_WARNING); + const String project_name_no_edges = project_name->get_text().strip_edges(); + if (project_name_no_edges == "" || created_folder_path != "" || project_name_no_edges.ends_with(".")) { + set_message(TTR("Invalid project name."), MESSAGE_WARNING); return; } DirAccess *d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); if (d->change_dir(project_path->get_text()) == OK) { - if (!d->dir_exists(project_name->get_text())) { - if (d->make_dir(project_name->get_text()) == OK) { - d->change_dir(project_name->get_text()); + if (!d->dir_exists(project_name_no_edges)) { + if (d->make_dir(project_name_no_edges) == OK) { + d->change_dir(project_name_no_edges); String dir_str = d->get_current_dir(); project_path->set_text(dir_str); _path_text_changed(dir_str); @@ -411,12 +413,17 @@ private: _test_path(); - if (p_text == "") { - set_message(TTR("It would be a good idea to name your project."), MESSAGE_WARNING); + if (p_text.strip_edges() == "") { + set_message(TTR("It would be a good idea to name your project."), MESSAGE_ERROR); } } - void ok_pressed() { + void _nonempty_confirmation_ok_pressed() { + is_folder_empty = true; + ok_pressed(); + } + + void ok_pressed() override { String dir = project_path->get_text(); if (mode == MODE_RENAME) { @@ -433,7 +440,7 @@ private: set_message(vformat(TTR("Couldn't load project.godot in project path (error %d). It may be missing or corrupted."), err), MESSAGE_ERROR); } else { ProjectSettings::CustomMap edited_settings; - edited_settings["application/config/name"] = project_name->get_text(); + edited_settings["application/config/name"] = project_name->get_text().strip_edges(); if (current->save_custom(dir2.plus_file("project.godot"), edited_settings, Vector<String>(), true) != OK) { set_message(TTR("Couldn't edit project.godot in project path."), MESSAGE_ERROR); @@ -454,32 +461,46 @@ private: } else { if (mode == MODE_NEW) { + // Before we create a project, check that the target folder is empty. + // If not, we need to ask the user if they're sure they want to do this. + if (!is_folder_empty) { + ConfirmationDialog *cd = memnew(ConfirmationDialog); + cd->set_title(TTR("Warning: This folder is not empty")); + cd->set_text(TTR("You are about to create a Godot project in a non-empty folder.\nThe entire contents of this folder will be imported as project resources!\n\nAre you sure you wish to continue?")); + cd->get_ok_button()->connect("pressed", callable_mp(this, &ProjectDialog::_nonempty_confirmation_ok_pressed)); + get_parent()->add_child(cd); + cd->popup_centered(); + cd->grab_focus(); + return; + } ProjectSettings::CustomMap initial_settings; if (rasterizer_button_group->get_pressed_button()->get_meta("driver_name") == "Vulkan") { - initial_settings["rendering/quality/driver/driver_name"] = "Vulkan"; + initial_settings["rendering/driver/driver_name"] = "Vulkan"; } else { - initial_settings["rendering/quality/driver/driver_name"] = "GLES2"; - initial_settings["rendering/vram_compression/import_etc2"] = false; - initial_settings["rendering/vram_compression/import_etc"] = true; + initial_settings["rendering/driver/driver_name"] = "GLES2"; + initial_settings["rendering/textures/vram_compression/import_etc2"] = false; + initial_settings["rendering/textures/vram_compression/import_etc"] = true; } - initial_settings["application/config/name"] = project_name->get_text(); + initial_settings["application/config/name"] = project_name->get_text().strip_edges(); initial_settings["application/config/icon"] = "res://icon.png"; - initial_settings["rendering/environment/default_environment"] = "res://default_env.tres"; + initial_settings["rendering/environment/defaults/default_environment"] = "res://default_env.tres"; if (ProjectSettings::get_singleton()->save_custom(dir.plus_file("project.godot"), initial_settings, Vector<String>(), false) != OK) { set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR); } else { - ResourceSaver::save(dir.plus_file("icon.png"), msg->get_theme_icon("DefaultProjectIcon", "EditorIcons")); + ResourceSaver::save(dir.plus_file("icon.png"), create_unscaled_default_project_icon()); FileAccess *f = FileAccess::open(dir.plus_file("default_env.tres"), FileAccess::WRITE); if (!f) { set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR); } else { f->store_line("[gd_resource type=\"Environment\" load_steps=2 format=2]"); + f->store_line(""); f->store_line("[sub_resource type=\"Sky\" id=1]"); + f->store_line(""); f->store_line("[resource]"); f->store_line("background_mode = 2"); - f->store_line("background_sky = SubResource( 1 )"); + f->store_line("sky = SubResource( 1 )"); memdelete(f); } } @@ -500,7 +521,24 @@ private: return; } + // Find the zip_root + String zip_root; int ret = unzGoToFirstFile(pkg); + while (ret == UNZ_OK) { + unz_file_info info; + char fname[16384]; + unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); + + String name = fname; + if (name.ends_with("project.godot")) { + zip_root = name.substr(0, name.rfind("project.godot")); + break; + } + + ret = unzGoToNextFile(pkg); + } + + ret = unzGoToFirstFile(pkg); Vector<String> failed_files; @@ -513,44 +551,34 @@ private: String path = fname; - int depth = 1; //stuff from github comes with tag - bool skip = false; - while (depth > 0) { - int pp = path.find("/"); - if (pp == -1) { - skip = true; - break; - } - path = path.substr(pp + 1, path.length()); - depth--; - } - - if (skip || path == String()) { + if (path == String() || path == zip_root || !zip_root.is_subsequence_of(path)) { // } else if (path.ends_with("/")) { // a dir path = path.substr(0, path.length() - 1); + String rel_path = path.substr(zip_root.length()); DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - da->make_dir(dir.plus_file(path)); + da->make_dir(dir.plus_file(rel_path)); memdelete(da); } else { Vector<uint8_t> data; data.resize(info.uncompressed_size); + String rel_path = path.substr(zip_root.length()); //read unzOpenCurrentFile(pkg); unzReadCurrentFile(pkg, data.ptrw(), data.size()); unzCloseCurrentFile(pkg); - FileAccess *f = FileAccess::open(dir.plus_file(path), FileAccess::WRITE); + FileAccess *f = FileAccess::open(dir.plus_file(rel_path), FileAccess::WRITE); if (f) { f->store_buffer(data.ptr(), data.size()); memdelete(f); } else { - failed_files.push_back(path); + failed_files.push_back(rel_path); } } @@ -604,7 +632,7 @@ private: } } - void cancel_pressed() { + void cancel_pressed() override { _remove_created_folder(); project_path->clear(); @@ -664,14 +692,14 @@ public: install_browse->hide(); set_title(TTR("Rename Project")); - get_ok()->set_text(TTR("Rename")); + get_ok_button()->set_text(TTR("Rename")); name_container->show(); status_rect->hide(); msg->hide(); install_path_container->hide(); install_status_rect->hide(); rasterizer_container->hide(); - get_ok()->set_disabled(false); + get_ok_button()->set_disabled(false); ProjectSettings *current = memnew(ProjectSettings); @@ -680,7 +708,7 @@ public: set_message(vformat(TTR("Couldn't load project.godot in project path (error %d). It may be missing or corrupted."), err), MESSAGE_ERROR); status_rect->show(); msg->show(); - get_ok()->set_disabled(true); + get_ok_button()->set_disabled(true); } else if (current->has_setting("application/config/name")) { String proj = current->get("application/config/name"); project_name->set_text(proj); @@ -718,7 +746,7 @@ public: if (mode == MODE_IMPORT) { set_title(TTR("Import Existing Project")); - get_ok()->set_text(TTR("Import & Edit")); + get_ok_button()->set_text(TTR("Import & Edit")); name_container->hide(); install_path_container->hide(); rasterizer_container->hide(); @@ -726,7 +754,7 @@ public: } else if (mode == MODE_NEW) { set_title(TTR("Create New Project")); - get_ok()->set_text(TTR("Create & Edit")); + get_ok_button()->set_text(TTR("Create & Edit")); name_container->show(); install_path_container->hide(); rasterizer_container->show(); @@ -735,7 +763,7 @@ public: } else if (mode == MODE_INSTALL) { set_title(TTR("Install Project:") + " " + zip_title); - get_ok()->set_text(TTR("Install & Edit")); + get_ok_button()->set_text(TTR("Install & Edit")); project_name->set_text(zip_title); name_container->show(); install_path_container->hide(); @@ -784,6 +812,7 @@ public: project_path = memnew(LineEdit); project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL); + project_path->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE); pphb->add_child(project_path); install_path_container = memnew(VBoxContainer); @@ -798,6 +827,7 @@ public: install_path = memnew(LineEdit); install_path->set_h_size_flags(Control::SIZE_EXPAND_FILL); + install_path->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE); iphb->add_child(install_path); // status icon @@ -832,7 +862,7 @@ public: rasterizer_container->add_child(l); Container *rshb = memnew(HBoxContainer); rasterizer_container->add_child(rshb); - rasterizer_button_group.instance(); + rasterizer_button_group.instantiate(); Container *rvb = memnew(VBoxContainer); rvb->set_h_size_flags(Control::SIZE_EXPAND_FILL); @@ -916,6 +946,8 @@ public: icon = nullptr; icon_needs_reload = true; hover = false; + + set_focus_mode(FocusMode::FOCUS_ALL); } void set_is_favorite(bool fav) { @@ -934,7 +966,7 @@ public: } break; case NOTIFICATION_DRAW: { if (hover) { - draw_style_box(get_theme_stylebox("hover", "Tree"), Rect2(Point2(), get_size() - Size2(10, 0) * EDSCALE)); + draw_style_box(get_theme_stylebox("hover", "Tree"), Rect2(Point2(), get_size())); } } break; } @@ -960,13 +992,13 @@ public: String path; String icon; String main_scene; - uint64_t last_edited; - bool favorite; - bool grayed; - bool missing; - int version; + uint64_t last_edited = 0; + bool favorite = false; + bool grayed = false; + bool missing = false; + int version = 0; - ProjectListItemControl *control; + ProjectListItemControl *control = nullptr; Item() {} @@ -1009,12 +1041,12 @@ public: void update_dock_menu(); void load_projects(); void set_search_term(String p_search_term); - void set_order_option(ProjectListFilter::FilterOption p_option); + void set_order_option(int p_option); void sort_projects(); int get_project_count() const; void select_project(int p_index); void select_first_visible_project(); - void erase_selected_projects(); + void erase_selected_projects(bool p_delete_project_contents); Vector<Item> get_selected_projects() const; const Set<String> &get_selected_project_keys() const; void ensure_project_visible(int p_index); @@ -1042,7 +1074,7 @@ private: static void load_project_data(const String &p_property_key, Item &p_item, bool p_favorite); String _search_term; - ProjectListFilter::FilterOption _order_option; + FilterOption _order_option; Set<String> _selected_project_keys; String _last_clicked; // Project key VBoxContainer *_scroll_children; @@ -1052,7 +1084,7 @@ private: }; struct ProjectListComparator { - ProjectListFilter::FilterOption order_option; + FilterOption order_option = FilterOption::EDIT_DATE; // operator< _FORCE_INLINE_ bool operator()(const ProjectList::Item &a, const ProjectList::Item &b) const { @@ -1063,9 +1095,9 @@ struct ProjectListComparator { return false; } switch (order_option) { - case ProjectListFilter::FILTER_PATH: + case PATH: return a.project_key < b.project_key; - case ProjectListFilter::FILTER_EDIT_DATE: + case EDIT_DATE: return a.last_edited > b.last_edited; default: return a.project_name < b.project_name; @@ -1074,8 +1106,7 @@ struct ProjectListComparator { }; ProjectList::ProjectList() { - _order_option = ProjectListFilter::FILTER_EDIT_DATE; - + _order_option = FilterOption::NAME; _scroll_children = memnew(VBoxContainer); _scroll_children->set_h_size_flags(Control::SIZE_EXPAND_FILL); add_child(_scroll_children); @@ -1114,7 +1145,7 @@ void ProjectList::load_project_icon(int p_index) { Ref<Texture2D> icon; if (item.icon != "") { Ref<Image> img; - img.instance(); + img.instantiate(); Error err = img->load(item.icon.replace_first("res://", item.path + "/")); if (err == OK) { img->resize(default_icon->get_width(), default_icon->get_height(), Image::INTERPOLATE_LANCZOS); @@ -1235,8 +1266,6 @@ void ProjectList::load_projects() { create_project_item_control(i); } - sort_projects(); - set_v_scroll(0); update_icons_async(); @@ -1276,9 +1305,7 @@ void ProjectList::_global_menu_new_window(const Variant &p_tag) { List<String> args; args.push_back("-p"); String exec = OS::get_singleton()->get_executable_path(); - - OS::ProcessID pid = 0; - OS::get_singleton()->execute(exec, args, false, &pid); + OS::get_singleton()->create_process(exec, args); } void ProjectList::_global_menu_open_project(const Variant &p_tag) { @@ -1289,9 +1316,7 @@ void ProjectList::_global_menu_open_project(const Variant &p_tag) { List<String> args; args.push_back(conf); String exec = OS::get_singleton()->get_executable_path(); - - OS::ProcessID pid = 0; - OS::get_singleton()->execute(exec, args, false, &pid); + OS::get_singleton()->create_process(exec, args); } } @@ -1348,6 +1373,7 @@ void ProjectList::create_project_item_control(int p_index) { vb->add_child(ec); Label *title = memnew(Label(!item.missing ? item.project_name : TTR("Missing Project"))); title->add_theme_font_override("font", get_theme_font("title", "EditorFonts")); + title->add_theme_font_size_override("font_size", get_theme_font_size("title_size", "EditorFonts")); title->add_theme_color_override("font_color", font_color); title->set_clip_text(true); vb->add_child(title); @@ -1357,11 +1383,10 @@ void ProjectList::create_project_item_control(int p_index) { vb->add_child(path_hb); Button *show = memnew(Button); - // Display a folder icon if the project directory can be opened, or a "broken file" icon if it can't + // Display a folder icon if the project directory can be opened, or a "broken file" icon if it can't. show->set_icon(get_theme_icon(!item.missing ? "Load" : "FileBroken", "EditorIcons")); - show->set_flat(true); if (!item.grayed) { - // Don't make the icon less prominent if the parent is already grayed out + // Don't make the icon less prominent if the parent is already grayed out. show->set_modulate(Color(1, 1, 1, 0.5)); } path_hb->add_child(show); @@ -1374,6 +1399,7 @@ void ProjectList::create_project_item_control(int p_index) { } Label *fpath = memnew(Label(item.path)); + fpath->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE); path_hb->add_child(fpath); fpath->set_h_size_flags(Control::SIZE_EXPAND_FILL); fpath->set_modulate(Color(1, 1, 1, 0.5)); @@ -1388,12 +1414,13 @@ void ProjectList::set_search_term(String p_search_term) { _search_term = p_search_term; } -void ProjectList::set_order_option(ProjectListFilter::FilterOption p_option) { - if (_order_option != p_option) { - _order_option = p_option; - EditorSettings::get_singleton()->set("project_manager/sorting_order", (int)_order_option); - EditorSettings::get_singleton()->save(); - } +void ProjectList::set_order_option(int p_option) { + FilterOption selected = (FilterOption)p_option; + EditorSettings::get_singleton()->set("project_manager/sorting_order", p_option); + EditorSettings::get_singleton()->save(); + _order_option = selected; + + sort_projects(); } void ProjectList::sort_projects() { @@ -1457,16 +1484,7 @@ Vector<ProjectList::Item> ProjectList::get_selected_projects() const { void ProjectList::ensure_project_visible(int p_index) { const Item &item = _projects[p_index]; - - int item_top = item.control->get_position().y; - int item_bottom = item.control->get_position().y + item.control->get_size().y; - - if (item_top < get_v_scroll()) { - set_v_scroll(item_top); - - } else if (item_bottom > get_v_scroll() + get_size().y) { - set_v_scroll(item_bottom - get_size().y); - } + ensure_control_visible(item.control); } int ProjectList::get_single_selected_index() const { @@ -1521,7 +1539,7 @@ bool ProjectList::is_any_project_missing() const { } void ProjectList::erase_missing_projects() { - if (_projects.empty()) { + if (_projects.is_empty()) { return; } @@ -1674,7 +1692,7 @@ void ProjectList::toggle_select(int p_index) { item.control->update(); } -void ProjectList::erase_selected_projects() { +void ProjectList::erase_selected_projects(bool p_delete_project_contents) { if (_selected_project_keys.size() == 0) { return; } @@ -1685,6 +1703,10 @@ void ProjectList::erase_selected_projects() { EditorSettings::get_singleton()->erase("projects/" + item.project_key); EditorSettings::get_singleton()->erase("favorite_projects/" + item.project_key); + if (p_delete_project_contents) { + OS::get_singleton()->move_to_trash(item.path); + } + memdelete(item.control); _projects.remove(i); --i; @@ -1703,12 +1725,16 @@ void ProjectList::erase_selected_projects() { void ProjectList::_panel_draw(Node *p_hb) { Control *hb = Object::cast_to<Control>(p_hb); - hb->draw_line(Point2(0, hb->get_size().y + 1), Point2(hb->get_size().x - 10, hb->get_size().y + 1), get_theme_color("guide_color", "Tree")); + if (is_layout_rtl() && get_v_scrollbar()->is_visible_in_tree()) { + hb->draw_line(Point2(get_v_scrollbar()->get_minimum_size().x, hb->get_size().y + 1), Point2(hb->get_size().x, hb->get_size().y + 1), get_theme_color("guide_color", "Tree")); + } else { + hb->draw_line(Point2(0, hb->get_size().y + 1), Point2(hb->get_size().x, hb->get_size().y + 1), get_theme_color("guide_color", "Tree")); + } String key = _projects[p_hb->get_index()].project_key; if (_selected_project_keys.has(key)) { - hb->draw_style_box(get_theme_stylebox("selected", "Tree"), Rect2(Point2(), hb->get_size() - Size2(10, 0) * EDSCALE)); + hb->draw_style_box(get_theme_stylebox("selected", "Tree"), Rect2(Point2(), hb->get_size())); } } @@ -1718,8 +1744,8 @@ void ProjectList::_panel_input(const Ref<InputEvent> &p_ev, Node *p_hb) { int clicked_index = p_hb->get_index(); const Item &clicked_project = _projects[clicked_index]; - if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { - if (mb->get_shift() && _selected_project_keys.size() > 0 && _last_clicked != "" && clicked_project.project_key != _last_clicked) { + if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { + if (mb->is_shift_pressed() && _selected_project_keys.size() > 0 && _last_clicked != "" && clicked_project.project_key != _last_clicked) { int anchor_index = -1; for (int i = 0; i < _projects.size(); ++i) { const Item &p = _projects[i]; @@ -1731,7 +1757,7 @@ void ProjectList::_panel_input(const Ref<InputEvent> &p_ev, Node *p_hb) { CRASH_COND(anchor_index == -1); select_range(anchor_index, clicked_index); - } else if (mb->get_control()) { + } else if (mb->is_ctrl_pressed()) { toggle_select(clicked_index); } else { @@ -1741,7 +1767,7 @@ void ProjectList::_panel_input(const Ref<InputEvent> &p_ev, Node *p_hb) { emit_signal(SIGNAL_SELECTION_CHANGED); - if (!mb->get_control() && mb->is_doubleclick()) { + if (!mb->is_ctrl_pressed() && mb->is_double_click()) { emit_signal(SIGNAL_PROJECT_ASK_OPEN); } } @@ -1794,7 +1820,15 @@ void ProjectList::_bind_methods() { void ProjectManager::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + settings_hb->set_anchors_and_offsets_preset(Control::PRESET_TOP_RIGHT); + update(); + } break; case NOTIFICATION_ENTER_TREE: { + search_box->set_right_icon(get_theme_icon("Search", "EditorIcons")); + search_box->set_clear_button_enabled(true); + Engine::get_singleton()->set_editor_hint(false); } break; case NOTIFICATION_RESIZED: { @@ -1803,6 +1837,10 @@ void ProjectManager::_notification(int p_what) { } } break; case NOTIFICATION_READY: { + int default_sorting = (int)EditorSettings::get_singleton()->get("project_manager/sorting_order"); + filter_option->select(default_sorting); + _project_list->set_order_option(default_sorting); + if (_project_list->get_project_count() == 0 && StreamPeerSSL::is_available()) { open_templates->popup_centered(); } @@ -1810,15 +1848,18 @@ void ProjectManager::_notification(int p_what) { if (_project_list->get_project_count() >= 1) { // Focus on the search box immediately to allow the user // to search without having to reach for their mouse - project_filter->search_box->grab_focus(); + search_box->grab_focus(); } } break; case NOTIFICATION_VISIBILITY_CHANGED: { - set_process_unhandled_input(is_visible_in_tree()); + set_process_unhandled_key_input(is_visible_in_tree()); } break; case NOTIFICATION_WM_CLOSE_REQUEST: { _dim_window(); } break; + case NOTIFICATION_WM_ABOUT: { + _show_about(); + } break; } } @@ -1830,12 +1871,12 @@ void ProjectManager::_dim_window() { // No transition is applied, as the effect needs to be visible immediately float c = 0.5f; Color dim_color = Color(c, c, c); - gui_base->set_modulate(dim_color); + set_modulate(dim_color); } void ProjectManager::_update_project_buttons() { Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects(); - bool empty_selection = selected_projects.empty(); + bool empty_selection = selected_projects.is_empty(); bool is_missing_project_selected = false; for (int i = 0; i < selected_projects.size(); ++i) { @@ -1850,10 +1891,12 @@ void ProjectManager::_update_project_buttons() { rename_btn->set_disabled(empty_selection || is_missing_project_selected); run_btn->set_disabled(empty_selection || is_missing_project_selected); - erase_missing_btn->set_visible(_project_list->is_any_project_missing()); + erase_missing_btn->set_disabled(!_project_list->is_any_project_missing()); } -void ProjectManager::_unhandled_input(const Ref<InputEvent> &p_ev) { +void ProjectManager::_unhandled_key_input(const Ref<InputEvent> &p_ev) { + ERR_FAIL_COND(p_ev.is_null()); + Ref<InputEventKey> k = p_ev; if (k.is_valid()) { @@ -1881,9 +1924,6 @@ void ProjectManager::_unhandled_input(const Ref<InputEvent> &p_ev) { case KEY_ENTER: { _open_selected_projects_ask(); } break; - case KEY_DELETE: { - _erase_project(); - } break; case KEY_HOME: { if (_project_list->get_project_count() > 0) { _project_list->select_project(0); @@ -1899,7 +1939,7 @@ void ProjectManager::_unhandled_input(const Ref<InputEvent> &p_ev) { } break; case KEY_UP: { - if (k->get_shift()) { + if (k->is_shift_pressed()) { break; } @@ -1913,7 +1953,7 @@ void ProjectManager::_unhandled_input(const Ref<InputEvent> &p_ev) { break; } case KEY_DOWN: { - if (k->get_shift()) { + if (k->is_shift_pressed()) { break; } @@ -1926,8 +1966,8 @@ void ProjectManager::_unhandled_input(const Ref<InputEvent> &p_ev) { } break; case KEY_F: { - if (k->get_command()) { - this->project_filter->search_box->grab_focus(); + if (k->is_command_pressed()) { + this->search_box->grab_focus(); } else { keycode_handled = false; } @@ -1944,8 +1984,7 @@ void ProjectManager::_unhandled_input(const Ref<InputEvent> &p_ev) { } void ProjectManager::_load_recent_projects() { - _project_list->set_order_option(project_order_filter->get_filter_option()); - _project_list->set_search_term(project_filter->get_search_term()); + _project_list->set_search_term(search_box->get_text().strip_edges()); _project_list->load_projects(); _update_project_buttons(); @@ -1967,7 +2006,7 @@ void ProjectManager::_on_projects_updated() { } void ProjectManager::_on_project_created(const String &dir) { - project_filter->clear(); + search_box->clear(); int i = _project_list->refresh_project(dir); _project_list->select_project(i); _project_list->ensure_project_visible(i); @@ -1981,6 +2020,10 @@ void ProjectManager::_confirm_update_settings() { } void ProjectManager::_open_selected_projects() { + // Show loading text to tell the user that the project manager is busy loading. + // This is especially important for the HTML5 project manager. + loading_label->set_modulate(Color(1, 1, 1)); + const Set<String> &selected_list = _project_list->get_selected_project_keys(); for (const Set<String>::Element *E = selected_list.front(); E; E = E->next()) { @@ -2003,14 +2046,20 @@ void ProjectManager::_open_selected_projects() { args.push_back("--editor"); + if (OS::get_singleton()->is_stdout_debug_enabled()) { + args.push_back("--debug"); + } + + if (OS::get_singleton()->is_stdout_verbose()) { + args.push_back("--verbose"); + } + if (OS::get_singleton()->is_disable_crash_handler()) { args.push_back("--disable-crash-handler"); } String exec = OS::get_singleton()->get_executable_path(); - - OS::ProcessID pid = 0; - Error err = OS::get_singleton()->execute(exec, args, false, &pid); + Error err = OS::get_singleton()->create_process(exec, args); ERR_FAIL_COND(err); } @@ -2071,16 +2120,17 @@ void ProjectManager::_run_project_confirm() { if (selected_main == "") { run_error_diag->set_text(TTR("Can't run project: no main scene defined.\nPlease edit the project and set the main scene in the Project Settings under the \"Application\" category.")); run_error_diag->popup_centered(); - return; + continue; } const String &selected = selected_list[i].project_key; String path = EditorSettings::get_singleton()->get("projects/" + selected); - if (!DirAccess::exists(path + "/.import")) { + // `.substr(6)` on `IMPORTED_FILES_PATH` strips away the leading "res://". + if (!DirAccess::exists(path.plus_file(ProjectSettings::IMPORTED_FILES_PATH.substr(6)))) { run_error_diag->set_text(TTR("Can't run project: Assets need to be imported.\nPlease edit the project to trigger the initial import.")); run_error_diag->popup_centered(); - return; + continue; } print_line("Running project: " + path + " (" + selected + ")"); @@ -2095,14 +2145,11 @@ void ProjectManager::_run_project_confirm() { } String exec = OS::get_singleton()->get_executable_path(); - - OS::ProcessID pid = 0; - Error err = OS::get_singleton()->execute(exec, args, false, &pid); + Error err = OS::get_singleton()->create_process(exec, args); ERR_FAIL_COND(err); } } -// When you press the "Run" button void ProjectManager::_run_project() { const Set<String> &selected_list = _project_list->get_selected_project_keys(); @@ -2119,8 +2166,9 @@ void ProjectManager::_run_project() { } void ProjectManager::_scan_dir(const String &path, List<String> *r_projects) { - DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - da->change_dir(path); + DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Error error = da->change_dir(path); + ERR_FAIL_COND_MSG(error != OK, "Could not scan directory at: " + path); da->list_dir_begin(); String n = da->get_next(); while (n != String()) { @@ -2132,7 +2180,6 @@ void ProjectManager::_scan_dir(const String &path, List<String> *r_projects) { n = da->get_next(); } da->list_dir_end(); - memdelete(da); } void ProjectManager::_scan_begin(const String &p_base) { @@ -2150,7 +2197,7 @@ void ProjectManager::_scan_begin(const String &p_base) { } void ProjectManager::_scan_projects() { - scan_dir->popup_centered_ratio(); + scan_dir->popup_file_dialog(); } void ProjectManager::_new_project() { @@ -2180,7 +2227,7 @@ void ProjectManager::_rename_project() { } void ProjectManager::_erase_project_confirm() { - _project_list->erase_selected_projects(); + _project_list->erase_selected_projects(delete_project_contents->is_pressed()); _update_project_buttons(); } @@ -2198,12 +2245,13 @@ void ProjectManager::_erase_project() { String confirm_message; if (selected_list.size() >= 2) { - confirm_message = vformat(TTR("Remove %d projects from the list?\nThe project folders' contents won't be modified."), selected_list.size()); + confirm_message = vformat(TTR("Remove %d projects from the list?"), selected_list.size()); } else { - confirm_message = TTR("Remove this project from the list?\nThe project folder's contents won't be modified."); + confirm_message = TTR("Remove this project from the list?"); } - erase_ask->set_text(confirm_message); + erase_ask_label->set_text(confirm_message); + delete_project_contents->set_pressed(false); erase_ask->popup_centered(); } @@ -2212,11 +2260,13 @@ void ProjectManager::_erase_missing_projects() { erase_missing_ask->popup_centered(); } +void ProjectManager::_show_about() { + about->popup_centered(Size2(780, 500) * EDSCALE); +} + void ProjectManager::_language_selected(int p_id) { String lang = language_btn->get_item_metadata(p_id); EditorSettings::get_singleton()->set("interface/editor/editor_language", lang); - language_btn->set_text(lang); - language_btn->set_icon(get_theme_icon("Environment", "EditorIcons")); language_restart_ask->set_text(TTR("Language changed.\nThe interface will update after restarting the editor or project manager.")); language_restart_ask->popup_centered(); @@ -2225,19 +2275,13 @@ void ProjectManager::_language_selected(int p_id) { void ProjectManager::_restart_confirm() { List<String> args = OS::get_singleton()->get_cmdline_args(); String exec = OS::get_singleton()->get_executable_path(); - OS::ProcessID pid = 0; - Error err = OS::get_singleton()->execute(exec, args, false, &pid); + Error err = OS::get_singleton()->create_process(exec, args); ERR_FAIL_COND(err); _dim_window(); get_tree()->quit(); } -void ProjectManager::_exit_dialog() { - _dim_window(); - get_tree()->quit(); -} - void ProjectManager::_install_project(const String &p_zip_path, const String &p_title) { npdialog->set_mode(ProjectDialog::MODE_INSTALL); npdialog->set_zip_path(p_zip_path); @@ -2246,6 +2290,11 @@ void ProjectManager::_install_project(const String &p_zip_path, const String &p_ } void ProjectManager::_files_dropped(PackedStringArray p_files, int p_screen) { + if (p_files.size() == 1 && p_files[0].ends_with(".zip")) { + const String file = p_files[0].get_file(); + _install_project(p_files[0], file.substr(0, file.length() - 4).capitalize()); + return; + } Set<String> folders_set; DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); for (int i = 0; i < p_files.size(); i++) { @@ -2276,8 +2325,8 @@ void ProjectManager::_files_dropped(PackedStringArray p_files, int p_screen) { memdelete(dir); } if (confirm) { - multi_scan_ask->get_ok()->disconnect("pressed", callable_mp(this, &ProjectManager::_scan_multiple_folders)); - multi_scan_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_scan_multiple_folders), varray(folders)); + multi_scan_ask->get_ok_button()->disconnect("pressed", callable_mp(this, &ProjectManager::_scan_multiple_folders)); + multi_scan_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_scan_multiple_folders), varray(folders)); multi_scan_ask->set_text( vformat(TTR("Are you sure to scan %s folders for existing Godot projects?\nThis could take a while."), folders.size())); multi_scan_ask->popup_centered(); @@ -2293,13 +2342,25 @@ void ProjectManager::_scan_multiple_folders(PackedStringArray p_files) { } } -void ProjectManager::_on_order_option_changed() { - _project_list->set_order_option(project_order_filter->get_filter_option()); - _project_list->sort_projects(); +void ProjectManager::_on_order_option_changed(int p_idx) { + if (is_inside_tree()) { + _project_list->set_order_option(p_idx); + } +} + +void ProjectManager::_on_tab_changed(int p_tab) { + if (p_tab == 0) { // Projects + // Automatically grab focus when the user moves from the Templates tab + // back to the Projects tab. + search_box->grab_focus(); + } + + // The Templates tab's search field is focused on display in the asset + // library editor plugin code. } -void ProjectManager::_on_filter_option_changed() { - _project_list->set_search_term(project_filter->get_search_term()); +void ProjectManager::_on_search_term_changed(const String &p_term) { + _project_list->set_search_term(p_term); _project_list->sort_projects(); // Select the first visible project in the list. @@ -2310,9 +2371,9 @@ void ProjectManager::_on_filter_option_changed() { } void ProjectManager::_bind_methods() { - ClassDB::bind_method("_exit_dialog", &ProjectManager::_exit_dialog); - ClassDB::bind_method("_unhandled_input", &ProjectManager::_unhandled_input); + ClassDB::bind_method("_unhandled_key_input", &ProjectManager::_unhandled_key_input); ClassDB::bind_method("_update_project_buttons", &ProjectManager::_update_project_buttons); + ClassDB::bind_method("_version_button_pressed", &ProjectManager::_version_button_pressed); } void ProjectManager::_open_asset_library() { @@ -2320,6 +2381,10 @@ void ProjectManager::_open_asset_library() { tabs->set_current_tab(1); } +void ProjectManager::_version_button_pressed() { + DisplayServer::get_singleton()->clipboard_set(version_btn->get_text()); +} + ProjectManager::ProjectManager() { // load settings if (!EditorSettings::get_singleton()) { @@ -2330,19 +2395,12 @@ ProjectManager::ProjectManager() { { int display_scale = EditorSettings::get_singleton()->get("interface/editor/display_scale"); - float custom_display_scale = EditorSettings::get_singleton()->get("interface/editor/custom_display_scale"); switch (display_scale) { - case 0: { - // Try applying a suitable display scale automatically - const int screen = DisplayServer::get_singleton()->window_get_current_screen(); -#ifdef OSX_ENABLED - editor_set_scale(DisplayServer::get_singleton()->screen_get_scale(screen)); -#else - editor_set_scale(DisplayServer::get_singleton()->screen_get_dpi(screen) >= 192 && DisplayServer::get_singleton()->screen_get_size(screen).x > 2000 ? 2.0 : 1.0); -#endif - } break; - + case 0: + // Try applying a suitable display scale automatically. + editor_set_scale(EditorSettings::get_singleton()->get_auto_display_scale()); + break; case 1: editor_set_scale(0.75); break; @@ -2361,43 +2419,38 @@ ProjectManager::ProjectManager() { case 6: editor_set_scale(2.0); break; - - default: { - editor_set_scale(custom_display_scale); - } break; + default: + editor_set_scale(EditorSettings::get_singleton()->get("interface/editor/custom_display_scale")); + break; } // Define a minimum window size to prevent UI elements from overlapping or being cut off DisplayServer::get_singleton()->window_set_min_size(Size2(750, 420) * EDSCALE); -#ifndef OSX_ENABLED - // The macOS platform implementation uses its own hiDPI window resizing code - // TODO: Resize windows on hiDPI displays on Windows and Linux and remove the line below - DisplayServer::get_singleton()->window_set_size(DisplayServer::get_singleton()->window_get_size() * MAX(1, EDSCALE)); -#endif + // TODO: Resize windows on hiDPI displays on Windows and Linux and remove the lines below + float scale_factor = MAX(1, EDSCALE); + Vector2i window_size = DisplayServer::get_singleton()->window_get_size(); + DisplayServer::get_singleton()->window_set_size(Vector2i(window_size.x * scale_factor, window_size.y * scale_factor)); } + // TRANSLATORS: This refers to the application where users manage their Godot projects. + DisplayServer::get_singleton()->window_set_title(VERSION_NAME + String(" - ") + TTR("Project Manager")); + FileDialog::set_default_show_hidden_files(EditorSettings::get_singleton()->get("filesystem/file_dialog/show_hidden_files")); - set_anchors_and_margins_preset(Control::PRESET_WIDE); + set_anchors_and_offsets_preset(Control::PRESET_WIDE); set_theme(create_custom_theme()); - gui_base = memnew(Control); - add_child(gui_base); - gui_base->set_anchors_and_margins_preset(Control::PRESET_WIDE); + set_anchors_and_offsets_preset(Control::PRESET_WIDE); Panel *panel = memnew(Panel); - gui_base->add_child(panel); - panel->set_anchors_and_margins_preset(Control::PRESET_WIDE); - panel->add_theme_style_override("panel", gui_base->get_theme_stylebox("Background", "EditorStyles")); + add_child(panel); + panel->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + panel->add_theme_style_override("panel", get_theme_stylebox("Background", "EditorStyles")); VBoxContainer *vb = memnew(VBoxContainer); panel->add_child(vb); - vb->set_anchors_and_margins_preset(Control::PRESET_WIDE, Control::PRESET_MODE_MINSIZE, 8 * EDSCALE); - - String cp; - cp += 0xA9; - DisplayServer::get_singleton()->window_set_title(VERSION_NAME + String(" - ") + TTR("Project Manager") + " - " + cp + " 2007-2020 Juan Linietsky, Ariel Manzur & Godot Contributors"); + vb->set_anchors_and_offsets_preset(Control::PRESET_WIDE, Control::PRESET_MODE_MINSIZE, 8 * EDSCALE); Control *center_box = memnew(Control); center_box->set_v_size_flags(Control::SIZE_EXPAND_FILL); @@ -2405,325 +2458,314 @@ ProjectManager::ProjectManager() { tabs = memnew(TabContainer); center_box->add_child(tabs); - tabs->set_anchors_and_margins_preset(Control::PRESET_WIDE); + tabs->set_anchors_and_offsets_preset(Control::PRESET_WIDE); tabs->set_tab_align(TabContainer::ALIGN_LEFT); + tabs->connect("tab_changed", callable_mp(this, &ProjectManager::_on_tab_changed)); - HBoxContainer *tree_hb = memnew(HBoxContainer); - projects_hb = tree_hb; - - projects_hb->set_name(TTR("Projects")); - - tabs->add_child(tree_hb); - - VBoxContainer *search_tree_vb = memnew(VBoxContainer); - tree_hb->add_child(search_tree_vb); - search_tree_vb->set_h_size_flags(Control::SIZE_EXPAND_FILL); - - HBoxContainer *sort_filters = memnew(HBoxContainer); - Label *sort_label = memnew(Label); - sort_label->set_text(TTR("Sort:")); - sort_filters->add_child(sort_label); - Vector<String> sort_filter_titles; - sort_filter_titles.push_back(TTR("Name")); - sort_filter_titles.push_back(TTR("Path")); - sort_filter_titles.push_back(TTR("Last Edited")); - project_order_filter = memnew(ProjectListFilter); - project_order_filter->add_filter_option(); - project_order_filter->_setup_filters(sort_filter_titles); - project_order_filter->set_filter_size(150); - sort_filters->add_child(project_order_filter); - project_order_filter->connect("filter_changed", callable_mp(this, &ProjectManager::_on_order_option_changed)); - project_order_filter->set_custom_minimum_size(Size2(180, 10) * EDSCALE); - - int projects_sorting_order = (int)EditorSettings::get_singleton()->get("project_manager/sorting_order"); - project_order_filter->set_filter_option((ProjectListFilter::FilterOption)projects_sorting_order); - - sort_filters->add_spacer(true); - - project_filter = memnew(ProjectListFilter); - project_filter->add_search_box(); - project_filter->connect("filter_changed", callable_mp(this, &ProjectManager::_on_filter_option_changed)); - project_filter->set_custom_minimum_size(Size2(280, 10) * EDSCALE); - sort_filters->add_child(project_filter); - - search_tree_vb->add_child(sort_filters); - - PanelContainer *pc = memnew(PanelContainer); - pc->add_theme_style_override("panel", gui_base->get_theme_stylebox("bg", "Tree")); - search_tree_vb->add_child(pc); - pc->set_v_size_flags(Control::SIZE_EXPAND_FILL); - - _project_list = memnew(ProjectList); - _project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons)); - _project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask)); - pc->add_child(_project_list); - _project_list->set_enable_h_scroll(false); - - VBoxContainer *tree_vb = memnew(VBoxContainer); - tree_hb->add_child(tree_vb); - - Button *open = memnew(Button); - open->set_text(TTR("Edit")); - tree_vb->add_child(open); - open->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects_ask)); - open_btn = open; - - Button *run = memnew(Button); - run->set_text(TTR("Run")); - tree_vb->add_child(run); - run->connect("pressed", callable_mp(this, &ProjectManager::_run_project)); - run_btn = run; - - tree_vb->add_child(memnew(HSeparator)); - - Button *scan = memnew(Button); - scan->set_text(TTR("Scan")); - tree_vb->add_child(scan); - scan->connect("pressed", callable_mp(this, &ProjectManager::_scan_projects)); - - tree_vb->add_child(memnew(HSeparator)); - - scan_dir = memnew(FileDialog); - scan_dir->set_access(FileDialog::ACCESS_FILESYSTEM); - scan_dir->set_file_mode(FileDialog::FILE_MODE_OPEN_DIR); - scan_dir->set_title(TTR("Select a Folder to Scan")); // must be after mode or it's overridden - scan_dir->set_current_dir(EditorSettings::get_singleton()->get("filesystem/directories/default_project_path")); - gui_base->add_child(scan_dir); - scan_dir->connect("dir_selected", callable_mp(this, &ProjectManager::_scan_begin)); - - Button *create = memnew(Button); - create->set_text(TTR("New Project")); - tree_vb->add_child(create); - create->connect("pressed", callable_mp(this, &ProjectManager::_new_project)); - - Button *import = memnew(Button); - import->set_text(TTR("Import")); - tree_vb->add_child(import); - import->connect("pressed", callable_mp(this, &ProjectManager::_import_project)); - - Button *rename = memnew(Button); - rename->set_text(TTR("Rename")); - tree_vb->add_child(rename); - rename->connect("pressed", callable_mp(this, &ProjectManager::_rename_project)); - rename_btn = rename; - - Button *erase = memnew(Button); - erase->set_text(TTR("Remove")); - tree_vb->add_child(erase); - erase->connect("pressed", callable_mp(this, &ProjectManager::_erase_project)); - erase_btn = erase; - - Button *erase_missing = memnew(Button); - erase_missing->set_text(TTR("Remove Missing")); - tree_vb->add_child(erase_missing); - erase_missing->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects)); - erase_missing_btn = erase_missing; - - tree_vb->add_spacer(); + HBoxContainer *projects_hb = memnew(HBoxContainer); + projects_hb->set_name(TTR("Local Projects")); + tabs->add_child(projects_hb); - if (StreamPeerSSL::is_available()) { - asset_library = memnew(EditorAssetLibrary(true)); - asset_library->set_name(TTR("Templates")); - tabs->add_child(asset_library); - asset_library->connect("install_asset", callable_mp(this, &ProjectManager::_install_project)); - } else { - WARN_PRINT("Asset Library not available, as it requires SSL to work."); + { + // Projects + search bar + VBoxContainer *search_tree_vb = memnew(VBoxContainer); + projects_hb->add_child(search_tree_vb); + search_tree_vb->set_h_size_flags(Control::SIZE_EXPAND_FILL); + + HBoxContainer *hb = memnew(HBoxContainer); + hb->set_h_size_flags(Control::SIZE_EXPAND_FILL); + search_tree_vb->add_child(hb); + + search_box = memnew(LineEdit); + search_box->set_placeholder(TTR("Filter projects")); + search_box->set_tooltip(TTR("This field filters projects by name and last path component.\nTo filter projects by name and full path, the query must contain at least one `/` character.")); + search_box->connect("text_changed", callable_mp(this, &ProjectManager::_on_search_term_changed)); + search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL); + hb->add_child(search_box); + + loading_label = memnew(Label(TTR("Loading, please wait..."))); + loading_label->add_theme_font_override("font", get_theme_font("bold", "EditorFonts")); + loading_label->set_h_size_flags(Control::SIZE_EXPAND_FILL); + hb->add_child(loading_label); + // Hide the label but make it still take up space. This prevents reflows when showing the label. + loading_label->set_modulate(Color(0, 0, 0, 0)); + + Label *sort_label = memnew(Label); + sort_label->set_text(TTR("Sort:")); + hb->add_child(sort_label); + + filter_option = memnew(OptionButton); + filter_option->set_clip_text(true); + filter_option->set_custom_minimum_size(Size2(150 * EDSCALE, 10 * EDSCALE)); + filter_option->connect("item_selected", callable_mp(this, &ProjectManager::_on_order_option_changed)); + hb->add_child(filter_option); + + Vector<String> sort_filter_titles; + sort_filter_titles.push_back(TTR("Name")); + sort_filter_titles.push_back(TTR("Path")); + sort_filter_titles.push_back(TTR("Last Edited")); + + for (int i = 0; i < sort_filter_titles.size(); i++) { + filter_option->add_item(sort_filter_titles[i]); + } + + PanelContainer *pc = memnew(PanelContainer); + pc->add_theme_style_override("panel", get_theme_stylebox("bg", "Tree")); + pc->set_v_size_flags(Control::SIZE_EXPAND_FILL); + search_tree_vb->add_child(pc); + + _project_list = memnew(ProjectList); + _project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons)); + _project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask)); + _project_list->set_enable_h_scroll(false); + pc->add_child(_project_list); } - HBoxContainer *settings_hb = memnew(HBoxContainer); - settings_hb->set_alignment(BoxContainer::ALIGN_END); - settings_hb->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN); - - Label *version_label = memnew(Label); - String hash = String(VERSION_HASH); - if (hash.length() != 0) { - hash = "." + hash.left(9); + { + // Project tab side bar + VBoxContainer *tree_vb = memnew(VBoxContainer); + tree_vb->set_custom_minimum_size(Size2(120, 120)); + projects_hb->add_child(tree_vb); + + Button *create = memnew(Button); + create->set_text(TTR("New Project")); + create->set_shortcut(ED_SHORTCUT("project_manager/new_project", TTR("New Project"), KEY_MASK_CMD | KEY_N)); + create->connect("pressed", callable_mp(this, &ProjectManager::_new_project)); + tree_vb->add_child(create); + + Button *import = memnew(Button); + import->set_text(TTR("Import")); + import->set_shortcut(ED_SHORTCUT("project_manager/import_project", TTR("Import Project"), KEY_MASK_CMD | KEY_I)); + import->connect("pressed", callable_mp(this, &ProjectManager::_import_project)); + tree_vb->add_child(import); + + Button *scan = memnew(Button); + scan->set_text(TTR("Scan")); + scan->set_shortcut(ED_SHORTCUT("project_manager/scan_projects", TTR("Scan Projects"), KEY_MASK_CMD | KEY_S)); + scan->connect("pressed", callable_mp(this, &ProjectManager::_scan_projects)); + tree_vb->add_child(scan); + + tree_vb->add_child(memnew(HSeparator)); + + open_btn = memnew(Button); + open_btn->set_text(TTR("Edit")); + open_btn->set_shortcut(ED_SHORTCUT("project_manager/edit_project", TTR("Edit Project"), KEY_MASK_CMD | KEY_E)); + open_btn->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects_ask)); + tree_vb->add_child(open_btn); + + run_btn = memnew(Button); + run_btn->set_text(TTR("Run")); + run_btn->set_shortcut(ED_SHORTCUT("project_manager/run_project", TTR("Run Project"), KEY_MASK_CMD | KEY_R)); + run_btn->connect("pressed", callable_mp(this, &ProjectManager::_run_project)); + tree_vb->add_child(run_btn); + + rename_btn = memnew(Button); + rename_btn->set_text(TTR("Rename")); + rename_btn->set_shortcut(ED_SHORTCUT("project_manager/rename_project", TTR("Rename Project"), KEY_F2)); + rename_btn->connect("pressed", callable_mp(this, &ProjectManager::_rename_project)); + tree_vb->add_child(rename_btn); + + erase_btn = memnew(Button); + erase_btn->set_text(TTR("Remove")); + erase_btn->set_shortcut(ED_SHORTCUT("project_manager/remove_project", TTR("Remove Project"), KEY_DELETE)); + erase_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_project)); + tree_vb->add_child(erase_btn); + + erase_missing_btn = memnew(Button); + erase_missing_btn->set_text(TTR("Remove Missing")); + erase_missing_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects)); + tree_vb->add_child(erase_missing_btn); + + tree_vb->add_spacer(); + + about_btn = memnew(Button); + about_btn->set_text(TTR("About")); + about_btn->connect("pressed", callable_mp(this, &ProjectManager::_show_about)); + tree_vb->add_child(about_btn); } - version_label->set_text("v" VERSION_FULL_BUILD "" + hash); - // Fade out the version label to be less prominent, but still readable - version_label->set_self_modulate(Color(1, 1, 1, 0.6)); - version_label->set_align(Label::ALIGN_CENTER); - settings_hb->add_child(version_label); - language_btn = memnew(OptionButton); - language_btn->set_flat(true); - language_btn->set_focus_mode(Control::FOCUS_NONE); - - Vector<String> editor_languages; - List<PropertyInfo> editor_settings_properties; - EditorSettings::get_singleton()->get_property_list(&editor_settings_properties); - for (List<PropertyInfo>::Element *E = editor_settings_properties.front(); E; E = E->next()) { - PropertyInfo &pi = E->get(); - if (pi.name == "interface/editor/editor_language") { - editor_languages = pi.hint_string.split(","); - } - } - String current_lang = EditorSettings::get_singleton()->get("interface/editor/editor_language"); - for (int i = 0; i < editor_languages.size(); i++) { - String lang = editor_languages[i]; - String lang_name = TranslationServer::get_singleton()->get_locale_name(lang); - language_btn->add_item(lang_name + " [" + lang + "]", i); - language_btn->set_item_metadata(i, lang); - if (current_lang == lang) { - language_btn->select(i); - language_btn->set_text(lang); + { + // Version info and language options + settings_hb = memnew(HBoxContainer); + settings_hb->set_alignment(BoxContainer::ALIGN_END); + settings_hb->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN); + settings_hb->set_anchors_and_offsets_preset(Control::PRESET_TOP_RIGHT); + + // A VBoxContainer that contains a dummy Control node to adjust the LinkButton's vertical position. + VBoxContainer *spacer_vb = memnew(VBoxContainer); + settings_hb->add_child(spacer_vb); + + Control *v_spacer = memnew(Control); + spacer_vb->add_child(v_spacer); + + version_btn = memnew(LinkButton); + String hash = String(VERSION_HASH); + if (hash.length() != 0) { + hash = " " + vformat("[%s]", hash.left(9)); + } + version_btn->set_text("v" VERSION_FULL_BUILD + hash); + // Fade the version label to be less prominent, but still readable. + version_btn->set_self_modulate(Color(1, 1, 1, 0.6)); + version_btn->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER); + version_btn->set_tooltip(TTR("Click to copy.")); + version_btn->connect("pressed", callable_mp(this, &ProjectManager::_version_button_pressed)); + spacer_vb->add_child(version_btn); + + // Add a small horizontal spacer between the version and language buttons + // to distinguish them. + Control *h_spacer = memnew(Control); + settings_hb->add_child(h_spacer); + + language_btn = memnew(OptionButton); + language_btn->set_flat(true); + language_btn->set_icon(get_theme_icon("Environment", "EditorIcons")); + language_btn->set_focus_mode(Control::FOCUS_NONE); + language_btn->connect("item_selected", callable_mp(this, &ProjectManager::_language_selected)); + + Vector<String> editor_languages; + List<PropertyInfo> editor_settings_properties; + EditorSettings::get_singleton()->get_property_list(&editor_settings_properties); + for (List<PropertyInfo>::Element *E = editor_settings_properties.front(); E; E = E->next()) { + PropertyInfo &pi = E->get(); + if (pi.name == "interface/editor/editor_language") { + editor_languages = pi.hint_string.split(","); + break; + } } - } - language_btn->set_icon(get_theme_icon("Environment", "EditorIcons")); - settings_hb->add_child(language_btn); - language_btn->connect("item_selected", callable_mp(this, &ProjectManager::_language_selected)); + String current_lang = EditorSettings::get_singleton()->get("interface/editor/editor_language"); + language_btn->set_text(current_lang); - center_box->add_child(settings_hb); - settings_hb->set_anchors_and_margins_preset(Control::PRESET_TOP_RIGHT); - - ////////////////////////////////////////////////////////////// - - language_restart_ask = memnew(ConfirmationDialog); - language_restart_ask->get_ok()->set_text(TTR("Restart Now")); - language_restart_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_restart_confirm)); - language_restart_ask->get_cancel()->set_text(TTR("Continue")); - gui_base->add_child(language_restart_ask); - - erase_missing_ask = memnew(ConfirmationDialog); - erase_missing_ask->get_ok()->set_text(TTR("Remove All")); - erase_missing_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects_confirm)); - gui_base->add_child(erase_missing_ask); - - erase_ask = memnew(ConfirmationDialog); - erase_ask->get_ok()->set_text(TTR("Remove")); - erase_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_erase_project_confirm)); - gui_base->add_child(erase_ask); - - multi_open_ask = memnew(ConfirmationDialog); - multi_open_ask->get_ok()->set_text(TTR("Edit")); - multi_open_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects)); - gui_base->add_child(multi_open_ask); - - multi_run_ask = memnew(ConfirmationDialog); - multi_run_ask->get_ok()->set_text(TTR("Run")); - multi_run_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_run_project_confirm)); - gui_base->add_child(multi_run_ask); - - multi_scan_ask = memnew(ConfirmationDialog); - multi_scan_ask->get_ok()->set_text(TTR("Scan")); - gui_base->add_child(multi_scan_ask); - - ask_update_settings = memnew(ConfirmationDialog); - ask_update_settings->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_confirm_update_settings)); - gui_base->add_child(ask_update_settings); - - OS::get_singleton()->set_low_processor_usage_mode(true); - - npdialog = memnew(ProjectDialog); - gui_base->add_child(npdialog); - - npdialog->connect("projects_updated", callable_mp(this, &ProjectManager::_on_projects_updated)); - npdialog->connect("project_created", callable_mp(this, &ProjectManager::_on_project_created)); - - _load_recent_projects(); + for (int i = 0; i < editor_languages.size(); i++) { + String lang = editor_languages[i]; + String lang_name = TranslationServer::get_singleton()->get_locale_name(lang); + language_btn->add_item(lang_name + " [" + lang + "]", i); + language_btn->set_item_metadata(i, lang); + if (current_lang == lang) { + language_btn->select(i); + } + } - if (EditorSettings::get_singleton()->get("filesystem/directories/autoscan_project_path")) { - _scan_begin(EditorSettings::get_singleton()->get("filesystem/directories/autoscan_project_path")); + settings_hb->add_child(language_btn); + center_box->add_child(settings_hb); } - SceneTree::get_singleton()->get_root()->connect("files_dropped", callable_mp(this, &ProjectManager::_files_dropped)); - - run_error_diag = memnew(AcceptDialog); - gui_base->add_child(run_error_diag); - run_error_diag->set_title(TTR("Can't run project")); + if (StreamPeerSSL::is_available()) { + asset_library = memnew(EditorAssetLibrary(true)); + asset_library->set_name(TTR("Asset Library Projects")); + tabs->add_child(asset_library); + asset_library->connect("install_asset", callable_mp(this, &ProjectManager::_install_project)); + } else { + WARN_PRINT("Asset Library not available, as it requires SSL to work."); + } - dialog_error = memnew(AcceptDialog); - gui_base->add_child(dialog_error); + { + // Dialogs + language_restart_ask = memnew(ConfirmationDialog); + language_restart_ask->get_ok_button()->set_text(TTR("Restart Now")); + language_restart_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_restart_confirm)); + language_restart_ask->get_cancel_button()->set_text(TTR("Continue")); + add_child(language_restart_ask); + + scan_dir = memnew(FileDialog); + scan_dir->set_access(FileDialog::ACCESS_FILESYSTEM); + scan_dir->set_file_mode(FileDialog::FILE_MODE_OPEN_DIR); + scan_dir->set_title(TTR("Select a Folder to Scan")); // must be after mode or it's overridden + scan_dir->set_current_dir(EditorSettings::get_singleton()->get("filesystem/directories/default_project_path")); + add_child(scan_dir); + scan_dir->connect("dir_selected", callable_mp(this, &ProjectManager::_scan_begin)); + + erase_missing_ask = memnew(ConfirmationDialog); + erase_missing_ask->get_ok_button()->set_text(TTR("Remove All")); + erase_missing_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects_confirm)); + add_child(erase_missing_ask); + + erase_ask = memnew(ConfirmationDialog); + erase_ask->get_ok_button()->set_text(TTR("Remove")); + erase_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_erase_project_confirm)); + add_child(erase_ask); + + VBoxContainer *erase_ask_vb = memnew(VBoxContainer); + erase_ask->add_child(erase_ask_vb); + + erase_ask_label = memnew(Label); + erase_ask_vb->add_child(erase_ask_label); + + delete_project_contents = memnew(CheckBox); + delete_project_contents->set_text(TTR("Also delete project contents (no undo!)")); + erase_ask_vb->add_child(delete_project_contents); + + multi_open_ask = memnew(ConfirmationDialog); + multi_open_ask->get_ok_button()->set_text(TTR("Edit")); + multi_open_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects)); + add_child(multi_open_ask); + + multi_run_ask = memnew(ConfirmationDialog); + multi_run_ask->get_ok_button()->set_text(TTR("Run")); + multi_run_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_run_project_confirm)); + add_child(multi_run_ask); + + multi_scan_ask = memnew(ConfirmationDialog); + multi_scan_ask->get_ok_button()->set_text(TTR("Scan")); + add_child(multi_scan_ask); + + ask_update_settings = memnew(ConfirmationDialog); + ask_update_settings->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_confirm_update_settings)); + add_child(ask_update_settings); + + npdialog = memnew(ProjectDialog); + npdialog->connect("projects_updated", callable_mp(this, &ProjectManager::_on_projects_updated)); + npdialog->connect("project_created", callable_mp(this, &ProjectManager::_on_project_created)); + add_child(npdialog); + + run_error_diag = memnew(AcceptDialog); + run_error_diag->set_title(TTR("Can't run project")); + add_child(run_error_diag); - open_templates = memnew(ConfirmationDialog); - open_templates->set_text(TTR("You currently don't have any projects.\nWould you like to explore official example projects in the Asset Library?")); - open_templates->get_ok()->set_text(TTR("Open Asset Library")); - open_templates->connect("confirmed", callable_mp(this, &ProjectManager::_open_asset_library)); - add_child(open_templates); -} + dialog_error = memnew(AcceptDialog); + add_child(dialog_error); -ProjectManager::~ProjectManager() { - if (EditorSettings::get_singleton()) { - EditorSettings::destroy(); - } -} + open_templates = memnew(ConfirmationDialog); + open_templates->set_text(TTR("You currently don't have any projects.\nWould you like to explore official example projects in the Asset Library?")); + open_templates->get_ok_button()->set_text(TTR("Open Asset Library")); + open_templates->connect("confirmed", callable_mp(this, &ProjectManager::_open_asset_library)); + add_child(open_templates); -void ProjectListFilter::_setup_filters(Vector<String> options) { - filter_option->clear(); - for (int i = 0; i < options.size(); i++) { - filter_option->add_item(options[i]); + about = memnew(EditorAbout); + add_child(about); } -} -void ProjectListFilter::_search_text_changed(const String &p_newtext) { - emit_signal("filter_changed"); -} - -String ProjectListFilter::get_search_term() { - return search_box->get_text().strip_edges(); -} - -ProjectListFilter::FilterOption ProjectListFilter::get_filter_option() { - return _current_filter; -} + _load_recent_projects(); -void ProjectListFilter::set_filter_option(FilterOption option) { - filter_option->select((int)option); - _filter_option_selected(0); -} + DirAccessRef dir_access = DirAccess::create(DirAccess::AccessType::ACCESS_FILESYSTEM); -void ProjectListFilter::_filter_option_selected(int p_idx) { - FilterOption selected = (FilterOption)(filter_option->get_selected()); - if (_current_filter != selected) { - _current_filter = selected; - if (is_inside_tree()) { - emit_signal("filter_changed"); + String default_project_path = EditorSettings::get_singleton()->get("filesystem/directories/default_project_path"); + if (!dir_access->dir_exists(default_project_path)) { + Error error = dir_access->make_dir_recursive(default_project_path); + if (error != OK) { + ERR_PRINT("Could not create default project directory at: " + default_project_path); } } -} -void ProjectListFilter::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE && has_search_box) { - search_box->set_right_icon(get_theme_icon("Search", "EditorIcons")); - search_box->set_clear_button_enabled(true); + String autoscan_path = EditorSettings::get_singleton()->get("filesystem/directories/autoscan_project_path"); + if (autoscan_path != "") { + if (dir_access->dir_exists(autoscan_path)) { + _scan_begin(autoscan_path); + } else { + Error error = dir_access->make_dir_recursive(autoscan_path); + if (error != OK) { + ERR_PRINT("Could not create project autoscan directory at: " + autoscan_path); + } + } } -} - -void ProjectListFilter::_bind_methods() { - ADD_SIGNAL(MethodInfo("filter_changed")); -} - -void ProjectListFilter::add_filter_option() { - filter_option = memnew(OptionButton); - filter_option->set_clip_text(true); - filter_option->connect("item_selected", callable_mp(this, &ProjectListFilter::_filter_option_selected)); - add_child(filter_option); -} - -void ProjectListFilter::add_search_box() { - search_box = memnew(LineEdit); - search_box->set_placeholder(TTR("Search")); - search_box->set_tooltip( - TTR("The search box filters projects by name and last path component.\nTo filter projects by name and full path, the query must contain at least one `/` character.")); - search_box->connect("text_changed", callable_mp(this, &ProjectListFilter::_search_text_changed)); - search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL); - add_child(search_box); - has_search_box = true; -} - -void ProjectListFilter::set_filter_size(int h_size) { - filter_option->set_custom_minimum_size(Size2(h_size * EDSCALE, 10 * EDSCALE)); -} + SceneTree::get_singleton()->get_root()->connect("files_dropped", callable_mp(this, &ProjectManager::_files_dropped)); -ProjectListFilter::ProjectListFilter() { - _current_filter = FILTER_NAME; - has_search_box = false; + OS::get_singleton()->set_low_processor_usage_mode(true); } -void ProjectListFilter::clear() { - if (has_search_box) { - search_box->clear(); +ProjectManager::~ProjectManager() { + if (EditorSettings::get_singleton()) { + EditorSettings::destroy(); } } |