1/**************************************************************************/
2/* project_manager.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "project_manager.h"
32
33#include "core/config/project_settings.h"
34#include "core/io/config_file.h"
35#include "core/io/dir_access.h"
36#include "core/io/file_access.h"
37#include "core/io/resource_saver.h"
38#include "core/io/stream_peer_tls.h"
39#include "core/io/zip_io.h"
40#include "core/os/keyboard.h"
41#include "core/os/os.h"
42#include "core/string/translation.h"
43#include "core/version.h"
44#include "editor/editor_paths.h"
45#include "editor/editor_scale.h"
46#include "editor/editor_settings.h"
47#include "editor/editor_string_names.h"
48#include "editor/editor_themes.h"
49#include "editor/editor_vcs_interface.h"
50#include "editor/gui/editor_file_dialog.h"
51#include "editor/plugins/asset_library_editor_plugin.h"
52#include "main/main.h"
53#include "scene/gui/center_container.h"
54#include "scene/gui/check_box.h"
55#include "scene/gui/color_rect.h"
56#include "scene/gui/flow_container.h"
57#include "scene/gui/line_edit.h"
58#include "scene/gui/margin_container.h"
59#include "scene/gui/panel_container.h"
60#include "scene/gui/separator.h"
61#include "scene/gui/texture_rect.h"
62#include "scene/main/window.h"
63#include "scene/resources/image_texture.h"
64#include "servers/display_server.h"
65#include "servers/navigation_server_3d.h"
66#include "servers/physics_server_2d.h"
67
68constexpr int GODOT4_CONFIG_VERSION = 5;
69
70/// Project Dialog.
71
72void ProjectDialog::_set_message(const String &p_msg, MessageType p_type, InputType input_type) {
73 msg->set_text(p_msg);
74 Ref<Texture2D> current_path_icon = status_rect->get_texture();
75 Ref<Texture2D> current_install_icon = install_status_rect->get_texture();
76 Ref<Texture2D> new_icon;
77
78 switch (p_type) {
79 case MESSAGE_ERROR: {
80 msg->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
81 msg->set_modulate(Color(1, 1, 1, 1));
82 new_icon = get_editor_theme_icon(SNAME("StatusError"));
83
84 } break;
85 case MESSAGE_WARNING: {
86 msg->add_theme_color_override("font_color", get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
87 msg->set_modulate(Color(1, 1, 1, 1));
88 new_icon = get_editor_theme_icon(SNAME("StatusWarning"));
89
90 } break;
91 case MESSAGE_SUCCESS: {
92 msg->remove_theme_color_override("font_color");
93 msg->set_modulate(Color(1, 1, 1, 0));
94 new_icon = get_editor_theme_icon(SNAME("StatusSuccess"));
95
96 } break;
97 }
98
99 if (current_path_icon != new_icon && input_type == PROJECT_PATH) {
100 status_rect->set_texture(new_icon);
101 } else if (current_install_icon != new_icon && input_type == INSTALL_PATH) {
102 install_status_rect->set_texture(new_icon);
103 }
104
105 Size2i window_size = get_size();
106 Size2 contents_min_size = get_contents_minimum_size();
107 if (window_size.x < contents_min_size.x || window_size.y < contents_min_size.y) {
108 set_size(window_size.max(contents_min_size));
109 }
110}
111
112String ProjectDialog::_test_path() {
113 Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
114 String valid_path, valid_install_path;
115 if (d->change_dir(project_path->get_text()) == OK) {
116 valid_path = project_path->get_text();
117 } else if (d->change_dir(project_path->get_text().strip_edges()) == OK) {
118 valid_path = project_path->get_text().strip_edges();
119 } else if (project_path->get_text().ends_with(".zip")) {
120 if (d->file_exists(project_path->get_text())) {
121 valid_path = project_path->get_text();
122 }
123 } else if (project_path->get_text().strip_edges().ends_with(".zip")) {
124 if (d->file_exists(project_path->get_text().strip_edges())) {
125 valid_path = project_path->get_text().strip_edges();
126 }
127 }
128
129 if (valid_path.is_empty()) {
130 _set_message(TTR("The path specified doesn't exist."), MESSAGE_ERROR);
131 get_ok_button()->set_disabled(true);
132 return "";
133 }
134
135 if (mode == MODE_IMPORT && valid_path.ends_with(".zip")) {
136 if (d->change_dir(install_path->get_text()) == OK) {
137 valid_install_path = install_path->get_text();
138 } else if (d->change_dir(install_path->get_text().strip_edges()) == OK) {
139 valid_install_path = install_path->get_text().strip_edges();
140 }
141
142 if (valid_install_path.is_empty()) {
143 _set_message(TTR("The path specified doesn't exist."), MESSAGE_ERROR, INSTALL_PATH);
144 get_ok_button()->set_disabled(true);
145 return "";
146 }
147 }
148
149 if (mode == MODE_IMPORT || mode == MODE_RENAME) {
150 if (!valid_path.is_empty() && !d->file_exists("project.godot")) {
151 if (valid_path.ends_with(".zip")) {
152 Ref<FileAccess> io_fa;
153 zlib_filefunc_def io = zipio_create_io(&io_fa);
154
155 unzFile pkg = unzOpen2(valid_path.utf8().get_data(), &io);
156 if (!pkg) {
157 _set_message(TTR("Error opening package file (it's not in ZIP format)."), MESSAGE_ERROR);
158 get_ok_button()->set_disabled(true);
159 unzClose(pkg);
160 return "";
161 }
162
163 int ret = unzGoToFirstFile(pkg);
164 while (ret == UNZ_OK) {
165 unz_file_info info;
166 char fname[16384];
167 ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
168 if (ret != UNZ_OK) {
169 break;
170 }
171
172 if (String::utf8(fname).ends_with("project.godot")) {
173 break;
174 }
175
176 ret = unzGoToNextFile(pkg);
177 }
178
179 if (ret == UNZ_END_OF_LIST_OF_FILE) {
180 _set_message(TTR("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR);
181 get_ok_button()->set_disabled(true);
182 unzClose(pkg);
183 return "";
184 }
185
186 unzClose(pkg);
187
188 // check if the specified install folder is empty, even though this is not an error, it is good to check here
189 d->list_dir_begin();
190 is_folder_empty = true;
191 String n = d->get_next();
192 while (!n.is_empty()) {
193 if (!n.begins_with(".")) {
194 // Allow `.`, `..` (reserved current/parent folder names)
195 // and hidden files/folders to be present.
196 // For instance, this lets users initialize a Git repository
197 // and still be able to create a project in the directory afterwards.
198 is_folder_empty = false;
199 break;
200 }
201 n = d->get_next();
202 }
203 d->list_dir_end();
204
205 if (!is_folder_empty) {
206 _set_message(TTR("Please choose an empty folder."), MESSAGE_WARNING, INSTALL_PATH);
207 get_ok_button()->set_disabled(true);
208 return "";
209 }
210
211 } else {
212 _set_message(TTR("Please choose a \"project.godot\", a directory with it, or a \".zip\" file."), MESSAGE_ERROR);
213 install_path_container->hide();
214 get_ok_button()->set_disabled(true);
215 return "";
216 }
217
218 } else if (valid_path.ends_with("zip")) {
219 _set_message(TTR("This directory already contains a Godot project."), MESSAGE_ERROR, INSTALL_PATH);
220 get_ok_button()->set_disabled(true);
221 return "";
222 }
223
224 } else {
225 // Check if the specified folder is empty, even though this is not an error, it is good to check here.
226 d->list_dir_begin();
227 is_folder_empty = true;
228 String n = d->get_next();
229 while (!n.is_empty()) {
230 if (!n.begins_with(".")) {
231 // Allow `.`, `..` (reserved current/parent folder names)
232 // and hidden files/folders to be present.
233 // For instance, this lets users initialize a Git repository
234 // and still be able to create a project in the directory afterwards.
235 is_folder_empty = false;
236 break;
237 }
238 n = d->get_next();
239 }
240 d->list_dir_end();
241
242 if (!is_folder_empty) {
243 if (valid_path == OS::get_singleton()->get_environment("HOME") || valid_path == OS::get_singleton()->get_system_dir(OS::SYSTEM_DIR_DOCUMENTS) || valid_path == OS::get_singleton()->get_executable_path().get_base_dir()) {
244 _set_message(TTR("You cannot save a project in the selected path. Please make a new folder or choose a new path."), MESSAGE_ERROR);
245 get_ok_button()->set_disabled(true);
246 return "";
247 }
248
249 _set_message(TTR("The selected path is not empty. Choosing an empty folder is highly recommended."), MESSAGE_WARNING);
250 get_ok_button()->set_disabled(false);
251 return valid_path;
252 }
253 }
254
255 _set_message("");
256 _set_message("", MESSAGE_SUCCESS, INSTALL_PATH);
257 get_ok_button()->set_disabled(false);
258 return valid_path;
259}
260
261void ProjectDialog::_path_text_changed(const String &p_path) {
262 String sp = _test_path();
263 if (!sp.is_empty()) {
264 // If the project name is empty or default, infer the project name from the selected folder name
265 if (project_name->get_text().strip_edges().is_empty() || project_name->get_text().strip_edges() == TTR("New Game Project")) {
266 sp = sp.replace("\\", "/");
267 int lidx = sp.rfind("/");
268
269 if (lidx != -1) {
270 sp = sp.substr(lidx + 1, sp.length()).capitalize();
271 }
272 if (sp.is_empty() && mode == MODE_IMPORT) {
273 sp = TTR("Imported Project");
274 }
275
276 project_name->set_text(sp);
277 _text_changed(sp);
278 }
279 }
280
281 if (!created_folder_path.is_empty() && created_folder_path != p_path) {
282 _remove_created_folder();
283 }
284}
285
286void ProjectDialog::_file_selected(const String &p_path) {
287 // If not already shown.
288 show_dialog();
289
290 String p = p_path;
291 if (mode == MODE_IMPORT) {
292 if (p.ends_with("project.godot")) {
293 p = p.get_base_dir();
294 install_path_container->hide();
295 get_ok_button()->set_disabled(false);
296 } else if (p.ends_with(".zip")) {
297 install_path->set_text(p.get_base_dir());
298 install_path_container->show();
299 get_ok_button()->set_disabled(false);
300 } else {
301 _set_message(TTR("Please choose a \"project.godot\" or \".zip\" file."), MESSAGE_ERROR);
302 get_ok_button()->set_disabled(true);
303 return;
304 }
305 }
306
307 String sp = p.simplify_path();
308 project_path->set_text(sp);
309 _path_text_changed(sp);
310 if (p.ends_with(".zip")) {
311 install_path->call_deferred(SNAME("grab_focus"));
312 } else {
313 get_ok_button()->call_deferred(SNAME("grab_focus"));
314 }
315}
316
317void ProjectDialog::_path_selected(const String &p_path) {
318 // If not already shown.
319 show_dialog();
320
321 String sp = p_path.simplify_path();
322 project_path->set_text(sp);
323 _path_text_changed(sp);
324 get_ok_button()->call_deferred(SNAME("grab_focus"));
325}
326
327void ProjectDialog::_install_path_selected(const String &p_path) {
328 String sp = p_path.simplify_path();
329 install_path->set_text(sp);
330 _path_text_changed(sp);
331 get_ok_button()->call_deferred(SNAME("grab_focus"));
332}
333
334void ProjectDialog::_browse_path() {
335 fdialog->set_current_dir(project_path->get_text());
336
337 if (mode == MODE_IMPORT) {
338 fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_ANY);
339 fdialog->clear_filters();
340 fdialog->add_filter("project.godot", vformat("%s %s", VERSION_NAME, TTR("Project")));
341 fdialog->add_filter("*.zip", TTR("ZIP File"));
342 } else {
343 fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
344 }
345 fdialog->popup_file_dialog();
346}
347
348void ProjectDialog::_browse_install_path() {
349 fdialog_install->set_current_dir(install_path->get_text());
350 fdialog_install->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
351 fdialog_install->popup_file_dialog();
352}
353
354void ProjectDialog::_create_folder() {
355 const String project_name_no_edges = project_name->get_text().strip_edges();
356 if (project_name_no_edges.is_empty() || !created_folder_path.is_empty() || project_name_no_edges.ends_with(".")) {
357 _set_message(TTR("Invalid project name."), MESSAGE_WARNING);
358 return;
359 }
360
361 Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
362 if (d->change_dir(project_path->get_text()) == OK) {
363 if (!d->dir_exists(project_name_no_edges)) {
364 if (d->make_dir(project_name_no_edges) == OK) {
365 d->change_dir(project_name_no_edges);
366 String dir_str = d->get_current_dir();
367 project_path->set_text(dir_str);
368 _path_text_changed(dir_str);
369 created_folder_path = d->get_current_dir();
370 create_dir->set_disabled(true);
371 } else {
372 dialog_error->set_text(TTR("Couldn't create folder."));
373 dialog_error->popup_centered();
374 }
375 } else {
376 dialog_error->set_text(TTR("There is already a folder in this path with the specified name."));
377 dialog_error->popup_centered();
378 }
379 }
380}
381
382void ProjectDialog::_text_changed(const String &p_text) {
383 if (mode != MODE_NEW) {
384 return;
385 }
386
387 _test_path();
388
389 if (p_text.strip_edges().is_empty()) {
390 _set_message(TTR("It would be a good idea to name your project."), MESSAGE_ERROR);
391 }
392}
393
394void ProjectDialog::_nonempty_confirmation_ok_pressed() {
395 is_folder_empty = true;
396 ok_pressed();
397}
398
399void ProjectDialog::_renderer_selected() {
400 ERR_FAIL_COND(!renderer_button_group->get_pressed_button());
401
402 String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));
403
404 if (renderer_type == "forward_plus") {
405 renderer_info->set_text(
406 String::utf8("• ") + TTR("Supports desktop platforms only.") +
407 String::utf8("\n• ") + TTR("Advanced 3D graphics available.") +
408 String::utf8("\n• ") + TTR("Can scale to large complex scenes.") +
409 String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +
410 String::utf8("\n• ") + TTR("Slower rendering of simple scenes."));
411 } else if (renderer_type == "mobile") {
412 renderer_info->set_text(
413 String::utf8("• ") + TTR("Supports desktop + mobile platforms.") +
414 String::utf8("\n• ") + TTR("Less advanced 3D graphics.") +
415 String::utf8("\n• ") + TTR("Less scalable for complex scenes.") +
416 String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +
417 String::utf8("\n• ") + TTR("Fast rendering of simple scenes."));
418 } else if (renderer_type == "gl_compatibility") {
419 renderer_info->set_text(
420 String::utf8("• ") + TTR("Supports desktop, mobile + web platforms.") +
421 String::utf8("\n• ") + TTR("Least advanced 3D graphics (currently work-in-progress).") +
422 String::utf8("\n• ") + TTR("Intended for low-end/older devices.") +
423 String::utf8("\n• ") + TTR("Uses OpenGL 3 backend (OpenGL 3.3/ES 3.0/WebGL2).") +
424 String::utf8("\n• ") + TTR("Fastest rendering of simple scenes."));
425 } else {
426 WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
427 }
428}
429
430void ProjectDialog::_remove_created_folder() {
431 if (!created_folder_path.is_empty()) {
432 Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
433 d->remove(created_folder_path);
434
435 create_dir->set_disabled(false);
436 created_folder_path = "";
437 }
438}
439
440void ProjectDialog::ok_pressed() {
441 String dir = project_path->get_text();
442
443 if (mode == MODE_RENAME) {
444 String dir2 = _test_path();
445 if (dir2.is_empty()) {
446 _set_message(TTR("Invalid project path (changed anything?)."), MESSAGE_ERROR);
447 return;
448 }
449
450 // Load project.godot as ConfigFile to set the new name.
451 ConfigFile cfg;
452 String project_godot = dir2.path_join("project.godot");
453 Error err = cfg.load(project_godot);
454 if (err != OK) {
455 _set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR);
456 } else {
457 cfg.set_value("application", "config/name", project_name->get_text().strip_edges());
458 err = cfg.save(project_godot);
459 if (err != OK) {
460 _set_message(vformat(TTR("Couldn't save project at '%s' (error %d)."), project_godot, err), MESSAGE_ERROR);
461 }
462 }
463
464 hide();
465 emit_signal(SNAME("projects_updated"));
466
467 } else {
468 if (mode == MODE_IMPORT) {
469 if (project_path->get_text().ends_with(".zip")) {
470 mode = MODE_INSTALL;
471 ok_pressed();
472
473 return;
474 }
475
476 } else {
477 if (mode == MODE_NEW) {
478 // Before we create a project, check that the target folder is empty.
479 // If not, we need to ask the user if they're sure they want to do this.
480 if (!is_folder_empty) {
481 ConfirmationDialog *cd = memnew(ConfirmationDialog);
482 cd->set_title(TTR("Warning: This folder is not empty"));
483 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?"));
484 cd->get_ok_button()->connect("pressed", callable_mp(this, &ProjectDialog::_nonempty_confirmation_ok_pressed));
485 get_parent()->add_child(cd);
486 cd->popup_centered();
487 cd->grab_focus();
488 return;
489 }
490 PackedStringArray project_features = ProjectSettings::get_required_features();
491 ProjectSettings::CustomMap initial_settings;
492
493 // Be sure to change this code if/when renderers are changed.
494 // Default values are "forward_plus" for the main setting, "mobile" for the mobile override,
495 // and "gl_compatibility" for the web override.
496 String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));
497 initial_settings["rendering/renderer/rendering_method"] = renderer_type;
498
499 EditorSettings::get_singleton()->set("project_manager/default_renderer", renderer_type);
500 EditorSettings::get_singleton()->save();
501
502 if (renderer_type == "forward_plus") {
503 project_features.push_back("Forward Plus");
504 } else if (renderer_type == "mobile") {
505 project_features.push_back("Mobile");
506 } else if (renderer_type == "gl_compatibility") {
507 project_features.push_back("GL Compatibility");
508 // Also change the default rendering method for the mobile override.
509 initial_settings["rendering/renderer/rendering_method.mobile"] = "gl_compatibility";
510 } else {
511 WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
512 }
513
514 project_features.sort();
515 initial_settings["application/config/features"] = project_features;
516 initial_settings["application/config/name"] = project_name->get_text().strip_edges();
517 initial_settings["application/config/icon"] = "res://icon.svg";
518
519 if (ProjectSettings::get_singleton()->save_custom(dir.path_join("project.godot"), initial_settings, Vector<String>(), false) != OK) {
520 _set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR);
521 } else {
522 // Store default project icon in SVG format.
523 Error err;
524 Ref<FileAccess> fa_icon = FileAccess::open(dir.path_join("icon.svg"), FileAccess::WRITE, &err);
525 fa_icon->store_string(get_default_project_icon());
526
527 if (err != OK) {
528 _set_message(TTR("Couldn't create icon.svg in project path."), MESSAGE_ERROR);
529 }
530
531 EditorVCSInterface::create_vcs_metadata_files(EditorVCSInterface::VCSMetadata(vcs_metadata_selection->get_selected()), dir);
532 }
533 } else if (mode == MODE_INSTALL) {
534 if (project_path->get_text().ends_with(".zip")) {
535 dir = install_path->get_text();
536 zip_path = project_path->get_text();
537 }
538
539 Ref<FileAccess> io_fa;
540 zlib_filefunc_def io = zipio_create_io(&io_fa);
541
542 unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io);
543 if (!pkg) {
544 dialog_error->set_text(TTR("Error opening package file, not in ZIP format."));
545 dialog_error->popup_centered();
546 return;
547 }
548
549 // Find the zip_root
550 String zip_root;
551 int ret = unzGoToFirstFile(pkg);
552 while (ret == UNZ_OK) {
553 unz_file_info info;
554 char fname[16384];
555 unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
556
557 String name = String::utf8(fname);
558 if (name.ends_with("project.godot")) {
559 zip_root = name.substr(0, name.rfind("project.godot"));
560 break;
561 }
562
563 ret = unzGoToNextFile(pkg);
564 }
565
566 ret = unzGoToFirstFile(pkg);
567
568 Vector<String> failed_files;
569
570 while (ret == UNZ_OK) {
571 //get filename
572 unz_file_info info;
573 char fname[16384];
574 ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
575 if (ret != UNZ_OK) {
576 break;
577 }
578
579 String path = String::utf8(fname);
580
581 if (path.is_empty() || path == zip_root || !zip_root.is_subsequence_of(path)) {
582 //
583 } else if (path.ends_with("/")) { // a dir
584 path = path.substr(0, path.length() - 1);
585 String rel_path = path.substr(zip_root.length());
586
587 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
588 da->make_dir(dir.path_join(rel_path));
589 } else {
590 Vector<uint8_t> uncomp_data;
591 uncomp_data.resize(info.uncompressed_size);
592 String rel_path = path.substr(zip_root.length());
593
594 //read
595 unzOpenCurrentFile(pkg);
596 ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size());
597 ERR_BREAK_MSG(ret < 0, vformat("An error occurred while attempting to read from file: %s. This file will not be used.", rel_path));
598 unzCloseCurrentFile(pkg);
599
600 Ref<FileAccess> f = FileAccess::open(dir.path_join(rel_path), FileAccess::WRITE);
601 if (f.is_valid()) {
602 f->store_buffer(uncomp_data.ptr(), uncomp_data.size());
603 } else {
604 failed_files.push_back(rel_path);
605 }
606 }
607
608 ret = unzGoToNextFile(pkg);
609 }
610
611 unzClose(pkg);
612
613 if (failed_files.size()) {
614 String err_msg = TTR("The following files failed extraction from package:") + "\n\n";
615 for (int i = 0; i < failed_files.size(); i++) {
616 if (i > 15) {
617 err_msg += "\nAnd " + itos(failed_files.size() - i) + " more files.";
618 break;
619 }
620 err_msg += failed_files[i] + "\n";
621 }
622
623 dialog_error->set_text(err_msg);
624 dialog_error->popup_centered();
625
626 } else if (!project_path->get_text().ends_with(".zip")) {
627 dialog_error->set_text(TTR("Package installed successfully!"));
628 dialog_error->popup_centered();
629 }
630 }
631 }
632
633 dir = dir.replace("\\", "/");
634 if (dir.ends_with("/")) {
635 dir = dir.substr(0, dir.length() - 1);
636 }
637
638 hide();
639 emit_signal(SNAME("project_created"), dir);
640 }
641}
642
643void ProjectDialog::cancel_pressed() {
644 _remove_created_folder();
645
646 project_path->clear();
647 _path_text_changed("");
648 project_name->clear();
649 _text_changed("");
650
651 if (status_rect->get_texture() == get_editor_theme_icon(SNAME("StatusError"))) {
652 msg->show();
653 }
654
655 if (install_status_rect->get_texture() == get_editor_theme_icon(SNAME("StatusError"))) {
656 msg->show();
657 }
658}
659
660void ProjectDialog::set_zip_path(const String &p_path) {
661 zip_path = p_path;
662}
663
664void ProjectDialog::set_zip_title(const String &p_title) {
665 zip_title = p_title;
666}
667
668void ProjectDialog::set_mode(Mode p_mode) {
669 mode = p_mode;
670}
671
672void ProjectDialog::set_project_path(const String &p_path) {
673 project_path->set_text(p_path);
674}
675
676void ProjectDialog::ask_for_path_and_show() {
677 // Workaround: for the file selection dialog content to be rendered we need to show its parent dialog.
678 show_dialog();
679 _set_message("");
680
681 _browse_path();
682}
683
684void ProjectDialog::show_dialog() {
685 if (mode == MODE_RENAME) {
686 project_path->set_editable(false);
687 browse->hide();
688 install_browse->hide();
689
690 set_title(TTR("Rename Project"));
691 set_ok_button_text(TTR("Rename"));
692 name_container->show();
693 status_rect->hide();
694 msg->hide();
695 install_path_container->hide();
696 install_status_rect->hide();
697 renderer_container->hide();
698 default_files_container->hide();
699 get_ok_button()->set_disabled(false);
700
701 // Fetch current name from project.godot to prefill the text input.
702 ConfigFile cfg;
703 String project_godot = project_path->get_text().path_join("project.godot");
704 Error err = cfg.load(project_godot);
705 if (err != OK) {
706 _set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR);
707 status_rect->show();
708 msg->show();
709 get_ok_button()->set_disabled(true);
710 } else {
711 String cur_name = cfg.get_value("application", "config/name", "");
712 project_name->set_text(cur_name);
713 _text_changed(cur_name);
714 }
715
716 project_name->call_deferred(SNAME("grab_focus"));
717
718 create_dir->hide();
719
720 } else {
721 fav_dir = EDITOR_GET("filesystem/directories/default_project_path");
722 if (!fav_dir.is_empty()) {
723 project_path->set_text(fav_dir);
724 fdialog->set_current_dir(fav_dir);
725 } else {
726 Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
727 project_path->set_text(d->get_current_dir());
728 fdialog->set_current_dir(d->get_current_dir());
729 }
730 String proj = TTR("New Game Project");
731 project_name->set_text(proj);
732 _text_changed(proj);
733
734 project_path->set_editable(true);
735 browse->set_disabled(false);
736 browse->show();
737 install_browse->set_disabled(false);
738 install_browse->show();
739 create_dir->show();
740 status_rect->show();
741 install_status_rect->show();
742 msg->show();
743
744 if (mode == MODE_IMPORT) {
745 set_title(TTR("Import Existing Project"));
746 set_ok_button_text(TTR("Import & Edit"));
747 name_container->hide();
748 install_path_container->hide();
749 renderer_container->hide();
750 default_files_container->hide();
751 project_path->grab_focus();
752
753 } else if (mode == MODE_NEW) {
754 set_title(TTR("Create New Project"));
755 set_ok_button_text(TTR("Create & Edit"));
756 name_container->show();
757 install_path_container->hide();
758 renderer_container->show();
759 default_files_container->show();
760 project_name->call_deferred(SNAME("grab_focus"));
761 project_name->call_deferred(SNAME("select_all"));
762
763 } else if (mode == MODE_INSTALL) {
764 set_title(TTR("Install Project:") + " " + zip_title);
765 set_ok_button_text(TTR("Install & Edit"));
766 project_name->set_text(zip_title);
767 name_container->show();
768 install_path_container->hide();
769 renderer_container->hide();
770 default_files_container->hide();
771 project_path->grab_focus();
772 }
773
774 _test_path();
775 }
776
777 popup_centered(Size2(500, 0) * EDSCALE);
778}
779
780void ProjectDialog::_notification(int p_what) {
781 switch (p_what) {
782 case NOTIFICATION_WM_CLOSE_REQUEST: {
783 _remove_created_folder();
784 } break;
785 }
786}
787
788void ProjectDialog::_bind_methods() {
789 ADD_SIGNAL(MethodInfo("project_created"));
790 ADD_SIGNAL(MethodInfo("projects_updated"));
791}
792
793ProjectDialog::ProjectDialog() {
794 VBoxContainer *vb = memnew(VBoxContainer);
795 add_child(vb);
796
797 name_container = memnew(VBoxContainer);
798 vb->add_child(name_container);
799
800 Label *l = memnew(Label);
801 l->set_text(TTR("Project Name:"));
802 name_container->add_child(l);
803
804 HBoxContainer *pnhb = memnew(HBoxContainer);
805 name_container->add_child(pnhb);
806
807 project_name = memnew(LineEdit);
808 project_name->set_h_size_flags(Control::SIZE_EXPAND_FILL);
809 pnhb->add_child(project_name);
810
811 create_dir = memnew(Button);
812 pnhb->add_child(create_dir);
813 create_dir->set_text(TTR("Create Folder"));
814 create_dir->connect("pressed", callable_mp(this, &ProjectDialog::_create_folder));
815
816 path_container = memnew(VBoxContainer);
817 vb->add_child(path_container);
818
819 l = memnew(Label);
820 l->set_text(TTR("Project Path:"));
821 path_container->add_child(l);
822
823 HBoxContainer *pphb = memnew(HBoxContainer);
824 path_container->add_child(pphb);
825
826 project_path = memnew(LineEdit);
827 project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
828 project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
829 pphb->add_child(project_path);
830
831 install_path_container = memnew(VBoxContainer);
832 vb->add_child(install_path_container);
833
834 l = memnew(Label);
835 l->set_text(TTR("Project Installation Path:"));
836 install_path_container->add_child(l);
837
838 HBoxContainer *iphb = memnew(HBoxContainer);
839 install_path_container->add_child(iphb);
840
841 install_path = memnew(LineEdit);
842 install_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
843 install_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
844 iphb->add_child(install_path);
845
846 // status icon
847 status_rect = memnew(TextureRect);
848 status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
849 pphb->add_child(status_rect);
850
851 browse = memnew(Button);
852 browse->set_text(TTR("Browse"));
853 browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_path));
854 pphb->add_child(browse);
855
856 // install status icon
857 install_status_rect = memnew(TextureRect);
858 install_status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
859 iphb->add_child(install_status_rect);
860
861 install_browse = memnew(Button);
862 install_browse->set_text(TTR("Browse"));
863 install_browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_install_path));
864 iphb->add_child(install_browse);
865
866 msg = memnew(Label);
867 msg->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
868 vb->add_child(msg);
869
870 // Renderer selection.
871 renderer_container = memnew(VBoxContainer);
872 vb->add_child(renderer_container);
873 l = memnew(Label);
874 l->set_text(TTR("Renderer:"));
875 renderer_container->add_child(l);
876 HBoxContainer *rshc = memnew(HBoxContainer);
877 renderer_container->add_child(rshc);
878 renderer_button_group.instantiate();
879
880 // Left hand side, used for checkboxes to select renderer.
881 Container *rvb = memnew(VBoxContainer);
882 rshc->add_child(rvb);
883
884 String default_renderer_type = "forward_plus";
885 if (EditorSettings::get_singleton()->has_setting("project_manager/default_renderer")) {
886 default_renderer_type = EditorSettings::get_singleton()->get_setting("project_manager/default_renderer");
887 }
888
889 Button *rs_button = memnew(CheckBox);
890 rs_button->set_button_group(renderer_button_group);
891 rs_button->set_text(TTR("Forward+"));
892#if defined(WEB_ENABLED)
893 rs_button->set_disabled(true);
894#endif
895 rs_button->set_meta(SNAME("rendering_method"), "forward_plus");
896 rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
897 rvb->add_child(rs_button);
898 if (default_renderer_type == "forward_plus") {
899 rs_button->set_pressed(true);
900 }
901 rs_button = memnew(CheckBox);
902 rs_button->set_button_group(renderer_button_group);
903 rs_button->set_text(TTR("Mobile"));
904#if defined(WEB_ENABLED)
905 rs_button->set_disabled(true);
906#endif
907 rs_button->set_meta(SNAME("rendering_method"), "mobile");
908 rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
909 rvb->add_child(rs_button);
910 if (default_renderer_type == "mobile") {
911 rs_button->set_pressed(true);
912 }
913 rs_button = memnew(CheckBox);
914 rs_button->set_button_group(renderer_button_group);
915 rs_button->set_text(TTR("Compatibility"));
916#if !defined(GLES3_ENABLED)
917 rs_button->set_disabled(true);
918#endif
919 rs_button->set_meta(SNAME("rendering_method"), "gl_compatibility");
920 rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
921 rvb->add_child(rs_button);
922#if defined(GLES3_ENABLED)
923 if (default_renderer_type == "gl_compatibility") {
924 rs_button->set_pressed(true);
925 }
926#endif
927 rshc->add_child(memnew(VSeparator));
928
929 // Right hand side, used for text explaining each choice.
930 rvb = memnew(VBoxContainer);
931 rvb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
932 rshc->add_child(rvb);
933 renderer_info = memnew(Label);
934 renderer_info->set_modulate(Color(1, 1, 1, 0.7));
935 rvb->add_child(renderer_info);
936 _renderer_selected();
937
938 l = memnew(Label);
939 l->set_text(TTR("The renderer can be changed later, but scenes may need to be adjusted."));
940 // Add some extra spacing to separate it from the list above and the buttons below.
941 l->set_custom_minimum_size(Size2(0, 40) * EDSCALE);
942 l->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
943 l->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
944 l->set_modulate(Color(1, 1, 1, 0.7));
945 renderer_container->add_child(l);
946
947 default_files_container = memnew(HBoxContainer);
948 vb->add_child(default_files_container);
949 l = memnew(Label);
950 l->set_text(TTR("Version Control Metadata:"));
951 default_files_container->add_child(l);
952 vcs_metadata_selection = memnew(OptionButton);
953 vcs_metadata_selection->set_custom_minimum_size(Size2(100, 20));
954 vcs_metadata_selection->add_item(TTR("None"), (int)EditorVCSInterface::VCSMetadata::NONE);
955 vcs_metadata_selection->add_item(TTR("Git"), (int)EditorVCSInterface::VCSMetadata::GIT);
956 vcs_metadata_selection->select((int)EditorVCSInterface::VCSMetadata::GIT);
957 default_files_container->add_child(vcs_metadata_selection);
958 Control *spacer = memnew(Control);
959 spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
960 default_files_container->add_child(spacer);
961
962 fdialog = memnew(EditorFileDialog);
963 fdialog->set_previews_enabled(false); //Crucial, otherwise the engine crashes.
964 fdialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
965 fdialog_install = memnew(EditorFileDialog);
966 fdialog_install->set_previews_enabled(false); //Crucial, otherwise the engine crashes.
967 fdialog_install->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
968 add_child(fdialog);
969 add_child(fdialog_install);
970
971 project_name->connect("text_changed", callable_mp(this, &ProjectDialog::_text_changed));
972 project_path->connect("text_changed", callable_mp(this, &ProjectDialog::_path_text_changed));
973 install_path->connect("text_changed", callable_mp(this, &ProjectDialog::_path_text_changed));
974 fdialog->connect("dir_selected", callable_mp(this, &ProjectDialog::_path_selected));
975 fdialog->connect("file_selected", callable_mp(this, &ProjectDialog::_file_selected));
976 fdialog_install->connect("dir_selected", callable_mp(this, &ProjectDialog::_install_path_selected));
977 fdialog_install->connect("file_selected", callable_mp(this, &ProjectDialog::_install_path_selected));
978
979 set_hide_on_ok(false);
980
981 dialog_error = memnew(AcceptDialog);
982 add_child(dialog_error);
983}
984
985/// Project List and friends.
986
987void ProjectListItemControl::_notification(int p_what) {
988 switch (p_what) {
989 case NOTIFICATION_THEME_CHANGED: {
990 if (icon_needs_reload) {
991 // The project icon may not be loaded by the time the control is displayed,
992 // so use a loading placeholder.
993 project_icon->set_texture(get_editor_theme_icon(SNAME("ProjectIconLoading")));
994 }
995
996 project_title->add_theme_font_override("font", get_theme_font(SNAME("title"), EditorStringName(EditorFonts)));
997 project_title->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("title_size"), EditorStringName(EditorFonts)));
998 project_title->add_theme_color_override("font_color", get_theme_color(SNAME("font_color"), SNAME("Tree")));
999 project_path->add_theme_color_override("font_color", get_theme_color(SNAME("font_color"), SNAME("Tree")));
1000 project_unsupported_features->set_texture(get_editor_theme_icon(SNAME("NodeWarning")));
1001
1002 favorite_button->set_texture_normal(get_editor_theme_icon(SNAME("Favorites")));
1003 if (project_is_missing) {
1004 explore_button->set_icon(get_editor_theme_icon(SNAME("FileBroken")));
1005 } else {
1006 explore_button->set_icon(get_editor_theme_icon(SNAME("Load")));
1007 }
1008 } break;
1009
1010 case NOTIFICATION_MOUSE_ENTER: {
1011 is_hovering = true;
1012 queue_redraw();
1013 } break;
1014
1015 case NOTIFICATION_MOUSE_EXIT: {
1016 is_hovering = false;
1017 queue_redraw();
1018 } break;
1019
1020 case NOTIFICATION_DRAW: {
1021 if (is_selected) {
1022 draw_style_box(get_theme_stylebox(SNAME("selected"), SNAME("Tree")), Rect2(Point2(), get_size()));
1023 }
1024 if (is_hovering) {
1025 draw_style_box(get_theme_stylebox(SNAME("hover"), SNAME("Tree")), Rect2(Point2(), get_size()));
1026 }
1027
1028 draw_line(Point2(0, get_size().y + 1), Point2(get_size().x, get_size().y + 1), get_theme_color(SNAME("guide_color"), SNAME("Tree")));
1029 } break;
1030 }
1031}
1032
1033void ProjectListItemControl::set_project_title(const String &p_title) {
1034 project_title->set_text(p_title);
1035}
1036
1037void ProjectListItemControl::set_project_path(const String &p_path) {
1038 project_path->set_text(p_path);
1039}
1040
1041void ProjectListItemControl::set_tags(const PackedStringArray &p_tags, ProjectList *p_parent_list) {
1042 for (const String &tag : p_tags) {
1043 ProjectTag *tag_control = memnew(ProjectTag(tag));
1044 tag_container->add_child(tag_control);
1045 tag_control->connect_button_to(callable_mp(p_parent_list, &ProjectList::add_search_tag).bind(tag));
1046 }
1047}
1048
1049void ProjectListItemControl::set_project_icon(const Ref<Texture2D> &p_icon) {
1050 icon_needs_reload = false;
1051
1052 // The default project icon is 128×128 to look crisp on hiDPI displays,
1053 // but we want the actual displayed size to be 64×64 on loDPI displays.
1054 project_icon->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
1055 project_icon->set_custom_minimum_size(Size2(64, 64) * EDSCALE);
1056 project_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
1057
1058 project_icon->set_texture(p_icon);
1059}
1060
1061bool _project_feature_looks_like_version(const String &p_feature) {
1062 return p_feature.contains(".") && p_feature.substr(0, 3).is_numeric();
1063}
1064
1065void ProjectListItemControl::set_unsupported_features(PackedStringArray p_features) {
1066 if (p_features.size() > 0) {
1067 String tooltip_text = "";
1068 for (int i = 0; i < p_features.size(); i++) {
1069 if (_project_feature_looks_like_version(p_features[i])) {
1070 tooltip_text += TTR("This project was last edited in a different Godot version: ") + p_features[i] + "\n";
1071 p_features.remove_at(i);
1072 i--;
1073 }
1074 }
1075 if (p_features.size() > 0) {
1076 String unsupported_features_str = String(", ").join(p_features);
1077 tooltip_text += TTR("This project uses features unsupported by the current build:") + "\n" + unsupported_features_str;
1078 }
1079 project_unsupported_features->set_tooltip_text(tooltip_text);
1080 project_unsupported_features->show();
1081 } else {
1082 project_unsupported_features->hide();
1083 }
1084}
1085
1086bool ProjectListItemControl::should_load_project_icon() const {
1087 return icon_needs_reload;
1088}
1089
1090void ProjectListItemControl::set_selected(bool p_selected) {
1091 is_selected = p_selected;
1092 queue_redraw();
1093}
1094
1095void ProjectListItemControl::set_is_favorite(bool p_favorite) {
1096 favorite_button->set_modulate(p_favorite ? Color(1, 1, 1, 1) : Color(1, 1, 1, 0.2));
1097}
1098
1099void ProjectListItemControl::set_is_missing(bool p_missing) {
1100 if (project_is_missing == p_missing) {
1101 return;
1102 }
1103 project_is_missing = p_missing;
1104
1105 if (project_is_missing) {
1106 project_icon->set_modulate(Color(1, 1, 1, 0.5));
1107
1108 explore_button->set_icon(get_editor_theme_icon(SNAME("FileBroken")));
1109 explore_button->set_tooltip_text(TTR("Error: Project is missing on the filesystem."));
1110 } else {
1111 project_icon->set_modulate(Color(1, 1, 1, 1.0));
1112
1113 explore_button->set_icon(get_editor_theme_icon(SNAME("Load")));
1114#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
1115 explore_button->set_tooltip_text(TTR("Show in File Manager"));
1116#else
1117 // Opening the system file manager is not supported on the Android and web editors.
1118 explore_button->hide();
1119#endif
1120 }
1121}
1122
1123void ProjectListItemControl::set_is_grayed(bool p_grayed) {
1124 if (p_grayed) {
1125 main_vbox->set_modulate(Color(1, 1, 1, 0.5));
1126 // Don't make the icon less prominent if the parent is already grayed out.
1127 explore_button->set_modulate(Color(1, 1, 1, 1.0));
1128 } else {
1129 main_vbox->set_modulate(Color(1, 1, 1, 1.0));
1130 explore_button->set_modulate(Color(1, 1, 1, 0.5));
1131 }
1132}
1133
1134void ProjectListItemControl::_favorite_button_pressed() {
1135 emit_signal(SNAME("favorite_pressed"));
1136}
1137
1138void ProjectListItemControl::_explore_button_pressed() {
1139 emit_signal(SNAME("explore_pressed"));
1140}
1141
1142void ProjectListItemControl::_bind_methods() {
1143 ADD_SIGNAL(MethodInfo("favorite_pressed"));
1144 ADD_SIGNAL(MethodInfo("explore_pressed"));
1145}
1146
1147ProjectListItemControl::ProjectListItemControl() {
1148 set_focus_mode(FocusMode::FOCUS_ALL);
1149
1150 VBoxContainer *favorite_box = memnew(VBoxContainer);
1151 favorite_box->set_alignment(BoxContainer::ALIGNMENT_CENTER);
1152 add_child(favorite_box);
1153
1154 favorite_button = memnew(TextureButton);
1155 favorite_button->set_name("FavoriteButton");
1156 // This makes the project's "hover" style display correctly when hovering the favorite icon.
1157 favorite_button->set_mouse_filter(MOUSE_FILTER_PASS);
1158 favorite_box->add_child(favorite_button);
1159 favorite_button->connect("pressed", callable_mp(this, &ProjectListItemControl::_favorite_button_pressed));
1160
1161 project_icon = memnew(TextureRect);
1162 project_icon->set_name("ProjectIcon");
1163 project_icon->set_v_size_flags(SIZE_SHRINK_CENTER);
1164 add_child(project_icon);
1165
1166 main_vbox = memnew(VBoxContainer);
1167 main_vbox->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1168 add_child(main_vbox);
1169
1170 Control *ec = memnew(Control);
1171 ec->set_custom_minimum_size(Size2(0, 1));
1172 ec->set_mouse_filter(MOUSE_FILTER_PASS);
1173 main_vbox->add_child(ec);
1174
1175 // Top half, title, tags and unsupported features labels.
1176 {
1177 HBoxContainer *title_hb = memnew(HBoxContainer);
1178 main_vbox->add_child(title_hb);
1179
1180 project_title = memnew(Label);
1181 project_title->set_auto_translate(false);
1182 project_title->set_name("ProjectName");
1183 project_title->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1184 project_title->set_clip_text(true);
1185 title_hb->add_child(project_title);
1186
1187 tag_container = memnew(HBoxContainer);
1188 title_hb->add_child(tag_container);
1189
1190 Control *spacer = memnew(Control);
1191 spacer->set_custom_minimum_size(Size2(10, 10));
1192 title_hb->add_child(spacer);
1193 }
1194
1195 // Bottom half, containing the path and view folder button.
1196 {
1197 HBoxContainer *path_hb = memnew(HBoxContainer);
1198 path_hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1199 main_vbox->add_child(path_hb);
1200
1201 explore_button = memnew(Button);
1202 explore_button->set_name("ExploreButton");
1203 explore_button->set_flat(true);
1204 path_hb->add_child(explore_button);
1205 explore_button->connect("pressed", callable_mp(this, &ProjectListItemControl::_explore_button_pressed));
1206
1207 project_path = memnew(Label);
1208 project_path->set_name("ProjectPath");
1209 project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
1210 project_path->set_clip_text(true);
1211 project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1212 project_path->set_modulate(Color(1, 1, 1, 0.5));
1213 path_hb->add_child(project_path);
1214
1215 project_unsupported_features = memnew(TextureRect);
1216 project_unsupported_features->set_name("ProjectUnsupportedFeatures");
1217 project_unsupported_features->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
1218 path_hb->add_child(project_unsupported_features);
1219 project_unsupported_features->hide();
1220
1221 Control *spacer = memnew(Control);
1222 spacer->set_custom_minimum_size(Size2(10, 10));
1223 path_hb->add_child(spacer);
1224 }
1225}
1226
1227struct ProjectListComparator {
1228 ProjectList::FilterOption order_option = ProjectList::FilterOption::EDIT_DATE;
1229
1230 // operator<
1231 _FORCE_INLINE_ bool operator()(const ProjectList::Item &a, const ProjectList::Item &b) const {
1232 if (a.favorite && !b.favorite) {
1233 return true;
1234 }
1235 if (b.favorite && !a.favorite) {
1236 return false;
1237 }
1238 switch (order_option) {
1239 case ProjectList::PATH:
1240 return a.path < b.path;
1241 case ProjectList::EDIT_DATE:
1242 return a.last_edited > b.last_edited;
1243 case ProjectList::TAGS:
1244 return a.tag_sort_string < b.tag_sort_string;
1245 default:
1246 return a.project_name < b.project_name;
1247 }
1248 }
1249};
1250
1251const char *ProjectList::SIGNAL_SELECTION_CHANGED = "selection_changed";
1252const char *ProjectList::SIGNAL_PROJECT_ASK_OPEN = "project_ask_open";
1253
1254void ProjectList::_notification(int p_what) {
1255 switch (p_what) {
1256 case NOTIFICATION_PROCESS: {
1257 // Load icons as a coroutine to speed up launch when you have hundreds of projects
1258 if (_icon_load_index < _projects.size()) {
1259 Item &item = _projects.write[_icon_load_index];
1260 if (item.control->should_load_project_icon()) {
1261 _load_project_icon(_icon_load_index);
1262 }
1263 _icon_load_index++;
1264
1265 } else {
1266 set_process(false);
1267 }
1268 } break;
1269 }
1270}
1271
1272void ProjectList::_update_icons_async() {
1273 _icon_load_index = 0;
1274 set_process(true);
1275}
1276
1277void ProjectList::_load_project_icon(int p_index) {
1278 Item &item = _projects.write[p_index];
1279
1280 Ref<Texture2D> default_icon = get_editor_theme_icon(SNAME("DefaultProjectIcon"));
1281 Ref<Texture2D> icon;
1282 if (!item.icon.is_empty()) {
1283 Ref<Image> img;
1284 img.instantiate();
1285 Error err = img->load(item.icon.replace_first("res://", item.path + "/"));
1286 if (err == OK) {
1287 img->resize(default_icon->get_width(), default_icon->get_height(), Image::INTERPOLATE_LANCZOS);
1288 icon = ImageTexture::create_from_image(img);
1289 }
1290 }
1291 if (icon.is_null()) {
1292 icon = default_icon;
1293 }
1294
1295 item.control->set_project_icon(icon);
1296}
1297
1298// Load project data from p_property_key and return it in a ProjectList::Item.
1299// p_favorite is passed directly into the Item.
1300ProjectList::Item ProjectList::load_project_data(const String &p_path, bool p_favorite) {
1301 String conf = p_path.path_join("project.godot");
1302 bool grayed = false;
1303 bool missing = false;
1304
1305 Ref<ConfigFile> cf = memnew(ConfigFile);
1306 Error cf_err = cf->load(conf);
1307
1308 int config_version = 0;
1309 String project_name = TTR("Unnamed Project");
1310 if (cf_err == OK) {
1311 String cf_project_name = cf->get_value("application", "config/name", "");
1312 if (!cf_project_name.is_empty()) {
1313 project_name = cf_project_name.xml_unescape();
1314 }
1315 config_version = (int)cf->get_value("", "config_version", 0);
1316 }
1317
1318 if (config_version > ProjectSettings::CONFIG_VERSION) {
1319 // Comes from an incompatible (more recent) Godot version, gray it out.
1320 grayed = true;
1321 }
1322
1323 const String description = cf->get_value("application", "config/description", "");
1324 const PackedStringArray tags = cf->get_value("application", "config/tags", PackedStringArray());
1325 const String icon = cf->get_value("application", "config/icon", "");
1326 const String main_scene = cf->get_value("application", "run/main_scene", "");
1327
1328 PackedStringArray project_features = cf->get_value("application", "config/features", PackedStringArray());
1329 PackedStringArray unsupported_features = ProjectSettings::get_unsupported_features(project_features);
1330
1331 uint64_t last_edited = 0;
1332 if (cf_err == OK) {
1333 // The modification date marks the date the project was last edited.
1334 // This is because the `project.godot` file will always be modified
1335 // when editing a project (but not when running it).
1336 last_edited = FileAccess::get_modified_time(conf);
1337
1338 String fscache = p_path.path_join(".fscache");
1339 if (FileAccess::exists(fscache)) {
1340 uint64_t cache_modified = FileAccess::get_modified_time(fscache);
1341 if (cache_modified > last_edited) {
1342 last_edited = cache_modified;
1343 }
1344 }
1345 } else {
1346 grayed = true;
1347 missing = true;
1348 print_line("Project is missing: " + conf);
1349 }
1350
1351 for (const String &tag : tags) {
1352 ProjectManager::get_singleton()->add_new_tag(tag);
1353 }
1354
1355 return Item(project_name, description, tags, p_path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, config_version);
1356}
1357
1358void ProjectList::migrate_config() {
1359 // Proposal #1637 moved the project list from editor settings to a separate config file
1360 // If the new config file doesn't exist, populate it from EditorSettings
1361 if (FileAccess::exists(_config_path)) {
1362 return;
1363 }
1364
1365 List<PropertyInfo> properties;
1366 EditorSettings::get_singleton()->get_property_list(&properties);
1367
1368 for (const PropertyInfo &E : properties) {
1369 // This is actually something like "projects/C:::Documents::Godot::Projects::MyGame"
1370 String property_key = E.name;
1371 if (!property_key.begins_with("projects/")) {
1372 continue;
1373 }
1374
1375 String path = EDITOR_GET(property_key);
1376 print_line("Migrating legacy project '" + path + "'.");
1377
1378 String favoriteKey = "favorite_projects/" + property_key.get_slice("/", 1);
1379 bool favorite = EditorSettings::get_singleton()->has_setting(favoriteKey);
1380 add_project(path, favorite);
1381 if (favorite) {
1382 EditorSettings::get_singleton()->erase(favoriteKey);
1383 }
1384 EditorSettings::get_singleton()->erase(property_key);
1385 }
1386
1387 save_config();
1388}
1389
1390void ProjectList::load_projects() {
1391 // This is a full, hard reload of the list. Don't call this unless really required, it's expensive.
1392 // If you have 150 projects, it may read through 150 files on your disk at once + load 150 icons.
1393
1394 // Clear whole list
1395 for (int i = 0; i < _projects.size(); ++i) {
1396 Item &project = _projects.write[i];
1397 CRASH_COND(project.control == nullptr);
1398 memdelete(project.control); // Why not queue_free()?
1399 }
1400 _projects.clear();
1401 _last_clicked = "";
1402 _selected_project_paths.clear();
1403
1404 List<String> sections;
1405 _config.load(_config_path);
1406 _config.get_sections(&sections);
1407
1408 for (const String &path : sections) {
1409 bool favorite = _config.get_value(path, "favorite", false);
1410 _projects.push_back(load_project_data(path, favorite));
1411 }
1412
1413 // Create controls
1414 for (int i = 0; i < _projects.size(); ++i) {
1415 _create_project_item_control(i);
1416 }
1417
1418 sort_projects();
1419 _update_icons_async();
1420 update_dock_menu();
1421
1422 set_v_scroll(0);
1423}
1424
1425void ProjectList::update_dock_menu() {
1426 if (!DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_GLOBAL_MENU)) {
1427 return;
1428 }
1429 DisplayServer::get_singleton()->global_menu_clear("_dock");
1430
1431 int favs_added = 0;
1432 int total_added = 0;
1433 for (int i = 0; i < _projects.size(); ++i) {
1434 if (!_projects[i].grayed && !_projects[i].missing) {
1435 if (_projects[i].favorite) {
1436 favs_added++;
1437 } else {
1438 if (favs_added != 0) {
1439 DisplayServer::get_singleton()->global_menu_add_separator("_dock");
1440 }
1441 favs_added = 0;
1442 }
1443 DisplayServer::get_singleton()->global_menu_add_item("_dock", _projects[i].project_name + " ( " + _projects[i].path + " )", callable_mp(this, &ProjectList::_global_menu_open_project), Callable(), i);
1444 total_added++;
1445 }
1446 }
1447 if (total_added != 0) {
1448 DisplayServer::get_singleton()->global_menu_add_separator("_dock");
1449 }
1450 DisplayServer::get_singleton()->global_menu_add_item("_dock", TTR("New Window"), callable_mp(this, &ProjectList::_global_menu_new_window));
1451}
1452
1453void ProjectList::_global_menu_new_window(const Variant &p_tag) {
1454 List<String> args;
1455 args.push_back("-p");
1456 OS::get_singleton()->create_instance(args);
1457}
1458
1459void ProjectList::_global_menu_open_project(const Variant &p_tag) {
1460 int idx = (int)p_tag;
1461
1462 if (idx >= 0 && idx < _projects.size()) {
1463 String conf = _projects[idx].path.path_join("project.godot");
1464 List<String> args;
1465 args.push_back(conf);
1466 OS::get_singleton()->create_instance(args);
1467 }
1468}
1469
1470void ProjectList::_create_project_item_control(int p_index) {
1471 // Will be added last in the list, so make sure indexes match
1472 ERR_FAIL_COND(p_index != _scroll_children->get_child_count());
1473
1474 Item &item = _projects.write[p_index];
1475 ERR_FAIL_COND(item.control != nullptr); // Already created
1476
1477 ProjectListItemControl *hb = memnew(ProjectListItemControl);
1478 hb->add_theme_constant_override("separation", 10 * EDSCALE);
1479
1480 hb->set_project_title(!item.missing ? item.project_name : TTR("Missing Project"));
1481 hb->set_project_path(item.path);
1482 hb->set_tooltip_text(item.description);
1483 hb->set_tags(item.tags, this);
1484 hb->set_unsupported_features(item.unsupported_features.duplicate());
1485
1486 hb->set_is_favorite(item.favorite);
1487 hb->set_is_missing(item.missing);
1488 hb->set_is_grayed(item.grayed);
1489
1490 hb->connect("gui_input", callable_mp(this, &ProjectList::_panel_input).bind(hb));
1491 hb->connect("favorite_pressed", callable_mp(this, &ProjectList::_favorite_pressed).bind(hb));
1492
1493#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
1494 hb->connect("explore_pressed", callable_mp(this, &ProjectList::_show_project).bind(item.path));
1495#endif
1496
1497 _scroll_children->add_child(hb);
1498 item.control = hb;
1499}
1500
1501void ProjectList::set_search_term(String p_search_term) {
1502 _search_term = p_search_term;
1503}
1504
1505void ProjectList::set_order_option(int p_option) {
1506 FilterOption selected = (FilterOption)p_option;
1507 EditorSettings::get_singleton()->set("project_manager/sorting_order", p_option);
1508 EditorSettings::get_singleton()->save();
1509 _order_option = selected;
1510
1511 sort_projects();
1512}
1513
1514void ProjectList::sort_projects() {
1515 SortArray<Item, ProjectListComparator> sorter;
1516 sorter.compare.order_option = _order_option;
1517 sorter.sort(_projects.ptrw(), _projects.size());
1518
1519 String search_term;
1520 PackedStringArray tags;
1521
1522 if (!_search_term.is_empty()) {
1523 PackedStringArray search_parts = _search_term.split(" ");
1524 if (search_parts.size() > 1 || search_parts[0].begins_with("tag:")) {
1525 PackedStringArray remaining;
1526 for (const String &part : search_parts) {
1527 if (part.begins_with("tag:")) {
1528 tags.push_back(part.get_slice(":", 1));
1529 } else {
1530 remaining.append(part);
1531 }
1532 }
1533 search_term = String(" ").join(remaining); // Search term without tags.
1534 } else {
1535 search_term = _search_term;
1536 }
1537 }
1538
1539 for (int i = 0; i < _projects.size(); ++i) {
1540 Item &item = _projects.write[i];
1541
1542 bool item_visible = true;
1543 if (!_search_term.is_empty()) {
1544 String search_path;
1545 if (search_term.contains("/")) {
1546 // Search path will match the whole path
1547 search_path = item.path;
1548 } else {
1549 // Search path will only match the last path component to make searching more strict
1550 search_path = item.path.get_file();
1551 }
1552
1553 bool missing_tags = false;
1554 for (const String &tag : tags) {
1555 if (!item.tags.has(tag)) {
1556 missing_tags = true;
1557 break;
1558 }
1559 }
1560
1561 // When searching, display projects whose name or path contain the search term and whose tags match the searched tags.
1562 item_visible = !missing_tags && (search_term.is_empty() || item.project_name.findn(search_term) != -1 || search_path.findn(search_term) != -1);
1563 }
1564
1565 item.control->set_visible(item_visible);
1566 }
1567
1568 for (int i = 0; i < _projects.size(); ++i) {
1569 Item &item = _projects.write[i];
1570 item.control->get_parent()->move_child(item.control, i);
1571 }
1572
1573 // Rewind the coroutine because order of projects changed
1574 _update_icons_async();
1575 update_dock_menu();
1576}
1577
1578const HashSet<String> &ProjectList::get_selected_project_keys() const {
1579 // Faster if that's all you need
1580 return _selected_project_paths;
1581}
1582
1583Vector<ProjectList::Item> ProjectList::get_selected_projects() const {
1584 Vector<Item> items;
1585 if (_selected_project_paths.size() == 0) {
1586 return items;
1587 }
1588 items.resize(_selected_project_paths.size());
1589 int j = 0;
1590 for (int i = 0; i < _projects.size(); ++i) {
1591 const Item &item = _projects[i];
1592 if (_selected_project_paths.has(item.path)) {
1593 items.write[j++] = item;
1594 }
1595 }
1596 ERR_FAIL_COND_V(j != items.size(), items);
1597 return items;
1598}
1599
1600void ProjectList::ensure_project_visible(int p_index) {
1601 const Item &item = _projects[p_index];
1602 ensure_control_visible(item.control);
1603}
1604
1605int ProjectList::get_single_selected_index() const {
1606 if (_selected_project_paths.size() == 0) {
1607 // Default selection
1608 return 0;
1609 }
1610 String key;
1611 if (_selected_project_paths.size() == 1) {
1612 // Only one selected
1613 key = *_selected_project_paths.begin();
1614 } else {
1615 // Multiple selected, consider the last clicked one as "main"
1616 key = _last_clicked;
1617 }
1618 for (int i = 0; i < _projects.size(); ++i) {
1619 if (_projects[i].path == key) {
1620 return i;
1621 }
1622 }
1623 return 0;
1624}
1625
1626void ProjectList::_remove_project(int p_index, bool p_update_config) {
1627 const Item item = _projects[p_index]; // Take a copy
1628
1629 _selected_project_paths.erase(item.path);
1630
1631 if (_last_clicked == item.path) {
1632 _last_clicked = "";
1633 }
1634
1635 memdelete(item.control);
1636 _projects.remove_at(p_index);
1637
1638 if (p_update_config) {
1639 _config.erase_section(item.path);
1640 // Not actually saving the file, in case you are doing more changes to settings
1641 }
1642
1643 update_dock_menu();
1644}
1645
1646bool ProjectList::is_any_project_missing() const {
1647 for (int i = 0; i < _projects.size(); ++i) {
1648 if (_projects[i].missing) {
1649 return true;
1650 }
1651 }
1652 return false;
1653}
1654
1655void ProjectList::erase_missing_projects() {
1656 if (_projects.is_empty()) {
1657 return;
1658 }
1659
1660 int deleted_count = 0;
1661 int remaining_count = 0;
1662
1663 for (int i = 0; i < _projects.size(); ++i) {
1664 const Item &item = _projects[i];
1665
1666 if (item.missing) {
1667 _remove_project(i, true);
1668 --i;
1669 ++deleted_count;
1670
1671 } else {
1672 ++remaining_count;
1673 }
1674 }
1675
1676 print_line("Removed " + itos(deleted_count) + " projects from the list, remaining " + itos(remaining_count) + " projects");
1677 save_config();
1678}
1679
1680int ProjectList::refresh_project(const String &dir_path) {
1681 // Reloads information about a specific project.
1682 // If it wasn't loaded and should be in the list, it is added (i.e new project).
1683 // If it isn't in the list anymore, it is removed.
1684 // If it is in the list but doesn't exist anymore, it is marked as missing.
1685
1686 bool should_be_in_list = _config.has_section(dir_path);
1687 bool is_favourite = _config.get_value(dir_path, "favorite", false);
1688
1689 bool was_selected = _selected_project_paths.has(dir_path);
1690
1691 // Remove item in any case
1692 for (int i = 0; i < _projects.size(); ++i) {
1693 const Item &existing_item = _projects[i];
1694 if (existing_item.path == dir_path) {
1695 _remove_project(i, false);
1696 break;
1697 }
1698 }
1699
1700 int index = -1;
1701 if (should_be_in_list) {
1702 // Recreate it with updated info
1703
1704 Item item = load_project_data(dir_path, is_favourite);
1705
1706 _projects.push_back(item);
1707 _create_project_item_control(_projects.size() - 1);
1708
1709 sort_projects();
1710
1711 for (int i = 0; i < _projects.size(); ++i) {
1712 if (_projects[i].path == dir_path) {
1713 if (was_selected) {
1714 select_project(i);
1715 ensure_project_visible(i);
1716 }
1717 _load_project_icon(i);
1718
1719 index = i;
1720 break;
1721 }
1722 }
1723 }
1724
1725 return index;
1726}
1727
1728void ProjectList::add_project(const String &dir_path, bool favorite) {
1729 if (!_config.has_section(dir_path)) {
1730 _config.set_value(dir_path, "favorite", favorite);
1731 }
1732}
1733
1734void ProjectList::save_config() {
1735 _config.save(_config_path);
1736}
1737
1738void ProjectList::set_project_version(const String &p_project_path, int p_version) {
1739 for (ProjectList::Item &E : _projects) {
1740 if (E.path == p_project_path) {
1741 E.version = p_version;
1742 break;
1743 }
1744 }
1745}
1746
1747int ProjectList::get_project_count() const {
1748 return _projects.size();
1749}
1750
1751void ProjectList::_clear_project_selection() {
1752 Vector<Item> previous_selected_items = get_selected_projects();
1753 _selected_project_paths.clear();
1754
1755 for (int i = 0; i < previous_selected_items.size(); ++i) {
1756 previous_selected_items[i].control->set_selected(false);
1757 }
1758}
1759
1760void ProjectList::_toggle_project(int p_index) {
1761 // This methods adds to the selection or removes from the
1762 // selection.
1763 Item &item = _projects.write[p_index];
1764
1765 if (_selected_project_paths.has(item.path)) {
1766 _deselect_project_nocheck(p_index);
1767 } else {
1768 _select_project_nocheck(p_index);
1769 }
1770}
1771
1772void ProjectList::_select_project_nocheck(int p_index) {
1773 Item &item = _projects.write[p_index];
1774 _selected_project_paths.insert(item.path);
1775 item.control->set_selected(true);
1776}
1777
1778void ProjectList::_deselect_project_nocheck(int p_index) {
1779 Item &item = _projects.write[p_index];
1780 _selected_project_paths.erase(item.path);
1781 item.control->set_selected(false);
1782}
1783
1784void ProjectList::select_project(int p_index) {
1785 // This method keeps only one project selected.
1786 _clear_project_selection();
1787 _select_project_nocheck(p_index);
1788}
1789
1790void ProjectList::select_first_visible_project() {
1791 _clear_project_selection();
1792
1793 for (int i = 0; i < _projects.size(); i++) {
1794 if (_projects[i].control->is_visible()) {
1795 _select_project_nocheck(i);
1796 break;
1797 }
1798 }
1799}
1800
1801inline void _sort_project_range(int &a, int &b) {
1802 if (a > b) {
1803 int temp = a;
1804 a = b;
1805 b = temp;
1806 }
1807}
1808
1809void ProjectList::_select_project_range(int p_begin, int p_end) {
1810 _clear_project_selection();
1811
1812 _sort_project_range(p_begin, p_end);
1813 for (int i = p_begin; i <= p_end; ++i) {
1814 _select_project_nocheck(i);
1815 }
1816}
1817
1818void ProjectList::erase_selected_projects(bool p_delete_project_contents) {
1819 if (_selected_project_paths.size() == 0) {
1820 return;
1821 }
1822
1823 for (int i = 0; i < _projects.size(); ++i) {
1824 Item &item = _projects.write[i];
1825 if (_selected_project_paths.has(item.path) && item.control->is_visible()) {
1826 _config.erase_section(item.path);
1827
1828 // Comment out for now until we have a better warning system to
1829 // ensure users delete their project only.
1830 //if (p_delete_project_contents) {
1831 // OS::get_singleton()->move_to_trash(item.path);
1832 //}
1833
1834 memdelete(item.control);
1835 _projects.remove_at(i);
1836 --i;
1837 }
1838 }
1839
1840 save_config();
1841 _selected_project_paths.clear();
1842 _last_clicked = "";
1843
1844 update_dock_menu();
1845}
1846
1847// Input for each item in the list.
1848void ProjectList::_panel_input(const Ref<InputEvent> &p_ev, Node *p_hb) {
1849 Ref<InputEventMouseButton> mb = p_ev;
1850 int clicked_index = p_hb->get_index();
1851 const Item &clicked_project = _projects[clicked_index];
1852
1853 if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
1854 if (mb->is_shift_pressed() && _selected_project_paths.size() > 0 && !_last_clicked.is_empty() && clicked_project.path != _last_clicked) {
1855 int anchor_index = -1;
1856 for (int i = 0; i < _projects.size(); ++i) {
1857 const Item &p = _projects[i];
1858 if (p.path == _last_clicked) {
1859 anchor_index = p.control->get_index();
1860 break;
1861 }
1862 }
1863 CRASH_COND(anchor_index == -1);
1864 _select_project_range(anchor_index, clicked_index);
1865
1866 } else if (mb->is_ctrl_pressed()) {
1867 _toggle_project(clicked_index);
1868
1869 } else {
1870 _last_clicked = clicked_project.path;
1871 select_project(clicked_index);
1872 }
1873
1874 emit_signal(SNAME(SIGNAL_SELECTION_CHANGED));
1875
1876 // Do not allow opening a project more than once using a single project manager instance.
1877 // Opening the same project in several editor instances at once can lead to various issues.
1878 if (!mb->is_ctrl_pressed() && mb->is_double_click() && !project_opening_initiated) {
1879 emit_signal(SNAME(SIGNAL_PROJECT_ASK_OPEN));
1880 }
1881 }
1882}
1883
1884void ProjectList::_favorite_pressed(Node *p_hb) {
1885 ProjectListItemControl *control = Object::cast_to<ProjectListItemControl>(p_hb);
1886
1887 int index = control->get_index();
1888 Item item = _projects.write[index]; // Take copy
1889
1890 item.favorite = !item.favorite;
1891
1892 _config.set_value(item.path, "favorite", item.favorite);
1893 save_config();
1894
1895 _projects.write[index] = item;
1896
1897 control->set_is_favorite(item.favorite);
1898
1899 sort_projects();
1900
1901 if (item.favorite) {
1902 for (int i = 0; i < _projects.size(); ++i) {
1903 if (_projects[i].path == item.path) {
1904 ensure_project_visible(i);
1905 break;
1906 }
1907 }
1908 }
1909
1910 update_dock_menu();
1911}
1912
1913void ProjectList::_show_project(const String &p_path) {
1914 OS::get_singleton()->shell_show_in_file_manager(p_path, true);
1915}
1916
1917void ProjectList::_bind_methods() {
1918 ADD_SIGNAL(MethodInfo(SIGNAL_SELECTION_CHANGED));
1919 ADD_SIGNAL(MethodInfo(SIGNAL_PROJECT_ASK_OPEN));
1920}
1921
1922ProjectList::ProjectList() {
1923 _scroll_children = memnew(VBoxContainer);
1924 _scroll_children->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1925 add_child(_scroll_children);
1926
1927 _config_path = EditorPaths::get_singleton()->get_data_dir().path_join("projects.cfg");
1928}
1929
1930/// Project Manager.
1931
1932ProjectManager *ProjectManager::singleton = nullptr;
1933
1934void ProjectManager::_notification(int p_what) {
1935 switch (p_what) {
1936 case NOTIFICATION_TRANSLATION_CHANGED:
1937 case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: {
1938 settings_hb->set_anchors_and_offsets_preset(Control::PRESET_TOP_RIGHT);
1939 queue_redraw();
1940 } break;
1941
1942 case NOTIFICATION_ENTER_TREE: {
1943 Engine::get_singleton()->set_editor_hint(false);
1944 } break;
1945
1946 case NOTIFICATION_THEME_CHANGED: {
1947 background_panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("Background"), EditorStringName(EditorStyles)));
1948 loading_label->add_theme_font_override("font", get_theme_font(SNAME("bold"), EditorStringName(EditorFonts)));
1949 search_panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("search_panel"), SNAME("ProjectManager")));
1950
1951 // Top bar.
1952 search_box->set_right_icon(get_editor_theme_icon(SNAME("Search")));
1953 language_btn->set_icon(get_editor_theme_icon(SNAME("Environment")));
1954
1955 // Sidebar.
1956 create_btn->set_icon(get_editor_theme_icon(SNAME("Add")));
1957 import_btn->set_icon(get_editor_theme_icon(SNAME("Load")));
1958 scan_btn->set_icon(get_editor_theme_icon(SNAME("Search")));
1959 open_btn->set_icon(get_editor_theme_icon(SNAME("Edit")));
1960 run_btn->set_icon(get_editor_theme_icon(SNAME("Play")));
1961 rename_btn->set_icon(get_editor_theme_icon(SNAME("Rename")));
1962 manage_tags_btn->set_icon(get_editor_theme_icon("Script"));
1963 erase_btn->set_icon(get_editor_theme_icon(SNAME("Remove")));
1964 erase_missing_btn->set_icon(get_editor_theme_icon(SNAME("Clear")));
1965 create_tag_btn->set_icon(get_editor_theme_icon("Add"));
1966
1967 tag_error->add_theme_color_override("font_color", get_theme_color("error_color", EditorStringName(Editor)));
1968 tag_edit_error->add_theme_color_override("font_color", get_theme_color("error_color", EditorStringName(Editor)));
1969
1970 create_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
1971 import_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
1972 scan_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
1973 open_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
1974 run_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
1975 rename_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
1976 manage_tags_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
1977 erase_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
1978 erase_missing_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager")));
1979
1980 // Asset library popup.
1981 if (asset_library) {
1982 // Removes extra border margins.
1983 asset_library->add_theme_style_override("panel", memnew(StyleBoxEmpty));
1984 }
1985 } break;
1986
1987 case NOTIFICATION_RESIZED: {
1988 if (open_templates && open_templates->is_visible()) {
1989 open_templates->popup_centered();
1990 }
1991 if (asset_library) {
1992 real_t size = get_size().x / EDSCALE;
1993 // Adjust names of tabs to fit the new size.
1994 if (size < 650) {
1995 local_projects_vb->set_name(TTR("Local"));
1996 asset_library->set_name(TTR("Asset Library"));
1997 } else {
1998 local_projects_vb->set_name(TTR("Local Projects"));
1999 asset_library->set_name(TTR("Asset Library Projects"));
2000 }
2001 }
2002 } break;
2003
2004 case NOTIFICATION_READY: {
2005 int default_sorting = (int)EDITOR_GET("project_manager/sorting_order");
2006 filter_option->select(default_sorting);
2007 _project_list->set_order_option(default_sorting);
2008
2009#ifndef ANDROID_ENABLED
2010 if (_project_list->get_project_count() >= 1) {
2011 // Focus on the search box immediately to allow the user
2012 // to search without having to reach for their mouse
2013 search_box->grab_focus();
2014 }
2015#endif
2016
2017 // Suggest browsing asset library to get templates/demos.
2018 if (asset_library && open_templates && _project_list->get_project_count() == 0) {
2019 open_templates->popup_centered();
2020 }
2021 } break;
2022
2023 case NOTIFICATION_VISIBILITY_CHANGED: {
2024 set_process_shortcut_input(is_visible_in_tree());
2025 } break;
2026
2027 case NOTIFICATION_WM_CLOSE_REQUEST: {
2028 _dim_window();
2029 } break;
2030
2031 case NOTIFICATION_WM_ABOUT: {
2032 _show_about();
2033 } break;
2034 }
2035}
2036
2037Ref<Texture2D> ProjectManager::_file_dialog_get_icon(const String &p_path) {
2038 if (p_path.get_extension().to_lower() == "godot") {
2039 return singleton->icon_type_cache["GodotMonochrome"];
2040 }
2041
2042 return singleton->icon_type_cache["Object"];
2043}
2044
2045Ref<Texture2D> ProjectManager::_file_dialog_get_thumbnail(const String &p_path) {
2046 if (p_path.get_extension().to_lower() == "godot") {
2047 return singleton->icon_type_cache["GodotFile"];
2048 }
2049
2050 return Ref<Texture2D>();
2051}
2052
2053void ProjectManager::_build_icon_type_cache(Ref<Theme> p_theme) {
2054 if (p_theme.is_null()) {
2055 return;
2056 }
2057 List<StringName> tl;
2058 p_theme->get_icon_list(EditorStringName(EditorIcons), &tl);
2059 for (List<StringName>::Element *E = tl.front(); E; E = E->next()) {
2060 icon_type_cache[E->get()] = p_theme->get_icon(E->get(), EditorStringName(EditorIcons));
2061 }
2062}
2063
2064void ProjectManager::_dim_window() {
2065 // This method must be called before calling `get_tree()->quit()`.
2066 // Otherwise, its effect won't be visible
2067
2068 // Dim the project manager window while it's quitting to make it clearer that it's busy.
2069 // No transition is applied, as the effect needs to be visible immediately
2070 float c = 0.5f;
2071 Color dim_color = Color(c, c, c);
2072 set_modulate(dim_color);
2073}
2074
2075void ProjectManager::_update_project_buttons() {
2076 Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects();
2077 bool empty_selection = selected_projects.is_empty();
2078
2079 bool is_missing_project_selected = false;
2080 for (int i = 0; i < selected_projects.size(); ++i) {
2081 if (selected_projects[i].missing) {
2082 is_missing_project_selected = true;
2083 break;
2084 }
2085 }
2086
2087 erase_btn->set_disabled(empty_selection);
2088 open_btn->set_disabled(empty_selection || is_missing_project_selected);
2089 rename_btn->set_disabled(empty_selection || is_missing_project_selected);
2090 manage_tags_btn->set_disabled(empty_selection || is_missing_project_selected || selected_projects.size() > 1);
2091 run_btn->set_disabled(empty_selection || is_missing_project_selected);
2092
2093 erase_missing_btn->set_disabled(!_project_list->is_any_project_missing());
2094}
2095
2096void ProjectManager::shortcut_input(const Ref<InputEvent> &p_ev) {
2097 ERR_FAIL_COND(p_ev.is_null());
2098
2099 Ref<InputEventKey> k = p_ev;
2100
2101 if (k.is_valid()) {
2102 if (!k->is_pressed()) {
2103 return;
2104 }
2105
2106 // Pressing Command + Q quits the Project Manager
2107 // This is handled by the platform implementation on macOS,
2108 // so only define the shortcut on other platforms
2109#ifndef MACOS_ENABLED
2110 if (k->get_keycode_with_modifiers() == (KeyModifierMask::META | Key::Q)) {
2111 _dim_window();
2112 get_tree()->quit();
2113 }
2114#endif
2115
2116 if (tabs->get_current_tab() != 0) {
2117 return;
2118 }
2119
2120 bool keycode_handled = true;
2121
2122 switch (k->get_keycode()) {
2123 case Key::ENTER: {
2124 _open_selected_projects_ask();
2125 } break;
2126 case Key::HOME: {
2127 if (_project_list->get_project_count() > 0) {
2128 _project_list->select_project(0);
2129 _update_project_buttons();
2130 }
2131
2132 } break;
2133 case Key::END: {
2134 if (_project_list->get_project_count() > 0) {
2135 _project_list->select_project(_project_list->get_project_count() - 1);
2136 _update_project_buttons();
2137 }
2138
2139 } break;
2140 case Key::UP: {
2141 if (k->is_shift_pressed()) {
2142 break;
2143 }
2144
2145 int index = _project_list->get_single_selected_index();
2146 if (index > 0) {
2147 _project_list->select_project(index - 1);
2148 _project_list->ensure_project_visible(index - 1);
2149 _update_project_buttons();
2150 }
2151
2152 break;
2153 }
2154 case Key::DOWN: {
2155 if (k->is_shift_pressed()) {
2156 break;
2157 }
2158
2159 int index = _project_list->get_single_selected_index();
2160 if (index + 1 < _project_list->get_project_count()) {
2161 _project_list->select_project(index + 1);
2162 _project_list->ensure_project_visible(index + 1);
2163 _update_project_buttons();
2164 }
2165
2166 } break;
2167 case Key::F: {
2168 if (k->is_command_or_control_pressed()) {
2169 this->search_box->grab_focus();
2170 } else {
2171 keycode_handled = false;
2172 }
2173 } break;
2174 default: {
2175 keycode_handled = false;
2176 } break;
2177 }
2178
2179 if (keycode_handled) {
2180 accept_event();
2181 }
2182 }
2183}
2184
2185void ProjectManager::_load_recent_projects() {
2186 _project_list->set_search_term(search_box->get_text().strip_edges());
2187 _project_list->load_projects();
2188
2189 _update_project_buttons();
2190
2191 tabs->set_current_tab(0);
2192}
2193
2194void ProjectManager::_on_projects_updated() {
2195 Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects();
2196 int index = 0;
2197 for (int i = 0; i < selected_projects.size(); ++i) {
2198 index = _project_list->refresh_project(selected_projects[i].path);
2199 }
2200 if (index != -1) {
2201 _project_list->ensure_project_visible(index);
2202 }
2203
2204 _project_list->update_dock_menu();
2205}
2206
2207void ProjectManager::_on_project_created(const String &dir) {
2208 _project_list->add_project(dir, false);
2209 _project_list->save_config();
2210 search_box->clear();
2211 int i = _project_list->refresh_project(dir);
2212 _project_list->select_project(i);
2213 _project_list->ensure_project_visible(i);
2214 _open_selected_projects_ask();
2215
2216 _project_list->update_dock_menu();
2217}
2218
2219void ProjectManager::_confirm_update_settings() {
2220 _open_selected_projects();
2221}
2222
2223void ProjectManager::_open_selected_projects() {
2224 // Show loading text to tell the user that the project manager is busy loading.
2225 // This is especially important for the Web project manager.
2226 loading_label->show();
2227
2228 const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
2229
2230 for (const String &path : selected_list) {
2231 String conf = path.path_join("project.godot");
2232
2233 if (!FileAccess::exists(conf)) {
2234 dialog_error->set_text(vformat(TTR("Can't open project at '%s'."), path));
2235 dialog_error->popup_centered();
2236 return;
2237 }
2238
2239 print_line("Editing project: " + path);
2240
2241 List<String> args;
2242
2243 for (const String &a : Main::get_forwardable_cli_arguments(Main::CLI_SCOPE_TOOL)) {
2244 args.push_back(a);
2245 }
2246
2247 args.push_back("--path");
2248 args.push_back(path);
2249
2250 args.push_back("--editor");
2251
2252 Error err = OS::get_singleton()->create_instance(args);
2253 ERR_FAIL_COND(err);
2254 }
2255
2256 _project_list->project_opening_initiated = true;
2257
2258 _dim_window();
2259 get_tree()->quit();
2260}
2261
2262void ProjectManager::_open_selected_projects_ask() {
2263 const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
2264
2265 if (selected_list.size() < 1) {
2266 return;
2267 }
2268
2269 const Size2i popup_min_size = Size2i(600.0 * EDSCALE, 0);
2270
2271 if (selected_list.size() > 1) {
2272 multi_open_ask->set_text(vformat(TTR("You requested to open %d projects in parallel. Do you confirm?\nNote that usual checks for engine version compatibility will be bypassed."), selected_list.size()));
2273 multi_open_ask->popup_centered(popup_min_size);
2274 return;
2275 }
2276
2277 ProjectList::Item project = _project_list->get_selected_projects()[0];
2278 if (project.missing) {
2279 return;
2280 }
2281
2282 // Update the project settings or don't open.
2283 const int config_version = project.version;
2284 PackedStringArray unsupported_features = project.unsupported_features;
2285
2286 Label *ask_update_label = ask_update_settings->get_label();
2287 ask_update_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_LEFT); // Reset in case of previous center align.
2288 full_convert_button->hide();
2289
2290 ask_update_settings->get_ok_button()->set_text("OK");
2291
2292 // Check if the config_version property was empty or 0.
2293 if (config_version == 0) {
2294 ask_update_settings->set_text(vformat(TTR("The selected project \"%s\" does not specify its supported Godot version in its configuration file (\"project.godot\").\n\nProject path: %s\n\nIf you proceed with opening it, it will be converted to Godot's current configuration file format.\n\nWarning: You won't be able to open the project with previous versions of the engine anymore."), project.project_name, project.path));
2295 ask_update_settings->popup_centered(popup_min_size);
2296 return;
2297 }
2298 // Check if we need to convert project settings from an earlier engine version.
2299 if (config_version < ProjectSettings::CONFIG_VERSION) {
2300 if (config_version == GODOT4_CONFIG_VERSION - 1 && ProjectSettings::CONFIG_VERSION == GODOT4_CONFIG_VERSION) { // Conversion from Godot 3 to 4.
2301 full_convert_button->show();
2302 ask_update_settings->set_text(vformat(TTR("The selected project \"%s\" was generated by Godot 3.x, and needs to be converted for Godot 4.x.\n\nProject path: %s\n\nYou have three options:\n- Convert only the configuration file (\"project.godot\"). Use this to open the project without attempting to convert its scenes, resources and scripts.\n- Convert the entire project including its scenes, resources and scripts (recommended if you are upgrading).\n- Do nothing and go back.\n\nWarning: If you select a conversion option, you won't be able to open the project with previous versions of the engine anymore."), project.project_name, project.path));
2303 ask_update_settings->get_ok_button()->set_text(TTR("Convert project.godot Only"));
2304 } else {
2305 ask_update_settings->set_text(vformat(TTR("The selected project \"%s\" was generated by an older engine version, and needs to be converted for this version.\n\nProject path: %s\n\nDo you want to convert it?\n\nWarning: You won't be able to open the project with previous versions of the engine anymore."), project.project_name, project.path));
2306 ask_update_settings->get_ok_button()->set_text(TTR("Convert project.godot"));
2307 }
2308 ask_update_settings->popup_centered(popup_min_size);
2309 ask_update_settings->get_cancel_button()->grab_focus(); // To prevent accidents.
2310 return;
2311 }
2312 // Check if the file was generated by a newer, incompatible engine version.
2313 if (config_version > ProjectSettings::CONFIG_VERSION) {
2314 dialog_error->set_text(vformat(TTR("Can't open project \"%s\" at the following path:\n\n%s\n\nThe project settings were created by a newer engine version, whose settings are not compatible with this version."), project.project_name, project.path));
2315 dialog_error->popup_centered(popup_min_size);
2316 return;
2317 }
2318 // Check if the project is using features not supported by this build of Godot.
2319 if (!unsupported_features.is_empty()) {
2320 String warning_message = "";
2321 for (int i = 0; i < unsupported_features.size(); i++) {
2322 String feature = unsupported_features[i];
2323 if (feature == "Double Precision") {
2324 warning_message += TTR("Warning: This project uses double precision floats, but this version of\nGodot uses single precision floats. Opening this project may cause data loss.\n\n");
2325 unsupported_features.remove_at(i);
2326 i--;
2327 } else if (feature == "C#") {
2328 warning_message += TTR("Warning: This project uses C#, but this build of Godot does not have\nthe Mono module. If you proceed you will not be able to use any C# scripts.\n\n");
2329 unsupported_features.remove_at(i);
2330 i--;
2331 } else if (_project_feature_looks_like_version(feature)) {
2332 warning_message += vformat(TTR("Warning: This project was last edited in Godot %s. Opening will change it to Godot %s.\n\n"), Variant(feature), Variant(VERSION_BRANCH));
2333 unsupported_features.remove_at(i);
2334 i--;
2335 }
2336 }
2337 if (!unsupported_features.is_empty()) {
2338 String unsupported_features_str = String(", ").join(unsupported_features);
2339 warning_message += vformat(TTR("Warning: This project uses the following features not supported by this build of Godot:\n\n%s\n\n"), unsupported_features_str);
2340 }
2341 warning_message += TTR("Open anyway? Project will be modified.");
2342 ask_update_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
2343 ask_update_settings->set_text(warning_message);
2344 ask_update_settings->popup_centered(popup_min_size);
2345 return;
2346 }
2347
2348 // Open if the project is up-to-date.
2349 _open_selected_projects();
2350}
2351
2352void ProjectManager::_full_convert_button_pressed() {
2353 ask_update_settings->hide();
2354 ask_full_convert_dialog->popup_centered(Size2i(600.0 * EDSCALE, 0));
2355 ask_full_convert_dialog->get_cancel_button()->grab_focus();
2356}
2357
2358void ProjectManager::_perform_full_project_conversion() {
2359 Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects();
2360 if (selected_list.is_empty()) {
2361 return;
2362 }
2363
2364 const String &path = selected_list[0].path;
2365
2366 print_line("Converting project: " + path);
2367 List<String> args;
2368 args.push_back("--path");
2369 args.push_back(path);
2370 args.push_back("--convert-3to4");
2371 args.push_back("--rendering-driver");
2372 args.push_back(Main::get_rendering_driver_name());
2373
2374 Error err = OS::get_singleton()->create_instance(args);
2375 ERR_FAIL_COND(err);
2376
2377 _project_list->set_project_version(path, GODOT4_CONFIG_VERSION);
2378}
2379
2380void ProjectManager::_run_project_confirm() {
2381 Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects();
2382
2383 for (int i = 0; i < selected_list.size(); ++i) {
2384 const String &selected_main = selected_list[i].main_scene;
2385 if (selected_main.is_empty()) {
2386 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."));
2387 run_error_diag->popup_centered();
2388 continue;
2389 }
2390
2391 const String &path = selected_list[i].path;
2392
2393 // `.substr(6)` on `ProjectSettings::get_singleton()->get_imported_files_path()` strips away the leading "res://".
2394 if (!DirAccess::exists(path.path_join(ProjectSettings::get_singleton()->get_imported_files_path().substr(6)))) {
2395 run_error_diag->set_text(TTR("Can't run project: Assets need to be imported.\nPlease edit the project to trigger the initial import."));
2396 run_error_diag->popup_centered();
2397 continue;
2398 }
2399
2400 print_line("Running project: " + path);
2401
2402 List<String> args;
2403
2404 for (const String &a : Main::get_forwardable_cli_arguments(Main::CLI_SCOPE_PROJECT)) {
2405 args.push_back(a);
2406 }
2407
2408 args.push_back("--path");
2409 args.push_back(path);
2410
2411 Error err = OS::get_singleton()->create_instance(args);
2412 ERR_FAIL_COND(err);
2413 }
2414}
2415
2416void ProjectManager::_run_project() {
2417 const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
2418
2419 if (selected_list.size() < 1) {
2420 return;
2421 }
2422
2423 if (selected_list.size() > 1) {
2424 multi_run_ask->set_text(vformat(TTR("Are you sure to run %d projects at once?"), selected_list.size()));
2425 multi_run_ask->popup_centered();
2426 } else {
2427 _run_project_confirm();
2428 }
2429}
2430
2431void ProjectManager::_scan_dir(const String &path) {
2432 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
2433 Error error = da->change_dir(path);
2434 ERR_FAIL_COND_MSG(error != OK, "Could not scan directory at: " + path);
2435 da->list_dir_begin();
2436 String n = da->get_next();
2437 while (!n.is_empty()) {
2438 if (da->current_is_dir() && !n.begins_with(".")) {
2439 _scan_dir(da->get_current_dir().path_join(n));
2440 } else if (n == "project.godot") {
2441 _project_list->add_project(da->get_current_dir(), false);
2442 }
2443 n = da->get_next();
2444 }
2445 da->list_dir_end();
2446}
2447
2448void ProjectManager::_scan_begin(const String &p_base) {
2449 print_line("Scanning projects at: " + p_base);
2450 _scan_dir(p_base);
2451 _project_list->save_config();
2452 _load_recent_projects();
2453}
2454
2455void ProjectManager::_scan_projects() {
2456 scan_dir->popup_file_dialog();
2457}
2458
2459void ProjectManager::_new_project() {
2460 npdialog->set_mode(ProjectDialog::MODE_NEW);
2461 npdialog->show_dialog();
2462}
2463
2464void ProjectManager::_import_project() {
2465 npdialog->set_mode(ProjectDialog::MODE_IMPORT);
2466 npdialog->ask_for_path_and_show();
2467}
2468
2469void ProjectManager::_rename_project() {
2470 const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
2471
2472 if (selected_list.size() == 0) {
2473 return;
2474 }
2475
2476 for (const String &E : selected_list) {
2477 npdialog->set_project_path(E);
2478 npdialog->set_mode(ProjectDialog::MODE_RENAME);
2479 npdialog->show_dialog();
2480 }
2481}
2482
2483void ProjectManager::_manage_project_tags() {
2484 for (int i = 0; i < project_tags->get_child_count(); i++) {
2485 project_tags->get_child(i)->queue_free();
2486 }
2487
2488 const ProjectList::Item item = _project_list->get_selected_projects()[0];
2489 current_project_tags = item.tags;
2490 for (const String &tag : current_project_tags) {
2491 ProjectTag *tag_control = memnew(ProjectTag(tag, true));
2492 project_tags->add_child(tag_control);
2493 tag_control->connect_button_to(callable_mp(this, &ProjectManager::_delete_project_tag).bind(tag));
2494 }
2495
2496 tag_edit_error->hide();
2497 tag_manage_dialog->popup_centered(Vector2i(500, 0) * EDSCALE);
2498}
2499
2500void ProjectManager::_add_project_tag(const String &p_tag) {
2501 if (current_project_tags.has(p_tag)) {
2502 return;
2503 }
2504 current_project_tags.append(p_tag);
2505
2506 ProjectTag *tag_control = memnew(ProjectTag(p_tag, true));
2507 project_tags->add_child(tag_control);
2508 tag_control->connect_button_to(callable_mp(this, &ProjectManager::_delete_project_tag).bind(p_tag));
2509}
2510
2511void ProjectManager::_delete_project_tag(const String &p_tag) {
2512 current_project_tags.erase(p_tag);
2513 for (int i = 0; i < project_tags->get_child_count(); i++) {
2514 ProjectTag *tag_control = Object::cast_to<ProjectTag>(project_tags->get_child(i));
2515 if (tag_control && tag_control->get_tag() == p_tag) {
2516 memdelete(tag_control);
2517 break;
2518 }
2519 }
2520}
2521
2522void ProjectManager::_apply_project_tags() {
2523 PackedStringArray tags;
2524 for (int i = 0; i < project_tags->get_child_count(); i++) {
2525 ProjectTag *tag_control = Object::cast_to<ProjectTag>(project_tags->get_child(i));
2526 if (tag_control) {
2527 tags.append(tag_control->get_tag());
2528 }
2529 }
2530
2531 ConfigFile cfg;
2532 const String project_godot = _project_list->get_selected_projects()[0].path.path_join("project.godot");
2533 Error err = cfg.load(project_godot);
2534 if (err != OK) {
2535 tag_edit_error->set_text(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err));
2536 tag_edit_error->show();
2537 callable_mp((Window *)tag_manage_dialog, &Window::show).call_deferred(); // Make sure the dialog does not disappear.
2538 return;
2539 } else {
2540 tags.sort();
2541 cfg.set_value("application", "config/tags", tags);
2542 err = cfg.save(project_godot);
2543 if (err != OK) {
2544 tag_edit_error->set_text(vformat(TTR("Couldn't save project at '%s' (error %d)."), project_godot, err));
2545 tag_edit_error->show();
2546 callable_mp((Window *)tag_manage_dialog, &Window::show).call_deferred();
2547 return;
2548 }
2549 }
2550
2551 _on_projects_updated();
2552}
2553
2554void ProjectManager::_set_new_tag_name(const String p_name) {
2555 create_tag_dialog->get_ok_button()->set_disabled(true);
2556 if (p_name.is_empty()) {
2557 tag_error->set_text(TTR("Tag name can't be empty."));
2558 return;
2559 }
2560
2561 if (p_name.contains(" ")) {
2562 tag_error->set_text(TTR("Tag name can't contain spaces."));
2563 return;
2564 }
2565
2566 for (const String &c : forbidden_tag_characters) {
2567 if (p_name.contains(c)) {
2568 tag_error->set_text(vformat(TTR("These characters are not allowed in tags: %s."), String(" ").join(forbidden_tag_characters)));
2569 return;
2570 }
2571 }
2572
2573 if (p_name.to_lower() != p_name) {
2574 tag_error->set_text(TTR("Tag name must be lowercase."));
2575 return;
2576 }
2577
2578 tag_error->set_text("");
2579 create_tag_dialog->get_ok_button()->set_disabled(false);
2580}
2581
2582void ProjectManager::_create_new_tag() {
2583 if (!tag_error->get_text().is_empty()) {
2584 return;
2585 }
2586 create_tag_dialog->hide(); // When using text_submitted, need to hide manually.
2587 add_new_tag(new_tag_name->get_text());
2588 _add_project_tag(new_tag_name->get_text());
2589}
2590
2591void ProjectManager::_erase_project_confirm() {
2592 _project_list->erase_selected_projects(false);
2593 _update_project_buttons();
2594}
2595
2596void ProjectManager::_erase_missing_projects_confirm() {
2597 _project_list->erase_missing_projects();
2598 _update_project_buttons();
2599}
2600
2601void ProjectManager::_erase_project() {
2602 const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
2603
2604 if (selected_list.size() == 0) {
2605 return;
2606 }
2607
2608 String confirm_message;
2609 if (selected_list.size() >= 2) {
2610 confirm_message = vformat(TTR("Remove %d projects from the list?"), selected_list.size());
2611 } else {
2612 confirm_message = TTR("Remove this project from the list?");
2613 }
2614
2615 erase_ask_label->set_text(confirm_message);
2616 //delete_project_contents->set_pressed(false);
2617 erase_ask->popup_centered();
2618}
2619
2620void ProjectManager::_erase_missing_projects() {
2621 erase_missing_ask->set_text(TTR("Remove all missing projects from the list?\nThe project folders' contents won't be modified."));
2622 erase_missing_ask->popup_centered();
2623}
2624
2625void ProjectManager::_show_about() {
2626 about->popup_centered(Size2(780, 500) * EDSCALE);
2627}
2628
2629void ProjectManager::_language_selected(int p_id) {
2630 String lang = language_btn->get_item_metadata(p_id);
2631 EditorSettings::get_singleton()->set("interface/editor/editor_language", lang);
2632
2633 language_restart_ask->set_text(TTR("Language changed.\nThe interface will update after restarting the editor or project manager."));
2634 language_restart_ask->popup_centered();
2635}
2636
2637void ProjectManager::_restart_confirm() {
2638 List<String> args = OS::get_singleton()->get_cmdline_args();
2639 Error err = OS::get_singleton()->create_instance(args);
2640 ERR_FAIL_COND(err);
2641
2642 _dim_window();
2643 get_tree()->quit();
2644}
2645
2646void ProjectManager::_install_project(const String &p_zip_path, const String &p_title) {
2647 npdialog->set_mode(ProjectDialog::MODE_INSTALL);
2648 npdialog->set_zip_path(p_zip_path);
2649 npdialog->set_zip_title(p_title);
2650 npdialog->show_dialog();
2651}
2652
2653void ProjectManager::_files_dropped(PackedStringArray p_files) {
2654 if (p_files.size() == 1 && p_files[0].ends_with(".zip")) {
2655 const String file = p_files[0].get_file();
2656 _install_project(p_files[0], file.substr(0, file.length() - 4).capitalize());
2657 return;
2658 }
2659 HashSet<String> folders_set;
2660 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
2661 for (int i = 0; i < p_files.size(); i++) {
2662 String file = p_files[i];
2663 folders_set.insert(da->dir_exists(file) ? file : file.get_base_dir());
2664 }
2665 if (folders_set.size() > 0) {
2666 PackedStringArray folders;
2667 for (const String &E : folders_set) {
2668 folders.push_back(E);
2669 }
2670
2671 bool confirm = true;
2672 if (folders.size() == 1) {
2673 Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
2674 if (dir->change_dir(folders[0]) == OK) {
2675 dir->list_dir_begin();
2676 String file = dir->get_next();
2677 while (confirm && !file.is_empty()) {
2678 if (!dir->current_is_dir() && file.ends_with("project.godot")) {
2679 confirm = false;
2680 }
2681 file = dir->get_next();
2682 }
2683 dir->list_dir_end();
2684 }
2685 }
2686 if (confirm) {
2687 multi_scan_ask->get_ok_button()->disconnect("pressed", callable_mp(this, &ProjectManager::_scan_multiple_folders));
2688 multi_scan_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_scan_multiple_folders).bind(folders));
2689 multi_scan_ask->set_text(
2690 vformat(TTR("Are you sure to scan %s folders for existing Godot projects?\nThis could take a while."), folders.size()));
2691 multi_scan_ask->popup_centered();
2692 } else {
2693 _scan_multiple_folders(folders);
2694 }
2695 }
2696}
2697
2698void ProjectManager::_scan_multiple_folders(PackedStringArray p_files) {
2699 for (int i = 0; i < p_files.size(); i++) {
2700 _scan_begin(p_files.get(i));
2701 }
2702}
2703
2704void ProjectManager::_on_order_option_changed(int p_idx) {
2705 if (is_inside_tree()) {
2706 _project_list->set_order_option(p_idx);
2707 }
2708}
2709
2710void ProjectManager::_on_tab_changed(int p_tab) {
2711#ifndef ANDROID_ENABLED
2712 if (p_tab == 0) { // Projects
2713 // Automatically grab focus when the user moves from the Templates tab
2714 // back to the Projects tab.
2715 search_box->grab_focus();
2716 }
2717
2718 // The Templates tab's search field is focused on display in the asset
2719 // library editor plugin code.
2720#endif
2721}
2722
2723void ProjectManager::_on_search_term_changed(const String &p_term) {
2724 _project_list->set_search_term(p_term);
2725 _project_list->sort_projects();
2726
2727 // Select the first visible project in the list.
2728 // This makes it possible to open a project without ever touching the mouse,
2729 // as the search field is automatically focused on startup.
2730 _project_list->select_first_visible_project();
2731 _update_project_buttons();
2732}
2733
2734void ProjectManager::_bind_methods() {
2735 ClassDB::bind_method("_update_project_buttons", &ProjectManager::_update_project_buttons);
2736 ClassDB::bind_method("_version_button_pressed", &ProjectManager::_version_button_pressed);
2737}
2738
2739void ProjectManager::_open_asset_library() {
2740 asset_library->disable_community_support();
2741 tabs->set_current_tab(1);
2742}
2743
2744void ProjectManager::_version_button_pressed() {
2745 DisplayServer::get_singleton()->clipboard_set(version_btn->get_text());
2746}
2747
2748LineEdit *ProjectManager::get_search_box() {
2749 return search_box;
2750}
2751
2752void ProjectManager::add_new_tag(const String &p_tag) {
2753 if (!tag_set.has(p_tag)) {
2754 tag_set.insert(p_tag);
2755 ProjectTag *tag_control = memnew(ProjectTag(p_tag));
2756 all_tags->add_child(tag_control);
2757 all_tags->move_child(tag_control, -2);
2758 tag_control->connect_button_to(callable_mp(this, &ProjectManager::_add_project_tag).bind(p_tag));
2759 }
2760}
2761
2762void ProjectList::add_search_tag(const String &p_tag) {
2763 const String tag_string = "tag:" + p_tag;
2764
2765 int exists = _search_term.find(tag_string);
2766 if (exists > -1) {
2767 _search_term = _search_term.erase(exists, tag_string.length() + 1);
2768 } else if (_search_term.is_empty() || _search_term.ends_with(" ")) {
2769 _search_term += tag_string;
2770 } else {
2771 _search_term += " " + tag_string;
2772 }
2773 ProjectManager::get_singleton()->get_search_box()->set_text(_search_term);
2774
2775 sort_projects();
2776}
2777
2778ProjectManager::ProjectManager() {
2779 singleton = this;
2780
2781 // load settings
2782 if (!EditorSettings::get_singleton()) {
2783 EditorSettings::create();
2784 }
2785
2786 // Turn off some servers we aren't going to be using in the Project Manager.
2787 NavigationServer3D::get_singleton()->set_active(false);
2788 PhysicsServer3D::get_singleton()->set_active(false);
2789 PhysicsServer2D::get_singleton()->set_active(false);
2790
2791 EditorSettings::get_singleton()->set_optimize_save(false); //just write settings as they came
2792
2793 {
2794 int display_scale = EDITOR_GET("interface/editor/display_scale");
2795
2796 switch (display_scale) {
2797 case 0:
2798 // Try applying a suitable display scale automatically.
2799 EditorScale::set_scale(EditorSettings::get_singleton()->get_auto_display_scale());
2800 break;
2801 case 1:
2802 EditorScale::set_scale(0.75);
2803 break;
2804 case 2:
2805 EditorScale::set_scale(1.0);
2806 break;
2807 case 3:
2808 EditorScale::set_scale(1.25);
2809 break;
2810 case 4:
2811 EditorScale::set_scale(1.5);
2812 break;
2813 case 5:
2814 EditorScale::set_scale(1.75);
2815 break;
2816 case 6:
2817 EditorScale::set_scale(2.0);
2818 break;
2819 default:
2820 EditorScale::set_scale(EDITOR_GET("interface/editor/custom_display_scale"));
2821 break;
2822 }
2823 EditorFileDialog::get_icon_func = &ProjectManager::_file_dialog_get_icon;
2824 EditorFileDialog::get_thumbnail_func = &ProjectManager::_file_dialog_get_thumbnail;
2825 }
2826
2827 // TRANSLATORS: This refers to the application where users manage their Godot projects.
2828 DisplayServer::get_singleton()->window_set_title(VERSION_NAME + String(" - ") + TTR("Project Manager", "Application"));
2829
2830 EditorFileDialog::set_default_show_hidden_files(EDITOR_GET("filesystem/file_dialog/show_hidden_files"));
2831 EditorFileDialog::set_default_display_mode((EditorFileDialog::DisplayMode)EDITOR_GET("filesystem/file_dialog/display_mode").operator int());
2832
2833 int swap_cancel_ok = EDITOR_GET("interface/editor/accept_dialog_cancel_ok_buttons");
2834 if (swap_cancel_ok != 0) { // 0 is auto, set in register_scene based on DisplayServer.
2835 // Swap on means OK first.
2836 AcceptDialog::set_swap_cancel_ok(swap_cancel_ok == 2);
2837 }
2838
2839 EditorColorMap::create();
2840 Ref<Theme> theme = create_custom_theme();
2841 DisplayServer::set_early_window_clear_color_override(true, theme->get_color(SNAME("background"), EditorStringName(Editor)));
2842
2843 set_theme(theme);
2844 set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
2845
2846 background_panel = memnew(Panel);
2847 add_child(background_panel);
2848 background_panel->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
2849
2850 VBoxContainer *vb = memnew(VBoxContainer);
2851 background_panel->add_child(vb);
2852 vb->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT, Control::PRESET_MODE_MINSIZE, 8 * EDSCALE);
2853
2854 Control *center_box = memnew(Control);
2855 center_box->set_v_size_flags(Control::SIZE_EXPAND_FILL);
2856 vb->add_child(center_box);
2857
2858 tabs = memnew(TabContainer);
2859 center_box->add_child(tabs);
2860 tabs->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
2861 tabs->connect("tab_changed", callable_mp(this, &ProjectManager::_on_tab_changed));
2862
2863 local_projects_vb = memnew(VBoxContainer);
2864 local_projects_vb->set_name(TTR("Local Projects"));
2865 tabs->add_child(local_projects_vb);
2866
2867 {
2868 // A bar at top with buttons and options.
2869 HBoxContainer *hb = memnew(HBoxContainer);
2870 hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
2871 local_projects_vb->add_child(hb);
2872
2873 create_btn = memnew(Button);
2874 create_btn->set_text(TTR("New"));
2875 create_btn->set_shortcut(ED_SHORTCUT("project_manager/new_project", TTR("New Project"), KeyModifierMask::CMD_OR_CTRL | Key::N));
2876 create_btn->connect("pressed", callable_mp(this, &ProjectManager::_new_project));
2877 hb->add_child(create_btn);
2878
2879 import_btn = memnew(Button);
2880 import_btn->set_text(TTR("Import"));
2881 import_btn->set_shortcut(ED_SHORTCUT("project_manager/import_project", TTR("Import Project"), KeyModifierMask::CMD_OR_CTRL | Key::I));
2882 import_btn->connect("pressed", callable_mp(this, &ProjectManager::_import_project));
2883 hb->add_child(import_btn);
2884
2885 scan_btn = memnew(Button);
2886 scan_btn->set_text(TTR("Scan"));
2887 scan_btn->set_shortcut(ED_SHORTCUT("project_manager/scan_projects", TTR("Scan Projects"), KeyModifierMask::CMD_OR_CTRL | Key::S));
2888 scan_btn->connect("pressed", callable_mp(this, &ProjectManager::_scan_projects));
2889 hb->add_child(scan_btn);
2890
2891 loading_label = memnew(Label(TTR("Loading, please wait...")));
2892 loading_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
2893 hb->add_child(loading_label);
2894 // The loading label is shown later.
2895 loading_label->hide();
2896
2897 search_box = memnew(LineEdit);
2898 search_box->set_placeholder(TTR("Filter Projects"));
2899 search_box->set_tooltip_text(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."));
2900 search_box->set_clear_button_enabled(true);
2901 search_box->connect("text_changed", callable_mp(this, &ProjectManager::_on_search_term_changed));
2902 search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL);
2903 hb->add_child(search_box);
2904
2905 Label *sort_label = memnew(Label);
2906 sort_label->set_text(TTR("Sort:"));
2907 hb->add_child(sort_label);
2908
2909 filter_option = memnew(OptionButton);
2910 filter_option->set_clip_text(true);
2911 filter_option->set_h_size_flags(Control::SIZE_EXPAND_FILL);
2912 filter_option->set_stretch_ratio(0.3);
2913 filter_option->connect("item_selected", callable_mp(this, &ProjectManager::_on_order_option_changed));
2914 hb->add_child(filter_option);
2915
2916 Vector<String> sort_filter_titles;
2917 sort_filter_titles.push_back(TTR("Last Edited"));
2918 sort_filter_titles.push_back(TTR("Name"));
2919 sort_filter_titles.push_back(TTR("Path"));
2920 sort_filter_titles.push_back(TTR("Tags"));
2921
2922 for (int i = 0; i < sort_filter_titles.size(); i++) {
2923 filter_option->add_item(sort_filter_titles[i]);
2924 }
2925 }
2926
2927 {
2928 // A container for the project list and for the side bar with buttons.
2929 HBoxContainer *search_tree_hb = memnew(HBoxContainer);
2930 local_projects_vb->add_child(search_tree_hb);
2931 search_tree_hb->set_v_size_flags(Control::SIZE_EXPAND_FILL);
2932
2933 search_panel = memnew(PanelContainer);
2934 search_panel->set_h_size_flags(Control::SIZE_EXPAND_FILL);
2935 search_tree_hb->add_child(search_panel);
2936
2937 _project_list = memnew(ProjectList);
2938 _project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
2939 _project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask));
2940 _project_list->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
2941 search_panel->add_child(_project_list);
2942
2943 // The side bar with the edit, run, rename, etc. buttons.
2944 VBoxContainer *tree_vb = memnew(VBoxContainer);
2945 tree_vb->set_custom_minimum_size(Size2(120, 120));
2946 search_tree_hb->add_child(tree_vb);
2947
2948 tree_vb->add_child(memnew(HSeparator));
2949
2950 open_btn = memnew(Button);
2951 open_btn->set_text(TTR("Edit"));
2952 open_btn->set_shortcut(ED_SHORTCUT("project_manager/edit_project", TTR("Edit Project"), KeyModifierMask::CMD_OR_CTRL | Key::E));
2953 open_btn->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects_ask));
2954 tree_vb->add_child(open_btn);
2955
2956 run_btn = memnew(Button);
2957 run_btn->set_text(TTR("Run"));
2958 run_btn->set_shortcut(ED_SHORTCUT("project_manager/run_project", TTR("Run Project"), KeyModifierMask::CMD_OR_CTRL | Key::R));
2959 run_btn->connect("pressed", callable_mp(this, &ProjectManager::_run_project));
2960 tree_vb->add_child(run_btn);
2961
2962 rename_btn = memnew(Button);
2963 rename_btn->set_text(TTR("Rename"));
2964 // The F2 shortcut isn't overridden with Enter on macOS as Enter is already used to edit a project.
2965 rename_btn->set_shortcut(ED_SHORTCUT("project_manager/rename_project", TTR("Rename Project"), Key::F2));
2966 rename_btn->connect("pressed", callable_mp(this, &ProjectManager::_rename_project));
2967 tree_vb->add_child(rename_btn);
2968
2969 manage_tags_btn = memnew(Button);
2970 manage_tags_btn->set_text(TTR("Manage Tags"));
2971 tree_vb->add_child(manage_tags_btn);
2972
2973 erase_btn = memnew(Button);
2974 erase_btn->set_text(TTR("Remove"));
2975 erase_btn->set_shortcut(ED_SHORTCUT("project_manager/remove_project", TTR("Remove Project"), Key::KEY_DELETE));
2976 erase_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_project));
2977 tree_vb->add_child(erase_btn);
2978
2979 erase_missing_btn = memnew(Button);
2980 erase_missing_btn->set_text(TTR("Remove Missing"));
2981 erase_missing_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects));
2982 tree_vb->add_child(erase_missing_btn);
2983
2984 tree_vb->add_spacer();
2985
2986 about_btn = memnew(Button);
2987 about_btn->set_text(TTR("About"));
2988 about_btn->connect("pressed", callable_mp(this, &ProjectManager::_show_about));
2989 tree_vb->add_child(about_btn);
2990 }
2991
2992 {
2993 // Version info and language options
2994 settings_hb = memnew(HBoxContainer);
2995 settings_hb->set_alignment(BoxContainer::ALIGNMENT_END);
2996 settings_hb->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN);
2997 settings_hb->set_anchors_and_offsets_preset(Control::PRESET_TOP_RIGHT);
2998
2999 // A VBoxContainer that contains a dummy Control node to adjust the LinkButton's vertical position.
3000 VBoxContainer *spacer_vb = memnew(VBoxContainer);
3001 settings_hb->add_child(spacer_vb);
3002
3003 Control *v_spacer = memnew(Control);
3004 spacer_vb->add_child(v_spacer);
3005
3006 version_btn = memnew(LinkButton);
3007 String hash = String(VERSION_HASH);
3008 if (hash.length() != 0) {
3009 hash = " " + vformat("[%s]", hash.left(9));
3010 }
3011 version_btn->set_text("v" VERSION_FULL_BUILD + hash);
3012 // Fade the version label to be less prominent, but still readable.
3013 version_btn->set_self_modulate(Color(1, 1, 1, 0.6));
3014 version_btn->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER);
3015 version_btn->set_tooltip_text(TTR("Click to copy."));
3016 version_btn->connect("pressed", callable_mp(this, &ProjectManager::_version_button_pressed));
3017 spacer_vb->add_child(version_btn);
3018
3019 // Add a small horizontal spacer between the version and language buttons
3020 // to distinguish them.
3021 Control *h_spacer = memnew(Control);
3022 settings_hb->add_child(h_spacer);
3023
3024 language_btn = memnew(OptionButton);
3025 language_btn->set_focus_mode(Control::FOCUS_NONE);
3026 language_btn->set_fit_to_longest_item(false);
3027 language_btn->set_flat(true);
3028 language_btn->connect("item_selected", callable_mp(this, &ProjectManager::_language_selected));
3029#ifdef ANDROID_ENABLED
3030 // The language selection dropdown doesn't work on Android (as the setting isn't saved), see GH-60353.
3031 // Also, the dropdown it spawns is very tall and can't be scrolled without a hardware mouse.
3032 // Hiding the language selection dropdown also leaves more space for the version label to display.
3033 language_btn->hide();
3034#endif
3035
3036 Vector<String> editor_languages;
3037 List<PropertyInfo> editor_settings_properties;
3038 EditorSettings::get_singleton()->get_property_list(&editor_settings_properties);
3039 for (const PropertyInfo &pi : editor_settings_properties) {
3040 if (pi.name == "interface/editor/editor_language") {
3041 editor_languages = pi.hint_string.split(",");
3042 break;
3043 }
3044 }
3045
3046 String current_lang = EDITOR_GET("interface/editor/editor_language");
3047 language_btn->set_text(current_lang);
3048
3049 for (int i = 0; i < editor_languages.size(); i++) {
3050 String lang = editor_languages[i];
3051 String lang_name = TranslationServer::get_singleton()->get_locale_name(lang);
3052 language_btn->add_item(vformat("[%s] %s", lang, lang_name), i);
3053 language_btn->set_item_metadata(i, lang);
3054 if (current_lang == lang) {
3055 language_btn->select(i);
3056 }
3057 }
3058
3059 settings_hb->add_child(language_btn);
3060 center_box->add_child(settings_hb);
3061 }
3062
3063 if (AssetLibraryEditorPlugin::is_available()) {
3064 asset_library = memnew(EditorAssetLibrary(true));
3065 asset_library->set_name(TTR("Asset Library Projects"));
3066 tabs->add_child(asset_library);
3067 asset_library->connect("install_asset", callable_mp(this, &ProjectManager::_install_project));
3068 } else {
3069 print_verbose("Asset Library not available (due to using Web editor, or SSL support disabled).");
3070 }
3071
3072 {
3073 // Dialogs
3074 language_restart_ask = memnew(ConfirmationDialog);
3075 language_restart_ask->set_ok_button_text(TTR("Restart Now"));
3076 language_restart_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_restart_confirm));
3077 language_restart_ask->set_cancel_button_text(TTR("Continue"));
3078 add_child(language_restart_ask);
3079
3080 scan_dir = memnew(EditorFileDialog);
3081 scan_dir->set_previews_enabled(false);
3082 scan_dir->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
3083 scan_dir->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
3084 scan_dir->set_title(TTR("Select a Folder to Scan")); // must be after mode or it's overridden
3085 scan_dir->set_current_dir(EDITOR_GET("filesystem/directories/default_project_path"));
3086 add_child(scan_dir);
3087 scan_dir->connect("dir_selected", callable_mp(this, &ProjectManager::_scan_begin));
3088
3089 erase_missing_ask = memnew(ConfirmationDialog);
3090 erase_missing_ask->set_ok_button_text(TTR("Remove All"));
3091 erase_missing_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects_confirm));
3092 add_child(erase_missing_ask);
3093
3094 erase_ask = memnew(ConfirmationDialog);
3095 erase_ask->set_ok_button_text(TTR("Remove"));
3096 erase_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_erase_project_confirm));
3097 add_child(erase_ask);
3098
3099 VBoxContainer *erase_ask_vb = memnew(VBoxContainer);
3100 erase_ask->add_child(erase_ask_vb);
3101
3102 erase_ask_label = memnew(Label);
3103 erase_ask_vb->add_child(erase_ask_label);
3104
3105 // Comment out for now until we have a better warning system to
3106 // ensure users delete their project only.
3107 //delete_project_contents = memnew(CheckBox);
3108 //delete_project_contents->set_text(TTR("Also delete project contents (no undo!)"));
3109 //erase_ask_vb->add_child(delete_project_contents);
3110
3111 multi_open_ask = memnew(ConfirmationDialog);
3112 multi_open_ask->set_ok_button_text(TTR("Edit"));
3113 multi_open_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects));
3114 add_child(multi_open_ask);
3115
3116 multi_run_ask = memnew(ConfirmationDialog);
3117 multi_run_ask->set_ok_button_text(TTR("Run"));
3118 multi_run_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_run_project_confirm));
3119 add_child(multi_run_ask);
3120
3121 multi_scan_ask = memnew(ConfirmationDialog);
3122 multi_scan_ask->set_ok_button_text(TTR("Scan"));
3123 add_child(multi_scan_ask);
3124
3125 ask_update_settings = memnew(ConfirmationDialog);
3126 ask_update_settings->set_autowrap(true);
3127 ask_update_settings->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_confirm_update_settings));
3128 full_convert_button = ask_update_settings->add_button(TTR("Convert Full Project"), !GLOBAL_GET("gui/common/swap_cancel_ok"));
3129 full_convert_button->connect("pressed", callable_mp(this, &ProjectManager::_full_convert_button_pressed));
3130 add_child(ask_update_settings);
3131
3132 ask_full_convert_dialog = memnew(ConfirmationDialog);
3133 ask_full_convert_dialog->set_autowrap(true);
3134 ask_full_convert_dialog->set_text(TTR("This option will perform full project conversion, updating scenes, resources and scripts from Godot 3 to work in Godot 4.\n\nNote that this is a best-effort conversion, i.e. it makes upgrading the project easier, but it will not open out-of-the-box and will still require manual adjustments.\n\nIMPORTANT: Make sure to backup your project before converting, as this operation makes it impossible to open it in older versions of Godot."));
3135 ask_full_convert_dialog->connect("confirmed", callable_mp(this, &ProjectManager::_perform_full_project_conversion));
3136 add_child(ask_full_convert_dialog);
3137
3138 npdialog = memnew(ProjectDialog);
3139 npdialog->connect("projects_updated", callable_mp(this, &ProjectManager::_on_projects_updated));
3140 npdialog->connect("project_created", callable_mp(this, &ProjectManager::_on_project_created));
3141 add_child(npdialog);
3142
3143 run_error_diag = memnew(AcceptDialog);
3144 run_error_diag->set_title(TTR("Can't run project"));
3145 add_child(run_error_diag);
3146
3147 dialog_error = memnew(AcceptDialog);
3148 add_child(dialog_error);
3149
3150 if (asset_library) {
3151 open_templates = memnew(ConfirmationDialog);
3152 open_templates->set_text(TTR("You currently don't have any projects.\nWould you like to explore official example projects in the Asset Library?"));
3153 open_templates->set_ok_button_text(TTR("Open Asset Library"));
3154 open_templates->connect("confirmed", callable_mp(this, &ProjectManager::_open_asset_library));
3155 add_child(open_templates);
3156 }
3157
3158 about = memnew(EditorAbout);
3159 add_child(about);
3160
3161 _build_icon_type_cache(get_theme());
3162 }
3163
3164 {
3165 // Tag management.
3166 tag_manage_dialog = memnew(ConfirmationDialog);
3167 add_child(tag_manage_dialog);
3168 tag_manage_dialog->set_title(TTR("Manage Project Tags"));
3169 tag_manage_dialog->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_apply_project_tags));
3170 manage_tags_btn->connect("pressed", callable_mp(this, &ProjectManager::_manage_project_tags));
3171
3172 VBoxContainer *tag_vb = memnew(VBoxContainer);
3173 tag_manage_dialog->add_child(tag_vb);
3174
3175 Label *label = memnew(Label(TTR("Project Tags")));
3176 tag_vb->add_child(label);
3177 label->set_theme_type_variation("HeaderMedium");
3178 label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
3179
3180 label = memnew(Label(TTR("Click tag to remove it from the project.")));
3181 tag_vb->add_child(label);
3182 label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
3183
3184 project_tags = memnew(HFlowContainer);
3185 tag_vb->add_child(project_tags);
3186 project_tags->set_custom_minimum_size(Vector2(0, 100) * EDSCALE);
3187
3188 tag_vb->add_child(memnew(HSeparator));
3189
3190 label = memnew(Label(TTR("All Tags")));
3191 tag_vb->add_child(label);
3192 label->set_theme_type_variation("HeaderMedium");
3193 label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
3194
3195 label = memnew(Label(TTR("Click tag to add it to the project.")));
3196 tag_vb->add_child(label);
3197 label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
3198
3199 all_tags = memnew(HFlowContainer);
3200 tag_vb->add_child(all_tags);
3201 all_tags->set_custom_minimum_size(Vector2(0, 100) * EDSCALE);
3202
3203 tag_edit_error = memnew(Label);
3204 tag_vb->add_child(tag_edit_error);
3205 tag_edit_error->set_autowrap_mode(TextServer::AUTOWRAP_WORD);
3206
3207 create_tag_dialog = memnew(ConfirmationDialog);
3208 tag_manage_dialog->add_child(create_tag_dialog);
3209 create_tag_dialog->set_title(TTR("Create New Tag"));
3210 create_tag_dialog->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_create_new_tag));
3211
3212 tag_vb = memnew(VBoxContainer);
3213 create_tag_dialog->add_child(tag_vb);
3214
3215 Label *info = memnew(Label(TTR("Tags are capitalized automatically when displayed.")));
3216 tag_vb->add_child(info);
3217
3218 new_tag_name = memnew(LineEdit);
3219 tag_vb->add_child(new_tag_name);
3220 new_tag_name->connect("text_changed", callable_mp(this, &ProjectManager::_set_new_tag_name));
3221 new_tag_name->connect("text_submitted", callable_mp(this, &ProjectManager::_create_new_tag).unbind(1));
3222 create_tag_dialog->connect("about_to_popup", callable_mp(new_tag_name, &LineEdit::clear));
3223 create_tag_dialog->connect("about_to_popup", callable_mp((Control *)new_tag_name, &Control::grab_focus), CONNECT_DEFERRED);
3224
3225 tag_error = memnew(Label);
3226 tag_vb->add_child(tag_error);
3227
3228 create_tag_btn = memnew(Button);
3229 all_tags->add_child(create_tag_btn);
3230 create_tag_btn->connect("pressed", callable_mp((Window *)create_tag_dialog, &Window::popup_centered).bind(Vector2i(500, 0) * EDSCALE));
3231 }
3232
3233 _project_list->migrate_config();
3234 _load_recent_projects();
3235
3236 Ref<DirAccess> dir_access = DirAccess::create(DirAccess::AccessType::ACCESS_FILESYSTEM);
3237
3238 String default_project_path = EDITOR_GET("filesystem/directories/default_project_path");
3239 if (!dir_access->dir_exists(default_project_path)) {
3240 Error error = dir_access->make_dir_recursive(default_project_path);
3241 if (error != OK) {
3242 ERR_PRINT("Could not create default project directory at: " + default_project_path);
3243 }
3244 }
3245
3246 String autoscan_path = EDITOR_GET("filesystem/directories/autoscan_project_path");
3247 if (!autoscan_path.is_empty()) {
3248 if (dir_access->dir_exists(autoscan_path)) {
3249 _scan_begin(autoscan_path);
3250 } else {
3251 Error error = dir_access->make_dir_recursive(autoscan_path);
3252 if (error != OK) {
3253 ERR_PRINT("Could not create project autoscan directory at: " + autoscan_path);
3254 }
3255 }
3256 }
3257
3258 SceneTree::get_singleton()->get_root()->connect("files_dropped", callable_mp(this, &ProjectManager::_files_dropped));
3259
3260 // Define a minimum window size to prevent UI elements from overlapping or being cut off.
3261 Window *w = Object::cast_to<Window>(SceneTree::get_singleton()->get_root());
3262 if (w) {
3263 w->set_min_size(Size2(520, 350) * EDSCALE);
3264 }
3265
3266 // Resize the bootsplash window based on Editor display scale EDSCALE.
3267 float scale_factor = MAX(1, EDSCALE);
3268 if (scale_factor > 1.0) {
3269 Vector2i window_size = DisplayServer::get_singleton()->window_get_size();
3270 Rect2i screen_rect = DisplayServer::get_singleton()->screen_get_usable_rect(DisplayServer::get_singleton()->window_get_current_screen());
3271
3272 window_size *= scale_factor;
3273
3274 DisplayServer::get_singleton()->window_set_size(window_size);
3275 if (screen_rect.size != Vector2i()) {
3276 Vector2i window_position;
3277 window_position.x = screen_rect.position.x + (screen_rect.size.x - window_size.x) / 2;
3278 window_position.y = screen_rect.position.y + (screen_rect.size.y - window_size.y) / 2;
3279 DisplayServer::get_singleton()->window_set_position(window_position);
3280 }
3281 }
3282
3283 OS::get_singleton()->set_low_processor_usage_mode(true);
3284}
3285
3286ProjectManager::~ProjectManager() {
3287 singleton = nullptr;
3288 if (EditorSettings::get_singleton()) {
3289 EditorSettings::destroy();
3290 }
3291}
3292
3293void ProjectTag::_notification(int p_what) {
3294 if (display_close && p_what == NOTIFICATION_THEME_CHANGED) {
3295 button->set_icon(get_theme_icon(SNAME("close"), SNAME("TabBar")));
3296 }
3297}
3298
3299ProjectTag::ProjectTag(const String &p_text, bool p_display_close) {
3300 add_theme_constant_override(SNAME("separation"), 0);
3301 set_v_size_flags(SIZE_SHRINK_CENTER);
3302 tag_string = p_text;
3303 display_close = p_display_close;
3304
3305 Color tag_color = Color(1, 0, 0);
3306 tag_color.set_ok_hsl_s(0.8);
3307 tag_color.set_ok_hsl_h(float(p_text.hash() * 10001 % UINT32_MAX) / float(UINT32_MAX));
3308 set_self_modulate(tag_color);
3309
3310 ColorRect *cr = memnew(ColorRect);
3311 add_child(cr);
3312 cr->set_custom_minimum_size(Vector2(4, 0) * EDSCALE);
3313 cr->set_color(tag_color);
3314
3315 button = memnew(Button);
3316 add_child(button);
3317 button->set_auto_translate(false);
3318 button->set_text(p_text.capitalize());
3319 button->set_focus_mode(FOCUS_NONE);
3320 button->set_icon_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
3321 button->set_theme_type_variation(SNAME("ProjectTag"));
3322}
3323
3324void ProjectTag::connect_button_to(const Callable &p_callable) {
3325 button->connect(SNAME("pressed"), p_callable, CONNECT_DEFERRED);
3326}
3327
3328const String ProjectTag::get_tag() const {
3329 return tag_string;
3330}
3331