summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/object/class_db.cpp7
-rw-r--r--editor/SCsub1
-rw-r--r--editor/debugger/editor_performance_profiler.cpp4
-rw-r--r--editor/editor_audio_buses.cpp7
-rw-r--r--editor/editor_node.cpp2
-rw-r--r--editor/editor_settings.cpp8
-rw-r--r--editor/editor_settings.h2
-rw-r--r--editor/filesystem_dock.cpp5
-rw-r--r--editor/import/resource_importer_texture.cpp6
-rw-r--r--editor/plugins/bone_map_editor_plugin.cpp3
-rw-r--r--editor/plugins/canvas_item_editor_plugin.cpp3
-rw-r--r--editor/plugins/particle_process_material_editor_plugin.cpp3
-rw-r--r--editor/plugins/script_text_editor.cpp1
-rw-r--r--editor/project_manager.cpp2946
-rw-r--r--editor/project_manager.h422
-rw-r--r--editor/project_manager/SCsub5
-rw-r--r--editor/project_manager/project_dialog.cpp977
-rw-r--r--editor/project_manager/project_dialog.h136
-rw-r--r--editor/project_manager/project_list.cpp1074
-rw-r--r--editor/project_manager/project_list.h264
-rw-r--r--editor/project_manager/project_tag.cpp74
-rw-r--r--editor/project_manager/project_tag.h56
-rw-r--r--editor/themes/editor_fonts.cpp3
-rw-r--r--editor/themes/editor_icons.cpp23
-rw-r--r--editor/themes/editor_icons.h2
-rw-r--r--editor/themes/editor_theme.h13
-rw-r--r--editor/themes/editor_theme_manager.cpp212
-rw-r--r--editor/themes/editor_theme_manager.h36
-rw-r--r--methods.py31
-rw-r--r--modules/gdscript/doc_classes/@GDScript.xml10
-rw-r--r--modules/gdscript/editor/gdscript_highlighter.cpp3
-rw-r--r--modules/gdscript/gdscript.cpp122
-rw-r--r--modules/gdscript/gdscript.h6
-rw-r--r--modules/gdscript/gdscript_compiler.cpp3
-rw-r--r--modules/gdscript/gdscript_parser.cpp38
-rw-r--r--modules/gdscript/gdscript_parser.h4
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_duplicate.gd5
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_duplicate.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_invalid.gd4
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_invalid.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_too_late.gd5
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_too_late.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/features/uid.gd5
-rw-r--r--modules/gdscript/tests/scripts/parser/features/uid.out1
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd10
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/free_is_callable.out3
-rw-r--r--modules/gdscript/tests/test_gdscript_uid.h115
-rw-r--r--platform/linuxbsd/x11/display_server_x11.cpp22
-rw-r--r--platform/linuxbsd/x11/display_server_x11.h1
-rw-r--r--platform/macos/detect.py6
50 files changed, 3758 insertions, 2937 deletions
diff --git a/core/object/class_db.cpp b/core/object/class_db.cpp
index bf1bd0de93..45fbb19f88 100644
--- a/core/object/class_db.cpp
+++ b/core/object/class_db.cpp
@@ -31,6 +31,7 @@
#include "class_db.h"
#include "core/config/engine.h"
+#include "core/core_string_names.h"
#include "core/io/resource_loader.h"
#include "core/object/script_language.h"
#include "core/os/mutex.h"
@@ -1299,6 +1300,12 @@ bool ClassDB::get_property(Object *p_object, const StringName &p_property, Varia
check = check->inherits_ptr;
}
+ // The "free()" method is special, so we assume it exists and return a Callable.
+ if (p_property == CoreStringNames::get_singleton()->_free) {
+ r_value = Callable(p_object, p_property);
+ return true;
+ }
+
return false;
}
diff --git a/editor/SCsub b/editor/SCsub
index 5b36bca81a..aa240a1e01 100644
--- a/editor/SCsub
+++ b/editor/SCsub
@@ -113,6 +113,7 @@ if env.editor_build:
SConscript("icons/SCsub")
SConscript("import/SCsub")
SConscript("plugins/SCsub")
+ SConscript("project_manager/SCsub")
SConscript("themes/SCsub")
lib = env.add_library("editor", env.editor_sources)
diff --git a/editor/debugger/editor_performance_profiler.cpp b/editor/debugger/editor_performance_profiler.cpp
index af723cc731..37e13b59cc 100644
--- a/editor/debugger/editor_performance_profiler.cpp
+++ b/editor/debugger/editor_performance_profiler.cpp
@@ -31,9 +31,9 @@
#include "editor_performance_profiler.h"
#include "editor/editor_property_name_processor.h"
-#include "editor/editor_settings.h"
#include "editor/editor_string_names.h"
#include "editor/themes/editor_scale.h"
+#include "editor/themes/editor_theme_manager.h"
#include "main/performance.h"
EditorPerformanceProfiler::Monitor::Monitor() {}
@@ -122,7 +122,7 @@ void EditorPerformanceProfiler::_monitor_draw() {
}
Size2i cell_size = Size2i(monitor_draw->get_size()) / Size2i(columns, rows);
float spacing = float(POINT_SEPARATION) / float(columns);
- float value_multiplier = EditorSettings::get_singleton()->is_dark_theme() ? 1.4f : 0.55f;
+ float value_multiplier = EditorThemeManager::is_dark_theme() ? 1.4f : 0.55f;
float hue_shift = 1.0f / float(monitors.size());
for (int i = 0; i < active.size(); i++) {
diff --git a/editor/editor_audio_buses.cpp b/editor/editor_audio_buses.cpp
index 50845b4458..61a4b341b9 100644
--- a/editor/editor_audio_buses.cpp
+++ b/editor/editor_audio_buses.cpp
@@ -41,6 +41,7 @@
#include "editor/filesystem_dock.h"
#include "editor/gui/editor_file_dialog.h"
#include "editor/themes/editor_scale.h"
+#include "editor/themes/editor_theme_manager.h"
#include "scene/gui/separator.h"
#include "scene/resources/font.h"
#include "servers/audio_server.h"
@@ -84,9 +85,9 @@ void EditorAudioBus::_notification(int p_what) {
disabled_vu = get_editor_theme_icon(SNAME("BusVuFrozen"));
- Color solo_color = EditorSettings::get_singleton()->is_dark_theme() ? Color(1.0, 0.89, 0.22) : Color(1.0, 0.92, 0.44);
- Color mute_color = EditorSettings::get_singleton()->is_dark_theme() ? Color(1.0, 0.16, 0.16) : Color(1.0, 0.44, 0.44);
- Color bypass_color = EditorSettings::get_singleton()->is_dark_theme() ? Color(0.13, 0.8, 1.0) : Color(0.44, 0.87, 1.0);
+ Color solo_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.89, 0.22) : Color(1.0, 0.92, 0.44);
+ Color mute_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.16, 0.16) : Color(1.0, 0.44, 0.44);
+ Color bypass_color = EditorThemeManager::is_dark_theme() ? Color(0.13, 0.8, 1.0) : Color(0.44, 0.87, 1.0);
solo->set_icon(get_editor_theme_icon(SNAME("AudioBusSolo")));
solo->add_theme_color_override("icon_pressed_color", solo_color);
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index 521477d470..87d5531806 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -808,7 +808,7 @@ void EditorNode::_update_update_spinner() {
// as this feature should only be enabled for troubleshooting purposes.
// Make the icon modulate color overbright because icons are not completely white on a dark theme.
// On a light theme, icons are dark, so we need to modulate them with an even brighter color.
- const bool dark_theme = EditorSettings::get_singleton()->is_dark_theme();
+ const bool dark_theme = EditorThemeManager::is_dark_theme();
update_spinner->set_self_modulate(theme->get_color(SNAME("error_color"), EditorStringName(Editor)) * (dark_theme ? Color(1.1, 1.1, 1.1) : Color(4.25, 4.25, 4.25)));
} else {
update_spinner->set_tooltip_text(TTR("Spins when the editor window redraws."));
diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp
index b565431185..25510122f4 100644
--- a/editor/editor_settings.cpp
+++ b/editor/editor_settings.cpp
@@ -1278,14 +1278,6 @@ void EditorSettings::load_favorites_and_recent_dirs() {
}
}
-bool EditorSettings::is_dark_theme() {
- int AUTO_COLOR = 0;
- int LIGHT_COLOR = 2;
- Color base_color = get("interface/theme/base_color");
- int icon_font_color_setting = get("interface/theme/icon_and_font_color");
- return (icon_font_color_setting == AUTO_COLOR && base_color.get_luminance() < 0.5) || icon_font_color_setting == LIGHT_COLOR;
-}
-
void EditorSettings::list_text_editor_themes() {
String themes = "Default,Godot 2,Custom";
diff --git a/editor/editor_settings.h b/editor/editor_settings.h
index c3ce790e0e..2e280ac9d6 100644
--- a/editor/editor_settings.h
+++ b/editor/editor_settings.h
@@ -158,8 +158,6 @@ public:
Vector<String> get_recent_dirs() const;
void load_favorites_and_recent_dirs();
- bool is_dark_theme();
-
void list_text_editor_themes();
void load_text_editor_theme();
bool import_text_editor_theme(String p_file);
diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp
index 0aa9a3bfee..ecbfe4bec5 100644
--- a/editor/filesystem_dock.cpp
+++ b/editor/filesystem_dock.cpp
@@ -53,6 +53,7 @@
#include "editor/scene_tree_dock.h"
#include "editor/shader_create_dialog.h"
#include "editor/themes/editor_scale.h"
+#include "editor/themes/editor_theme_manager.h"
#include "scene/gui/item_list.h"
#include "scene/gui/label.h"
#include "scene/gui/line_edit.h"
@@ -625,7 +626,7 @@ void FileSystemDock::_notification(int p_what) {
// Update editor dark theme & always show folders states from editor settings, redraw if needed.
bool do_redraw = false;
- bool new_editor_is_dark_theme = EditorSettings::get_singleton()->is_dark_theme();
+ bool new_editor_is_dark_theme = EditorThemeManager::is_dark_theme();
if (new_editor_is_dark_theme != editor_is_dark_theme) {
editor_is_dark_theme = new_editor_is_dark_theme;
do_redraw = true;
@@ -3752,7 +3753,7 @@ FileSystemDock::FileSystemDock() {
assigned_folder_colors = ProjectSettings::get_singleton()->get_setting("file_customization/folder_colors");
- editor_is_dark_theme = EditorSettings::get_singleton()->is_dark_theme();
+ editor_is_dark_theme = EditorThemeManager::is_dark_theme();
VBoxContainer *top_vbc = memnew(VBoxContainer);
add_child(top_vbc);
diff --git a/editor/import/resource_importer_texture.cpp b/editor/import/resource_importer_texture.cpp
index cdfc85cf6f..8cf104725a 100644
--- a/editor/import/resource_importer_texture.cpp
+++ b/editor/import/resource_importer_texture.cpp
@@ -36,10 +36,10 @@
#include "core/version.h"
#include "editor/editor_file_system.h"
#include "editor/editor_node.h"
-#include "editor/editor_settings.h"
#include "editor/gui/editor_toaster.h"
#include "editor/import/resource_importer_texture_settings.h"
#include "editor/themes/editor_scale.h"
+#include "editor/themes/editor_theme_manager.h"
#include "scene/resources/compressed_texture.h"
void ResourceImporterTexture::_texture_reimport_roughness(const Ref<CompressedTexture2D> &p_tex, const String &p_normal_path, RS::TextureDetectRoughnessChannel p_channel) {
@@ -696,7 +696,7 @@ Error ResourceImporterTexture::import(const String &p_source_file, const String
editor_meta["editor_scale"] = EDSCALE;
}
if (convert_editor_colors) {
- editor_meta["editor_dark_theme"] = EditorSettings::get_singleton()->is_dark_theme();
+ editor_meta["editor_dark_theme"] = EditorThemeManager::is_dark_theme();
}
_save_editor_meta(editor_meta, p_save_path + ".editor.meta");
@@ -755,7 +755,7 @@ bool ResourceImporterTexture::are_import_settings_valid(const String &p_path) co
if (editor_meta.has("editor_scale") && (float)editor_meta["editor_scale"] != EDSCALE) {
return false;
}
- if (editor_meta.has("editor_dark_theme") && (bool)editor_meta["editor_dark_theme"] != EditorSettings::get_singleton()->is_dark_theme()) {
+ if (editor_meta.has("editor_dark_theme") && (bool)editor_meta["editor_dark_theme"] != EditorThemeManager::is_dark_theme()) {
return false;
}
}
diff --git a/editor/plugins/bone_map_editor_plugin.cpp b/editor/plugins/bone_map_editor_plugin.cpp
index 38573fbaa7..3256b90aba 100644
--- a/editor/plugins/bone_map_editor_plugin.cpp
+++ b/editor/plugins/bone_map_editor_plugin.cpp
@@ -36,6 +36,7 @@
#include "editor/import/3d/post_import_plugin_skeleton_track_organizer.h"
#include "editor/import/3d/scene_import_settings.h"
#include "editor/themes/editor_scale.h"
+#include "editor/themes/editor_theme_manager.h"
#include "scene/gui/aspect_ratio_container.h"
#include "scene/gui/separator.h"
#include "scene/gui/texture_rect.h"
@@ -52,7 +53,7 @@ void BoneMapperButton::fetch_textures() {
set_offset(SIDE_BOTTOM, 0);
// Hack to avoid handle color darkening...
- set_modulate(EditorSettings::get_singleton()->is_dark_theme() ? Color(1, 1, 1) : Color(4.25, 4.25, 4.25));
+ set_modulate(EditorThemeManager::is_dark_theme() ? Color(1, 1, 1) : Color(4.25, 4.25, 4.25));
circle = memnew(TextureRect);
circle->set_texture(get_editor_theme_icon(SNAME("BoneMapperHandleCircle")));
diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp
index 6c776ad9b3..3722d6beba 100644
--- a/editor/plugins/canvas_item_editor_plugin.cpp
+++ b/editor/plugins/canvas_item_editor_plugin.cpp
@@ -45,6 +45,7 @@
#include "editor/plugins/script_editor_plugin.h"
#include "editor/scene_tree_dock.h"
#include "editor/themes/editor_scale.h"
+#include "editor/themes/editor_theme_manager.h"
#include "scene/2d/polygon_2d.h"
#include "scene/2d/skeleton_2d.h"
#include "scene/2d/sprite_2d.h"
@@ -3898,7 +3899,7 @@ void CanvasItemEditor::_update_editor_settings() {
// to distinguish from the other key icons at the top. On a light theme,
// the icon will be dark, so we need to lighten it before blending it
// with the red color.
- const Color key_auto_color = EditorSettings::get_singleton()->is_dark_theme() ? Color(1, 1, 1) : Color(4.25, 4.25, 4.25);
+ const Color key_auto_color = EditorThemeManager::is_dark_theme() ? Color(1, 1, 1) : Color(4.25, 4.25, 4.25);
key_auto_insert_button->add_theme_color_override("icon_pressed_color", key_auto_color.lerp(Color(1, 0, 0), 0.55));
animation_menu->set_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl")));
diff --git a/editor/plugins/particle_process_material_editor_plugin.cpp b/editor/plugins/particle_process_material_editor_plugin.cpp
index e696da3f5e..d6ec3921d5 100644
--- a/editor/plugins/particle_process_material_editor_plugin.cpp
+++ b/editor/plugins/particle_process_material_editor_plugin.cpp
@@ -34,6 +34,7 @@
#include "editor/editor_settings.h"
#include "editor/editor_string_names.h"
#include "editor/gui/editor_spin_slider.h"
+#include "editor/themes/editor_theme_manager.h"
#include "scene/gui/box_container.h"
#include "scene/gui/button.h"
#include "scene/gui/label.h"
@@ -352,7 +353,7 @@ void ParticleProcessMaterialMinMaxPropertyEditor::_notification(int p_what) {
min_edit->add_theme_color_override(SNAME("label_color"), get_theme_color(SNAME("property_color_x"), EditorStringName(Editor)));
max_edit->add_theme_color_override(SNAME("label_color"), get_theme_color(SNAME("property_color_y"), EditorStringName(Editor)));
- const bool dark_theme = EditorSettings::get_singleton()->is_dark_theme();
+ const bool dark_theme = EditorThemeManager::is_dark_theme();
const Color accent_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));
background_color = dark_theme ? Color(0.3, 0.3, 0.3) : Color(0.7, 0.7, 0.7);
normal_color = dark_theme ? Color(0.5, 0.5, 0.5) : Color(0.8, 0.8, 0.8);
diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp
index 5bd6f83616..370144a427 100644
--- a/editor/plugins/script_text_editor.cpp
+++ b/editor/plugins/script_text_editor.cpp
@@ -146,6 +146,7 @@ void ScriptTextEditor::set_edited_resource(const Ref<Resource> &p_res) {
ERR_FAIL_COND(p_res.is_null());
script = p_res;
+ script->connect_changed(callable_mp((ScriptEditorBase *)this, &ScriptEditorBase::reload_text));
code_editor->get_text_editor()->set_text(script->get_source_code());
code_editor->get_text_editor()->clear_undo_history();
diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp
index e7277bad6a..fff7b89c01 100644
--- a/editor/project_manager.cpp
+++ b/editor/project_manager.cpp
@@ -36,17 +36,18 @@
#include "core/io/file_access.h"
#include "core/io/resource_saver.h"
#include "core/io/stream_peer_tls.h"
-#include "core/io/zip_io.h"
#include "core/os/keyboard.h"
#include "core/os/os.h"
#include "core/string/translation.h"
#include "core/version.h"
-#include "editor/editor_paths.h"
+#include "editor/editor_about.h"
#include "editor/editor_settings.h"
#include "editor/editor_string_names.h"
-#include "editor/editor_vcs_interface.h"
#include "editor/gui/editor_file_dialog.h"
#include "editor/plugins/asset_library_editor_plugin.h"
+#include "editor/project_manager/project_dialog.h"
+#include "editor/project_manager/project_list.h"
+#include "editor/project_manager/project_tag.h"
#include "editor/themes/editor_icons.h"
#include "editor/themes/editor_scale.h"
#include "editor/themes/editor_theme_manager.h"
@@ -57,1948 +58,21 @@
#include "scene/gui/flow_container.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/margin_container.h"
+#include "scene/gui/option_button.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/separator.h"
#include "scene/gui/texture_rect.h"
#include "scene/main/window.h"
-#include "scene/resources/image_texture.h"
#include "servers/display_server.h"
#include "servers/navigation_server_3d.h"
#include "servers/physics_server_2d.h"
constexpr int GODOT4_CONFIG_VERSION = 5;
-/// Project Dialog.
-
-void ProjectDialog::_set_message(const String &p_msg, MessageType p_type, InputType input_type) {
- msg->set_text(p_msg);
- Ref<Texture2D> current_path_icon = status_rect->get_texture();
- Ref<Texture2D> current_install_icon = install_status_rect->get_texture();
- Ref<Texture2D> new_icon;
-
- switch (p_type) {
- case MESSAGE_ERROR: {
- msg->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
- msg->set_modulate(Color(1, 1, 1, 1));
- new_icon = get_editor_theme_icon(SNAME("StatusError"));
-
- } break;
- case MESSAGE_WARNING: {
- msg->add_theme_color_override("font_color", get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
- msg->set_modulate(Color(1, 1, 1, 1));
- new_icon = get_editor_theme_icon(SNAME("StatusWarning"));
-
- } break;
- case MESSAGE_SUCCESS: {
- msg->remove_theme_color_override("font_color");
- msg->set_modulate(Color(1, 1, 1, 0));
- new_icon = get_editor_theme_icon(SNAME("StatusSuccess"));
-
- } break;
- }
-
- if (current_path_icon != new_icon && input_type == PROJECT_PATH) {
- status_rect->set_texture(new_icon);
- } else if (current_install_icon != new_icon && input_type == INSTALL_PATH) {
- install_status_rect->set_texture(new_icon);
- }
-}
-
-static bool is_zip_file(Ref<DirAccess> p_d, const String &p_path) {
- return p_path.ends_with(".zip") && p_d->file_exists(p_path);
-}
-
-String ProjectDialog::_test_path() {
- Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- const String base_path = project_path->get_text();
- String valid_path, valid_install_path;
- bool is_zip = false;
- if (d->change_dir(base_path) == OK) {
- valid_path = base_path;
- } else if (is_zip_file(d, base_path)) {
- valid_path = base_path;
- is_zip = true;
- } else if (d->change_dir(base_path.strip_edges()) == OK) {
- valid_path = base_path.strip_edges();
- } else if (is_zip_file(d, base_path.strip_edges())) {
- valid_path = base_path.strip_edges();
- is_zip = true;
- }
-
- if (valid_path.is_empty()) {
- _set_message(TTR("The path specified doesn't exist."), MESSAGE_ERROR);
- get_ok_button()->set_disabled(true);
- return "";
- }
-
- if (mode == MODE_IMPORT && is_zip) {
- if (d->change_dir(install_path->get_text()) == OK) {
- valid_install_path = install_path->get_text();
- } else if (d->change_dir(install_path->get_text().strip_edges()) == OK) {
- valid_install_path = install_path->get_text().strip_edges();
- }
-
- if (valid_install_path.is_empty()) {
- _set_message(TTR("The install path specified doesn't exist."), MESSAGE_ERROR, INSTALL_PATH);
- get_ok_button()->set_disabled(true);
- return "";
- }
- }
-
- if (mode == MODE_IMPORT || mode == MODE_RENAME) {
- if (!d->file_exists("project.godot")) {
- if (is_zip) {
- Ref<FileAccess> io_fa;
- zlib_filefunc_def io = zipio_create_io(&io_fa);
-
- unzFile pkg = unzOpen2(valid_path.utf8().get_data(), &io);
- if (!pkg) {
- _set_message(TTR("Error opening package file (it's not in ZIP format)."), MESSAGE_ERROR);
- get_ok_button()->set_disabled(true);
- unzClose(pkg);
- return "";
- }
-
- int ret = unzGoToFirstFile(pkg);
- while (ret == UNZ_OK) {
- unz_file_info info;
- char fname[16384];
- ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
- if (ret != UNZ_OK) {
- break;
- }
-
- if (String::utf8(fname).ends_with("project.godot")) {
- break;
- }
-
- ret = unzGoToNextFile(pkg);
- }
-
- if (ret == UNZ_END_OF_LIST_OF_FILE) {
- _set_message(TTR("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR);
- get_ok_button()->set_disabled(true);
- unzClose(pkg);
- return "";
- }
-
- unzClose(pkg);
-
- // check if the specified install folder is empty, even though this is not an error, it is good to check here
- d->list_dir_begin();
- is_folder_empty = true;
- String n = d->get_next();
- while (!n.is_empty()) {
- if (!n.begins_with(".")) {
- // Allow `.`, `..` (reserved current/parent folder names)
- // and hidden files/folders to be present.
- // For instance, this lets users initialize a Git repository
- // and still be able to create a project in the directory afterwards.
- is_folder_empty = false;
- break;
- }
- n = d->get_next();
- }
- d->list_dir_end();
-
- if (!is_folder_empty) {
- _set_message(TTR("Please choose an empty install folder."), MESSAGE_WARNING, INSTALL_PATH);
- get_ok_button()->set_disabled(true);
- return "";
- }
-
- } else {
- _set_message(TTR("Please choose a \"project.godot\", a directory with it, or a \".zip\" file."), MESSAGE_ERROR);
- install_path_container->hide();
- get_ok_button()->set_disabled(true);
- return "";
- }
-
- } else if (is_zip) {
- _set_message(TTR("The install directory already contains a Godot project."), MESSAGE_ERROR, INSTALL_PATH);
- get_ok_button()->set_disabled(true);
- return "";
- }
-
- } else {
- // Check if the specified folder is empty, even though this is not an error, it is good to check here.
- d->list_dir_begin();
- is_folder_empty = true;
- String n = d->get_next();
- while (!n.is_empty()) {
- if (!n.begins_with(".")) {
- // Allow `.`, `..` (reserved current/parent folder names)
- // and hidden files/folders to be present.
- // For instance, this lets users initialize a Git repository
- // and still be able to create a project in the directory afterwards.
- is_folder_empty = false;
- break;
- }
- n = d->get_next();
- }
- d->list_dir_end();
-
- if (!is_folder_empty) {
- 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()) {
- _set_message(TTR("You cannot save a project in the selected path. Please make a new folder or choose a new path."), MESSAGE_ERROR);
- get_ok_button()->set_disabled(true);
- return "";
- }
-
- _set_message(TTR("The selected path is not empty. Choosing an empty folder is highly recommended."), MESSAGE_WARNING);
- get_ok_button()->set_disabled(false);
- return valid_path;
- }
- }
-
- _set_message("");
- _set_message("", MESSAGE_SUCCESS, INSTALL_PATH);
- get_ok_button()->set_disabled(false);
- return valid_path;
-}
-
-void ProjectDialog::_update_path(const String &p_path) {
- String sp = _test_path();
- if (!sp.is_empty()) {
- // If the project name is empty or default, infer the project name from the selected folder name
- if (project_name->get_text().strip_edges().is_empty() || project_name->get_text().strip_edges() == TTR("New Game Project")) {
- sp = sp.replace("\\", "/");
- int lidx = sp.rfind("/");
-
- if (lidx != -1) {
- sp = sp.substr(lidx + 1, sp.length()).capitalize();
- }
- if (sp.is_empty() && mode == MODE_IMPORT) {
- sp = TTR("Imported Project");
- }
-
- project_name->set_text(sp);
- _text_changed(sp);
- }
- }
-
- if (!created_folder_path.is_empty() && created_folder_path != p_path) {
- _remove_created_folder();
- }
-}
-
-void ProjectDialog::_path_text_changed(const String &p_path) {
- Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- if (mode == MODE_IMPORT && is_zip_file(d, p_path)) {
- install_path->set_text(p_path.get_base_dir());
- install_path_container->show();
- } else if (mode == MODE_IMPORT && is_zip_file(d, p_path.strip_edges())) {
- install_path->set_text(p_path.strip_edges().get_base_dir());
- install_path_container->show();
- } else {
- install_path_container->hide();
- }
-
- _update_path(p_path.simplify_path());
-}
-
-void ProjectDialog::_file_selected(const String &p_path) {
- // If not already shown.
- show_dialog();
-
- String p = p_path;
- if (mode == MODE_IMPORT) {
- if (p.ends_with("project.godot")) {
- p = p.get_base_dir();
- install_path_container->hide();
- get_ok_button()->set_disabled(false);
- } else if (p.ends_with(".zip")) {
- install_path->set_text(p.get_base_dir());
- install_path_container->show();
- get_ok_button()->set_disabled(false);
- } else {
- _set_message(TTR("Please choose a \"project.godot\" or \".zip\" file."), MESSAGE_ERROR);
- get_ok_button()->set_disabled(true);
- return;
- }
- }
-
- String sp = p.simplify_path();
- project_path->set_text(sp);
- _update_path(sp);
- if (p.ends_with(".zip")) {
- callable_mp((Control *)install_path, &Control::grab_focus).call_deferred();
- } else {
- callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred();
- }
-}
-
-void ProjectDialog::_path_selected(const String &p_path) {
- // If not already shown.
- show_dialog();
-
- String sp = p_path.simplify_path();
- project_path->set_text(sp);
- _update_path(sp);
- callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred();
-}
-
-void ProjectDialog::_install_path_selected(const String &p_path) {
- String sp = p_path.simplify_path();
- install_path->set_text(sp);
- _update_path(sp);
- callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred();
-}
-
-void ProjectDialog::_browse_path() {
- fdialog->set_current_dir(project_path->get_text());
-
- if (mode == MODE_IMPORT) {
- fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_ANY);
- fdialog->clear_filters();
- fdialog->add_filter("project.godot", vformat("%s %s", VERSION_NAME, TTR("Project")));
- fdialog->add_filter("*.zip", TTR("ZIP File"));
- } else {
- fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
- }
- fdialog->popup_file_dialog();
-}
-
-void ProjectDialog::_browse_install_path() {
- fdialog_install->set_current_dir(install_path->get_text());
- fdialog_install->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
- fdialog_install->popup_file_dialog();
-}
-
-void ProjectDialog::_create_folder() {
- const String project_name_no_edges = project_name->get_text().strip_edges();
- if (project_name_no_edges.is_empty() || !created_folder_path.is_empty() || project_name_no_edges.ends_with(".")) {
- _set_message(TTR("Invalid project name."), MESSAGE_WARNING);
- return;
- }
-
- Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- if (d->change_dir(project_path->get_text()) == OK) {
- if (!d->dir_exists(project_name_no_edges)) {
- if (d->make_dir(project_name_no_edges) == OK) {
- d->change_dir(project_name_no_edges);
- String dir_str = d->get_current_dir();
- project_path->set_text(dir_str);
- _update_path(dir_str);
- created_folder_path = d->get_current_dir();
- create_dir->set_disabled(true);
- } else {
- dialog_error->set_text(TTR("Couldn't create folder."));
- dialog_error->popup_centered();
- }
- } else {
- dialog_error->set_text(TTR("There is already a folder in this path with the specified name."));
- dialog_error->popup_centered();
- }
- }
-}
-
-void ProjectDialog::_text_changed(const String &p_text) {
- if (mode != MODE_NEW) {
- return;
- }
-
- _test_path();
-
- if (p_text.strip_edges().is_empty()) {
- _set_message(TTR("It would be a good idea to name your project."), MESSAGE_ERROR);
- }
-}
-
-void ProjectDialog::_nonempty_confirmation_ok_pressed() {
- is_folder_empty = true;
- ok_pressed();
-}
-
-void ProjectDialog::_renderer_selected() {
- ERR_FAIL_NULL(renderer_button_group->get_pressed_button());
-
- String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));
-
- if (renderer_type == "forward_plus") {
- renderer_info->set_text(
- String::utf8("• ") + TTR("Supports desktop platforms only.") +
- String::utf8("\n• ") + TTR("Advanced 3D graphics available.") +
- String::utf8("\n• ") + TTR("Can scale to large complex scenes.") +
- String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +
- String::utf8("\n• ") + TTR("Slower rendering of simple scenes."));
- } else if (renderer_type == "mobile") {
- renderer_info->set_text(
- String::utf8("• ") + TTR("Supports desktop + mobile platforms.") +
- String::utf8("\n• ") + TTR("Less advanced 3D graphics.") +
- String::utf8("\n• ") + TTR("Less scalable for complex scenes.") +
- String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +
- String::utf8("\n• ") + TTR("Fast rendering of simple scenes."));
- } else if (renderer_type == "gl_compatibility") {
- renderer_info->set_text(
- String::utf8("• ") + TTR("Supports desktop, mobile + web platforms.") +
- String::utf8("\n• ") + TTR("Least advanced 3D graphics (currently work-in-progress).") +
- String::utf8("\n• ") + TTR("Intended for low-end/older devices.") +
- String::utf8("\n• ") + TTR("Uses OpenGL 3 backend (OpenGL 3.3/ES 3.0/WebGL2).") +
- String::utf8("\n• ") + TTR("Fastest rendering of simple scenes."));
- } else {
- WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
- }
-}
-
-void ProjectDialog::_remove_created_folder() {
- if (!created_folder_path.is_empty()) {
- Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- d->remove(created_folder_path);
-
- create_dir->set_disabled(false);
- created_folder_path = "";
- }
-}
-
-void ProjectDialog::ok_pressed() {
- String dir = project_path->get_text();
-
- if (mode == MODE_RENAME) {
- String dir2 = _test_path();
- if (dir2.is_empty()) {
- _set_message(TTR("Invalid project path (changed anything?)."), MESSAGE_ERROR);
- return;
- }
-
- // Load project.godot as ConfigFile to set the new name.
- ConfigFile cfg;
- String project_godot = dir2.path_join("project.godot");
- Error err = cfg.load(project_godot);
- if (err != OK) {
- _set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR);
- } else {
- cfg.set_value("application", "config/name", project_name->get_text().strip_edges());
- err = cfg.save(project_godot);
- if (err != OK) {
- _set_message(vformat(TTR("Couldn't save project at '%s' (error %d)."), project_godot, err), MESSAGE_ERROR);
- }
- }
-
- hide();
- emit_signal(SNAME("projects_updated"));
-
- } else {
- if (mode == MODE_IMPORT) {
- if (project_path->get_text().ends_with(".zip")) {
- mode = MODE_INSTALL;
- ok_pressed();
-
- return;
- }
-
- } else {
- if (mode == MODE_NEW) {
- // Before we create a project, check that the target folder is empty.
- // If not, we need to ask the user if they're sure they want to do this.
- if (!is_folder_empty) {
- ConfirmationDialog *cd = memnew(ConfirmationDialog);
- cd->set_title(TTR("Warning: This folder is not empty"));
- cd->set_text(TTR("You are about to create a Godot project in a non-empty folder.\nThe entire contents of this folder will be imported as project resources!\n\nAre you sure you wish to continue?"));
- cd->get_ok_button()->connect("pressed", callable_mp(this, &ProjectDialog::_nonempty_confirmation_ok_pressed));
- get_parent()->add_child(cd);
- cd->popup_centered();
- cd->grab_focus();
- return;
- }
- PackedStringArray project_features = ProjectSettings::get_required_features();
- ProjectSettings::CustomMap initial_settings;
-
- // Be sure to change this code if/when renderers are changed.
- // Default values are "forward_plus" for the main setting, "mobile" for the mobile override,
- // and "gl_compatibility" for the web override.
- String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));
- initial_settings["rendering/renderer/rendering_method"] = renderer_type;
-
- EditorSettings::get_singleton()->set("project_manager/default_renderer", renderer_type);
- EditorSettings::get_singleton()->save();
-
- if (renderer_type == "forward_plus") {
- project_features.push_back("Forward Plus");
- } else if (renderer_type == "mobile") {
- project_features.push_back("Mobile");
- } else if (renderer_type == "gl_compatibility") {
- project_features.push_back("GL Compatibility");
- // Also change the default rendering method for the mobile override.
- initial_settings["rendering/renderer/rendering_method.mobile"] = "gl_compatibility";
- } else {
- WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
- }
-
- project_features.sort();
- initial_settings["application/config/features"] = project_features;
- initial_settings["application/config/name"] = project_name->get_text().strip_edges();
- initial_settings["application/config/icon"] = "res://icon.svg";
-
- if (ProjectSettings::get_singleton()->save_custom(dir.path_join("project.godot"), initial_settings, Vector<String>(), false) != OK) {
- _set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR);
- } else {
- // Store default project icon in SVG format.
- Error err;
- Ref<FileAccess> fa_icon = FileAccess::open(dir.path_join("icon.svg"), FileAccess::WRITE, &err);
- fa_icon->store_string(get_default_project_icon());
-
- if (err != OK) {
- _set_message(TTR("Couldn't create icon.svg in project path."), MESSAGE_ERROR);
- }
-
- EditorVCSInterface::create_vcs_metadata_files(EditorVCSInterface::VCSMetadata(vcs_metadata_selection->get_selected()), dir);
- }
- } else if (mode == MODE_INSTALL) {
- if (project_path->get_text().ends_with(".zip")) {
- dir = install_path->get_text();
- zip_path = project_path->get_text();
- }
-
- Ref<FileAccess> io_fa;
- zlib_filefunc_def io = zipio_create_io(&io_fa);
-
- unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io);
- if (!pkg) {
- dialog_error->set_text(TTR("Error opening package file, not in ZIP format."));
- dialog_error->popup_centered();
- return;
- }
-
- // Find the zip_root
- String zip_root;
- int ret = unzGoToFirstFile(pkg);
- while (ret == UNZ_OK) {
- unz_file_info info;
- char fname[16384];
- unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
-
- String name = String::utf8(fname);
- if (name.ends_with("project.godot")) {
- zip_root = name.substr(0, name.rfind("project.godot"));
- break;
- }
-
- ret = unzGoToNextFile(pkg);
- }
-
- ret = unzGoToFirstFile(pkg);
-
- Vector<String> failed_files;
-
- while (ret == UNZ_OK) {
- //get filename
- unz_file_info info;
- char fname[16384];
- ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
- if (ret != UNZ_OK) {
- break;
- }
-
- String path = String::utf8(fname);
-
- if (path.is_empty() || path == zip_root || !zip_root.is_subsequence_of(path)) {
- //
- } else if (path.ends_with("/")) { // a dir
- path = path.substr(0, path.length() - 1);
- String rel_path = path.substr(zip_root.length());
-
- Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- da->make_dir(dir.path_join(rel_path));
- } else {
- Vector<uint8_t> uncomp_data;
- uncomp_data.resize(info.uncompressed_size);
- String rel_path = path.substr(zip_root.length());
-
- //read
- unzOpenCurrentFile(pkg);
- ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size());
- ERR_BREAK_MSG(ret < 0, vformat("An error occurred while attempting to read from file: %s. This file will not be used.", rel_path));
- unzCloseCurrentFile(pkg);
-
- Ref<FileAccess> f = FileAccess::open(dir.path_join(rel_path), FileAccess::WRITE);
- if (f.is_valid()) {
- f->store_buffer(uncomp_data.ptr(), uncomp_data.size());
- } else {
- failed_files.push_back(rel_path);
- }
- }
-
- ret = unzGoToNextFile(pkg);
- }
-
- unzClose(pkg);
-
- if (failed_files.size()) {
- String err_msg = TTR("The following files failed extraction from package:") + "\n\n";
- for (int i = 0; i < failed_files.size(); i++) {
- if (i > 15) {
- err_msg += "\nAnd " + itos(failed_files.size() - i) + " more files.";
- break;
- }
- err_msg += failed_files[i] + "\n";
- }
-
- dialog_error->set_text(err_msg);
- dialog_error->popup_centered();
-
- } else if (!project_path->get_text().ends_with(".zip")) {
- dialog_error->set_text(TTR("Package installed successfully!"));
- dialog_error->popup_centered();
- }
- }
- }
-
- dir = dir.replace("\\", "/");
- if (dir.ends_with("/")) {
- dir = dir.substr(0, dir.length() - 1);
- }
-
- hide();
- emit_signal(SNAME("project_created"), dir);
- }
-}
-
-void ProjectDialog::cancel_pressed() {
- _remove_created_folder();
-
- project_path->clear();
- _update_path("");
- project_name->clear();
- _text_changed("");
-
- if (status_rect->get_texture() == get_editor_theme_icon(SNAME("StatusError"))) {
- msg->show();
- }
-
- if (install_status_rect->get_texture() == get_editor_theme_icon(SNAME("StatusError"))) {
- msg->show();
- }
-}
-
-void ProjectDialog::set_zip_path(const String &p_path) {
- zip_path = p_path;
-}
-
-void ProjectDialog::set_zip_title(const String &p_title) {
- zip_title = p_title;
-}
-
-void ProjectDialog::set_mode(Mode p_mode) {
- mode = p_mode;
-}
-
-void ProjectDialog::set_project_path(const String &p_path) {
- project_path->set_text(p_path);
-}
-
-void ProjectDialog::ask_for_path_and_show() {
- // Workaround: for the file selection dialog content to be rendered we need to show its parent dialog.
- show_dialog();
- _set_message("");
-
- _browse_path();
-}
-
-void ProjectDialog::show_dialog() {
- if (mode == MODE_RENAME) {
- project_path->set_editable(false);
- browse->hide();
- install_browse->hide();
-
- set_title(TTR("Rename Project"));
- set_ok_button_text(TTR("Rename"));
- name_container->show();
- status_rect->hide();
- msg->hide();
- install_path_container->hide();
- install_status_rect->hide();
- renderer_container->hide();
- default_files_container->hide();
- get_ok_button()->set_disabled(false);
-
- // Fetch current name from project.godot to prefill the text input.
- ConfigFile cfg;
- String project_godot = project_path->get_text().path_join("project.godot");
- Error err = cfg.load(project_godot);
- if (err != OK) {
- _set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR);
- status_rect->show();
- msg->show();
- get_ok_button()->set_disabled(true);
- } else {
- String cur_name = cfg.get_value("application", "config/name", "");
- project_name->set_text(cur_name);
- _text_changed(cur_name);
- }
-
- callable_mp((Control *)project_name, &Control::grab_focus).call_deferred();
-
- create_dir->hide();
-
- } else {
- fav_dir = EDITOR_GET("filesystem/directories/default_project_path");
- if (!fav_dir.is_empty()) {
- project_path->set_text(fav_dir);
- fdialog->set_current_dir(fav_dir);
- } else {
- Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- project_path->set_text(d->get_current_dir());
- fdialog->set_current_dir(d->get_current_dir());
- }
-
- if (project_name->get_text().is_empty()) {
- String proj = TTR("New Game Project");
- project_name->set_text(proj);
- _text_changed(proj);
- }
-
- project_path->set_editable(true);
- browse->set_disabled(false);
- browse->show();
- install_browse->set_disabled(false);
- install_browse->show();
- create_dir->show();
- status_rect->show();
- install_status_rect->show();
- msg->show();
-
- if (mode == MODE_IMPORT) {
- set_title(TTR("Import Existing Project"));
- set_ok_button_text(TTR("Import & Edit"));
- name_container->hide();
- install_path_container->hide();
- renderer_container->hide();
- default_files_container->hide();
- project_path->grab_focus();
-
- } else if (mode == MODE_NEW) {
- set_title(TTR("Create New Project"));
- set_ok_button_text(TTR("Create & Edit"));
- name_container->show();
- install_path_container->hide();
- renderer_container->show();
- default_files_container->show();
- callable_mp((Control *)project_name, &Control::grab_focus).call_deferred();
- callable_mp(project_name, &LineEdit::select_all).call_deferred();
-
- } else if (mode == MODE_INSTALL) {
- set_title(TTR("Install Project:") + " " + zip_title);
- set_ok_button_text(TTR("Install & Edit"));
- project_name->set_text(zip_title);
- name_container->show();
- install_path_container->hide();
- renderer_container->hide();
- default_files_container->hide();
- project_path->grab_focus();
- }
-
- _test_path();
- }
-
- popup_centered(Size2(500, 0) * EDSCALE);
-}
-
-void ProjectDialog::_notification(int p_what) {
- switch (p_what) {
- case NOTIFICATION_WM_CLOSE_REQUEST: {
- _remove_created_folder();
- } break;
- }
-}
-
-void ProjectDialog::_bind_methods() {
- ADD_SIGNAL(MethodInfo("project_created"));
- ADD_SIGNAL(MethodInfo("projects_updated"));
-}
-
-ProjectDialog::ProjectDialog() {
- VBoxContainer *vb = memnew(VBoxContainer);
- add_child(vb);
-
- name_container = memnew(VBoxContainer);
- vb->add_child(name_container);
-
- Label *l = memnew(Label);
- l->set_text(TTR("Project Name:"));
- name_container->add_child(l);
-
- HBoxContainer *pnhb = memnew(HBoxContainer);
- name_container->add_child(pnhb);
-
- project_name = memnew(LineEdit);
- project_name->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- pnhb->add_child(project_name);
-
- create_dir = memnew(Button);
- pnhb->add_child(create_dir);
- create_dir->set_text(TTR("Create Folder"));
- create_dir->connect("pressed", callable_mp(this, &ProjectDialog::_create_folder));
-
- path_container = memnew(VBoxContainer);
- vb->add_child(path_container);
-
- l = memnew(Label);
- l->set_text(TTR("Project Path:"));
- path_container->add_child(l);
-
- HBoxContainer *pphb = memnew(HBoxContainer);
- path_container->add_child(pphb);
-
- project_path = memnew(LineEdit);
- project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
- pphb->add_child(project_path);
-
- install_path_container = memnew(VBoxContainer);
- vb->add_child(install_path_container);
-
- l = memnew(Label);
- l->set_text(TTR("Project Installation Path:"));
- install_path_container->add_child(l);
-
- HBoxContainer *iphb = memnew(HBoxContainer);
- install_path_container->add_child(iphb);
-
- install_path = memnew(LineEdit);
- install_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- install_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
- iphb->add_child(install_path);
-
- // status icon
- status_rect = memnew(TextureRect);
- status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
- pphb->add_child(status_rect);
-
- browse = memnew(Button);
- browse->set_text(TTR("Browse"));
- browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_path));
- pphb->add_child(browse);
-
- // install status icon
- install_status_rect = memnew(TextureRect);
- install_status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
- iphb->add_child(install_status_rect);
-
- install_browse = memnew(Button);
- install_browse->set_text(TTR("Browse"));
- install_browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_install_path));
- iphb->add_child(install_browse);
-
- msg = memnew(Label);
- msg->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
- msg->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
- vb->add_child(msg);
-
- // Renderer selection.
- renderer_container = memnew(VBoxContainer);
- vb->add_child(renderer_container);
- l = memnew(Label);
- l->set_text(TTR("Renderer:"));
- renderer_container->add_child(l);
- HBoxContainer *rshc = memnew(HBoxContainer);
- renderer_container->add_child(rshc);
- renderer_button_group.instantiate();
-
- // Left hand side, used for checkboxes to select renderer.
- Container *rvb = memnew(VBoxContainer);
- rshc->add_child(rvb);
-
- String default_renderer_type = "forward_plus";
- if (EditorSettings::get_singleton()->has_setting("project_manager/default_renderer")) {
- default_renderer_type = EditorSettings::get_singleton()->get_setting("project_manager/default_renderer");
- }
-
- Button *rs_button = memnew(CheckBox);
- rs_button->set_button_group(renderer_button_group);
- rs_button->set_text(TTR("Forward+"));
-#if defined(WEB_ENABLED)
- rs_button->set_disabled(true);
-#endif
- rs_button->set_meta(SNAME("rendering_method"), "forward_plus");
- rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
- rvb->add_child(rs_button);
- if (default_renderer_type == "forward_plus") {
- rs_button->set_pressed(true);
- }
- rs_button = memnew(CheckBox);
- rs_button->set_button_group(renderer_button_group);
- rs_button->set_text(TTR("Mobile"));
-#if defined(WEB_ENABLED)
- rs_button->set_disabled(true);
-#endif
- rs_button->set_meta(SNAME("rendering_method"), "mobile");
- rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
- rvb->add_child(rs_button);
- if (default_renderer_type == "mobile") {
- rs_button->set_pressed(true);
- }
- rs_button = memnew(CheckBox);
- rs_button->set_button_group(renderer_button_group);
- rs_button->set_text(TTR("Compatibility"));
-#if !defined(GLES3_ENABLED)
- rs_button->set_disabled(true);
-#endif
- rs_button->set_meta(SNAME("rendering_method"), "gl_compatibility");
- rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
- rvb->add_child(rs_button);
-#if defined(GLES3_ENABLED)
- if (default_renderer_type == "gl_compatibility") {
- rs_button->set_pressed(true);
- }
-#endif
- rshc->add_child(memnew(VSeparator));
-
- // Right hand side, used for text explaining each choice.
- rvb = memnew(VBoxContainer);
- rvb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- rshc->add_child(rvb);
- renderer_info = memnew(Label);
- renderer_info->set_modulate(Color(1, 1, 1, 0.7));
- rvb->add_child(renderer_info);
- _renderer_selected();
-
- l = memnew(Label);
- l->set_text(TTR("The renderer can be changed later, but scenes may need to be adjusted."));
- // Add some extra spacing to separate it from the list above and the buttons below.
- l->set_custom_minimum_size(Size2(0, 40) * EDSCALE);
- l->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
- l->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
- l->set_modulate(Color(1, 1, 1, 0.7));
- renderer_container->add_child(l);
-
- default_files_container = memnew(HBoxContainer);
- vb->add_child(default_files_container);
- l = memnew(Label);
- l->set_text(TTR("Version Control Metadata:"));
- default_files_container->add_child(l);
- vcs_metadata_selection = memnew(OptionButton);
- vcs_metadata_selection->set_custom_minimum_size(Size2(100, 20));
- vcs_metadata_selection->add_item(TTR("None"), (int)EditorVCSInterface::VCSMetadata::NONE);
- vcs_metadata_selection->add_item(TTR("Git"), (int)EditorVCSInterface::VCSMetadata::GIT);
- vcs_metadata_selection->select((int)EditorVCSInterface::VCSMetadata::GIT);
- default_files_container->add_child(vcs_metadata_selection);
- Control *spacer = memnew(Control);
- spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- default_files_container->add_child(spacer);
-
- fdialog = memnew(EditorFileDialog);
- fdialog->set_previews_enabled(false); //Crucial, otherwise the engine crashes.
- fdialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
- fdialog_install = memnew(EditorFileDialog);
- fdialog_install->set_previews_enabled(false); //Crucial, otherwise the engine crashes.
- fdialog_install->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
- add_child(fdialog);
- add_child(fdialog_install);
-
- project_name->connect("text_changed", callable_mp(this, &ProjectDialog::_text_changed));
- project_path->connect("text_changed", callable_mp(this, &ProjectDialog::_path_text_changed));
- install_path->connect("text_changed", callable_mp(this, &ProjectDialog::_update_path));
- fdialog->connect("dir_selected", callable_mp(this, &ProjectDialog::_path_selected));
- fdialog->connect("file_selected", callable_mp(this, &ProjectDialog::_file_selected));
- fdialog_install->connect("dir_selected", callable_mp(this, &ProjectDialog::_install_path_selected));
- fdialog_install->connect("file_selected", callable_mp(this, &ProjectDialog::_install_path_selected));
-
- set_hide_on_ok(false);
-
- dialog_error = memnew(AcceptDialog);
- add_child(dialog_error);
-}
-
-/// Project List and friends.
-
-void ProjectListItemControl::_notification(int p_what) {
- switch (p_what) {
- case NOTIFICATION_THEME_CHANGED: {
- if (icon_needs_reload) {
- // The project icon may not be loaded by the time the control is displayed,
- // so use a loading placeholder.
- project_icon->set_texture(get_editor_theme_icon(SNAME("ProjectIconLoading")));
- }
-
- project_title->begin_bulk_theme_override();
- project_title->add_theme_font_override("font", get_theme_font(SNAME("title"), EditorStringName(EditorFonts)));
- project_title->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("title_size"), EditorStringName(EditorFonts)));
- project_title->add_theme_color_override("font_color", get_theme_color(SNAME("font_color"), SNAME("Tree")));
- project_title->end_bulk_theme_override();
-
- project_path->add_theme_color_override("font_color", get_theme_color(SNAME("font_color"), SNAME("Tree")));
- project_unsupported_features->set_texture(get_editor_theme_icon(SNAME("NodeWarning")));
-
- favorite_button->set_texture_normal(get_editor_theme_icon(SNAME("Favorites")));
- if (project_is_missing) {
- explore_button->set_icon(get_editor_theme_icon(SNAME("FileBroken")));
- } else {
- explore_button->set_icon(get_editor_theme_icon(SNAME("Load")));
- }
- } break;
-
- case NOTIFICATION_MOUSE_ENTER: {
- is_hovering = true;
- queue_redraw();
- } break;
-
- case NOTIFICATION_MOUSE_EXIT: {
- is_hovering = false;
- queue_redraw();
- } break;
-
- case NOTIFICATION_DRAW: {
- if (is_selected) {
- draw_style_box(get_theme_stylebox(SNAME("selected"), SNAME("Tree")), Rect2(Point2(), get_size()));
- }
- if (is_hovering) {
- draw_style_box(get_theme_stylebox(SNAME("hover"), SNAME("Tree")), Rect2(Point2(), get_size()));
- }
-
- draw_line(Point2(0, get_size().y + 1), Point2(get_size().x, get_size().y + 1), get_theme_color(SNAME("guide_color"), SNAME("Tree")));
- } break;
- }
-}
-
-void ProjectListItemControl::set_project_title(const String &p_title) {
- project_title->set_text(p_title);
-}
-
-void ProjectListItemControl::set_project_path(const String &p_path) {
- project_path->set_text(p_path);
-}
-
-void ProjectListItemControl::set_tags(const PackedStringArray &p_tags, ProjectList *p_parent_list) {
- for (const String &tag : p_tags) {
- ProjectTag *tag_control = memnew(ProjectTag(tag));
- tag_container->add_child(tag_control);
- tag_control->connect_button_to(callable_mp(p_parent_list, &ProjectList::add_search_tag).bind(tag));
- }
-}
-
-void ProjectListItemControl::set_project_icon(const Ref<Texture2D> &p_icon) {
- icon_needs_reload = false;
-
- // The default project icon is 128×128 to look crisp on hiDPI displays,
- // but we want the actual displayed size to be 64×64 on loDPI displays.
- project_icon->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
- project_icon->set_custom_minimum_size(Size2(64, 64) * EDSCALE);
- project_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
-
- project_icon->set_texture(p_icon);
-}
-
-bool _project_feature_looks_like_version(const String &p_feature) {
- return p_feature.contains(".") && p_feature.substr(0, 3).is_numeric();
-}
-
-void ProjectListItemControl::set_unsupported_features(PackedStringArray p_features) {
- if (p_features.size() > 0) {
- String tooltip_text = "";
- for (int i = 0; i < p_features.size(); i++) {
- if (_project_feature_looks_like_version(p_features[i])) {
- tooltip_text += TTR("This project was last edited in a different Godot version: ") + p_features[i] + "\n";
- p_features.remove_at(i);
- i--;
- }
- }
- if (p_features.size() > 0) {
- String unsupported_features_str = String(", ").join(p_features);
- tooltip_text += TTR("This project uses features unsupported by the current build:") + "\n" + unsupported_features_str;
- }
- project_unsupported_features->set_tooltip_text(tooltip_text);
- project_unsupported_features->show();
- } else {
- project_unsupported_features->hide();
- }
-}
-
-bool ProjectListItemControl::should_load_project_icon() const {
- return icon_needs_reload;
-}
-
-void ProjectListItemControl::set_selected(bool p_selected) {
- is_selected = p_selected;
- queue_redraw();
-}
-
-void ProjectListItemControl::set_is_favorite(bool p_favorite) {
- favorite_button->set_modulate(p_favorite ? Color(1, 1, 1, 1) : Color(1, 1, 1, 0.2));
-}
-
-void ProjectListItemControl::set_is_missing(bool p_missing) {
- if (project_is_missing == p_missing) {
- return;
- }
- project_is_missing = p_missing;
-
- if (project_is_missing) {
- project_icon->set_modulate(Color(1, 1, 1, 0.5));
-
- explore_button->set_icon(get_editor_theme_icon(SNAME("FileBroken")));
- explore_button->set_tooltip_text(TTR("Error: Project is missing on the filesystem."));
- } else {
- project_icon->set_modulate(Color(1, 1, 1, 1.0));
-
- explore_button->set_icon(get_editor_theme_icon(SNAME("Load")));
-#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
- explore_button->set_tooltip_text(TTR("Show in File Manager"));
-#else
- // Opening the system file manager is not supported on the Android and web editors.
- explore_button->hide();
-#endif
- }
-}
-
-void ProjectListItemControl::set_is_grayed(bool p_grayed) {
- if (p_grayed) {
- main_vbox->set_modulate(Color(1, 1, 1, 0.5));
- // Don't make the icon less prominent if the parent is already grayed out.
- explore_button->set_modulate(Color(1, 1, 1, 1.0));
- } else {
- main_vbox->set_modulate(Color(1, 1, 1, 1.0));
- explore_button->set_modulate(Color(1, 1, 1, 0.5));
- }
-}
-
-void ProjectListItemControl::_favorite_button_pressed() {
- emit_signal(SNAME("favorite_pressed"));
-}
-
-void ProjectListItemControl::_explore_button_pressed() {
- emit_signal(SNAME("explore_pressed"));
-}
-
-void ProjectListItemControl::_bind_methods() {
- ADD_SIGNAL(MethodInfo("favorite_pressed"));
- ADD_SIGNAL(MethodInfo("explore_pressed"));
-}
-
-ProjectListItemControl::ProjectListItemControl() {
- set_focus_mode(FocusMode::FOCUS_ALL);
-
- VBoxContainer *favorite_box = memnew(VBoxContainer);
- favorite_box->set_alignment(BoxContainer::ALIGNMENT_CENTER);
- add_child(favorite_box);
-
- favorite_button = memnew(TextureButton);
- favorite_button->set_name("FavoriteButton");
- // This makes the project's "hover" style display correctly when hovering the favorite icon.
- favorite_button->set_mouse_filter(MOUSE_FILTER_PASS);
- favorite_box->add_child(favorite_button);
- favorite_button->connect("pressed", callable_mp(this, &ProjectListItemControl::_favorite_button_pressed));
-
- project_icon = memnew(TextureRect);
- project_icon->set_name("ProjectIcon");
- project_icon->set_v_size_flags(SIZE_SHRINK_CENTER);
- add_child(project_icon);
-
- main_vbox = memnew(VBoxContainer);
- main_vbox->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- add_child(main_vbox);
-
- Control *ec = memnew(Control);
- ec->set_custom_minimum_size(Size2(0, 1));
- ec->set_mouse_filter(MOUSE_FILTER_PASS);
- main_vbox->add_child(ec);
-
- // Top half, title, tags and unsupported features labels.
- {
- HBoxContainer *title_hb = memnew(HBoxContainer);
- main_vbox->add_child(title_hb);
-
- project_title = memnew(Label);
- project_title->set_auto_translate(false);
- project_title->set_name("ProjectName");
- project_title->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- project_title->set_clip_text(true);
- title_hb->add_child(project_title);
-
- tag_container = memnew(HBoxContainer);
- title_hb->add_child(tag_container);
-
- Control *spacer = memnew(Control);
- spacer->set_custom_minimum_size(Size2(10, 10));
- title_hb->add_child(spacer);
- }
-
- // Bottom half, containing the path and view folder button.
- {
- HBoxContainer *path_hb = memnew(HBoxContainer);
- path_hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- main_vbox->add_child(path_hb);
-
- explore_button = memnew(Button);
- explore_button->set_name("ExploreButton");
- explore_button->set_flat(true);
- path_hb->add_child(explore_button);
- explore_button->connect("pressed", callable_mp(this, &ProjectListItemControl::_explore_button_pressed));
-
- project_path = memnew(Label);
- project_path->set_name("ProjectPath");
- project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
- project_path->set_clip_text(true);
- project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- project_path->set_modulate(Color(1, 1, 1, 0.5));
- path_hb->add_child(project_path);
-
- project_unsupported_features = memnew(TextureRect);
- project_unsupported_features->set_name("ProjectUnsupportedFeatures");
- project_unsupported_features->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
- path_hb->add_child(project_unsupported_features);
- project_unsupported_features->hide();
-
- Control *spacer = memnew(Control);
- spacer->set_custom_minimum_size(Size2(10, 10));
- path_hb->add_child(spacer);
- }
-}
-
-struct ProjectListComparator {
- ProjectList::FilterOption order_option = ProjectList::FilterOption::EDIT_DATE;
-
- // operator<
- _FORCE_INLINE_ bool operator()(const ProjectList::Item &a, const ProjectList::Item &b) const {
- if (a.favorite && !b.favorite) {
- return true;
- }
- if (b.favorite && !a.favorite) {
- return false;
- }
- switch (order_option) {
- case ProjectList::PATH:
- return a.path < b.path;
- case ProjectList::EDIT_DATE:
- return a.last_edited > b.last_edited;
- case ProjectList::TAGS:
- return a.tag_sort_string < b.tag_sort_string;
- default:
- return a.project_name < b.project_name;
- }
- }
-};
-
-const char *ProjectList::SIGNAL_LIST_CHANGED = "list_changed";
-const char *ProjectList::SIGNAL_SELECTION_CHANGED = "selection_changed";
-const char *ProjectList::SIGNAL_PROJECT_ASK_OPEN = "project_ask_open";
-
-void ProjectList::_notification(int p_what) {
- switch (p_what) {
- case NOTIFICATION_PROCESS: {
- // Load icons as a coroutine to speed up launch when you have hundreds of projects
- if (_icon_load_index < _projects.size()) {
- Item &item = _projects.write[_icon_load_index];
- if (item.control->should_load_project_icon()) {
- _load_project_icon(_icon_load_index);
- }
- _icon_load_index++;
-
- } else {
- set_process(false);
- }
- } break;
- }
-}
-
-void ProjectList::_update_icons_async() {
- _icon_load_index = 0;
- set_process(true);
-}
-
-void ProjectList::_load_project_icon(int p_index) {
- Item &item = _projects.write[p_index];
-
- Ref<Texture2D> default_icon = get_editor_theme_icon(SNAME("DefaultProjectIcon"));
- Ref<Texture2D> icon;
- if (!item.icon.is_empty()) {
- Ref<Image> img;
- img.instantiate();
- Error err = img->load(item.icon.replace_first("res://", item.path + "/"));
- if (err == OK) {
- img->resize(default_icon->get_width(), default_icon->get_height(), Image::INTERPOLATE_LANCZOS);
- icon = ImageTexture::create_from_image(img);
- }
- }
- if (icon.is_null()) {
- icon = default_icon;
- }
-
- item.control->set_project_icon(icon);
-}
-
-// Load project data from p_property_key and return it in a ProjectList::Item.
-// p_favorite is passed directly into the Item.
-ProjectList::Item ProjectList::load_project_data(const String &p_path, bool p_favorite) {
- String conf = p_path.path_join("project.godot");
- bool grayed = false;
- bool missing = false;
-
- Ref<ConfigFile> cf = memnew(ConfigFile);
- Error cf_err = cf->load(conf);
-
- int config_version = 0;
- String project_name = TTR("Unnamed Project");
- if (cf_err == OK) {
- String cf_project_name = cf->get_value("application", "config/name", "");
- if (!cf_project_name.is_empty()) {
- project_name = cf_project_name.xml_unescape();
- }
- config_version = (int)cf->get_value("", "config_version", 0);
- }
-
- if (config_version > ProjectSettings::CONFIG_VERSION) {
- // Comes from an incompatible (more recent) Godot version, gray it out.
- grayed = true;
- }
-
- const String description = cf->get_value("application", "config/description", "");
- const PackedStringArray tags = cf->get_value("application", "config/tags", PackedStringArray());
- const String icon = cf->get_value("application", "config/icon", "");
- const String main_scene = cf->get_value("application", "run/main_scene", "");
-
- PackedStringArray project_features = cf->get_value("application", "config/features", PackedStringArray());
- PackedStringArray unsupported_features = ProjectSettings::get_unsupported_features(project_features);
-
- uint64_t last_edited = 0;
- if (cf_err == OK) {
- // The modification date marks the date the project was last edited.
- // This is because the `project.godot` file will always be modified
- // when editing a project (but not when running it).
- last_edited = FileAccess::get_modified_time(conf);
-
- String fscache = p_path.path_join(".fscache");
- if (FileAccess::exists(fscache)) {
- uint64_t cache_modified = FileAccess::get_modified_time(fscache);
- if (cache_modified > last_edited) {
- last_edited = cache_modified;
- }
- }
- } else {
- grayed = true;
- missing = true;
- print_line("Project is missing: " + conf);
- }
-
- for (const String &tag : tags) {
- ProjectManager::get_singleton()->add_new_tag(tag);
- }
-
- return Item(project_name, description, tags, p_path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, config_version);
-}
-
-void ProjectList::_migrate_config() {
- // Proposal #1637 moved the project list from editor settings to a separate config file
- // If the new config file doesn't exist, populate it from EditorSettings
- if (FileAccess::exists(_config_path)) {
- return;
- }
-
- List<PropertyInfo> properties;
- EditorSettings::get_singleton()->get_property_list(&properties);
-
- for (const PropertyInfo &E : properties) {
- // This is actually something like "projects/C:::Documents::Godot::Projects::MyGame"
- String property_key = E.name;
- if (!property_key.begins_with("projects/")) {
- continue;
- }
-
- String path = EDITOR_GET(property_key);
- print_line("Migrating legacy project '" + path + "'.");
-
- String favoriteKey = "favorite_projects/" + property_key.get_slice("/", 1);
- bool favorite = EditorSettings::get_singleton()->has_setting(favoriteKey);
- add_project(path, favorite);
- if (favorite) {
- EditorSettings::get_singleton()->erase(favoriteKey);
- }
- EditorSettings::get_singleton()->erase(property_key);
- }
-
- save_config();
-}
-
-void ProjectList::update_project_list() {
- // This is a full, hard reload of the list. Don't call this unless really required, it's expensive.
- // If you have 150 projects, it may read through 150 files on your disk at once + load 150 icons.
- // FIXME: Does it really have to be a full, hard reload? Runtime updates should be made much cheaper.
-
- // Clear whole list
- for (int i = 0; i < _projects.size(); ++i) {
- Item &project = _projects.write[i];
- CRASH_COND(project.control == nullptr);
- memdelete(project.control); // Why not queue_free()?
- }
- _projects.clear();
- _last_clicked = "";
- _selected_project_paths.clear();
-
- List<String> sections;
- _config.load(_config_path);
- _config.get_sections(&sections);
-
- for (const String &path : sections) {
- bool favorite = _config.get_value(path, "favorite", false);
- _projects.push_back(load_project_data(path, favorite));
- }
-
- // Create controls
- for (int i = 0; i < _projects.size(); ++i) {
- _create_project_item_control(i);
- }
-
- sort_projects();
- _update_icons_async();
- update_dock_menu();
-
- set_v_scroll(0);
- emit_signal(SNAME(SIGNAL_LIST_CHANGED));
-}
-
-void ProjectList::update_dock_menu() {
- if (!DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_GLOBAL_MENU)) {
- return;
- }
- DisplayServer::get_singleton()->global_menu_clear("_dock");
-
- int favs_added = 0;
- int total_added = 0;
- for (int i = 0; i < _projects.size(); ++i) {
- if (!_projects[i].grayed && !_projects[i].missing) {
- if (_projects[i].favorite) {
- favs_added++;
- } else {
- if (favs_added != 0) {
- DisplayServer::get_singleton()->global_menu_add_separator("_dock");
- }
- favs_added = 0;
- }
- 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);
- total_added++;
- }
- }
- if (total_added != 0) {
- DisplayServer::get_singleton()->global_menu_add_separator("_dock");
- }
- DisplayServer::get_singleton()->global_menu_add_item("_dock", TTR("New Window"), callable_mp(this, &ProjectList::_global_menu_new_window));
-}
-
-void ProjectList::_global_menu_new_window(const Variant &p_tag) {
- List<String> args;
- args.push_back("-p");
- OS::get_singleton()->create_instance(args);
-}
-
-void ProjectList::_global_menu_open_project(const Variant &p_tag) {
- int idx = (int)p_tag;
-
- if (idx >= 0 && idx < _projects.size()) {
- String conf = _projects[idx].path.path_join("project.godot");
- List<String> args;
- args.push_back(conf);
- OS::get_singleton()->create_instance(args);
- }
-}
-
-void ProjectList::_create_project_item_control(int p_index) {
- // Will be added last in the list, so make sure indexes match
- ERR_FAIL_COND(p_index != _scroll_children->get_child_count());
-
- Item &item = _projects.write[p_index];
- ERR_FAIL_COND(item.control != nullptr); // Already created
-
- ProjectListItemControl *hb = memnew(ProjectListItemControl);
- hb->add_theme_constant_override("separation", 10 * EDSCALE);
-
- hb->set_project_title(!item.missing ? item.project_name : TTR("Missing Project"));
- hb->set_project_path(item.path);
- hb->set_tooltip_text(item.description);
- hb->set_tags(item.tags, this);
- hb->set_unsupported_features(item.unsupported_features.duplicate());
-
- hb->set_is_favorite(item.favorite);
- hb->set_is_missing(item.missing);
- hb->set_is_grayed(item.grayed);
-
- hb->connect("gui_input", callable_mp(this, &ProjectList::_panel_input).bind(hb));
- hb->connect("favorite_pressed", callable_mp(this, &ProjectList::_favorite_pressed).bind(hb));
-
-#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
- hb->connect("explore_pressed", callable_mp(this, &ProjectList::_show_project).bind(item.path));
-#endif
-
- _scroll_children->add_child(hb);
- item.control = hb;
-}
-
-void ProjectList::set_search_term(String p_search_term) {
- _search_term = p_search_term;
-}
-
-void ProjectList::set_order_option(int p_option) {
- FilterOption selected = (FilterOption)p_option;
- EditorSettings::get_singleton()->set("project_manager/sorting_order", p_option);
- EditorSettings::get_singleton()->save();
- _order_option = selected;
-
- sort_projects();
-}
-
-void ProjectList::sort_projects() {
- SortArray<Item, ProjectListComparator> sorter;
- sorter.compare.order_option = _order_option;
- sorter.sort(_projects.ptrw(), _projects.size());
-
- String search_term;
- PackedStringArray tags;
-
- if (!_search_term.is_empty()) {
- PackedStringArray search_parts = _search_term.split(" ");
- if (search_parts.size() > 1 || search_parts[0].begins_with("tag:")) {
- PackedStringArray remaining;
- for (const String &part : search_parts) {
- if (part.begins_with("tag:")) {
- tags.push_back(part.get_slice(":", 1));
- } else {
- remaining.append(part);
- }
- }
- search_term = String(" ").join(remaining); // Search term without tags.
- } else {
- search_term = _search_term;
- }
- }
-
- for (int i = 0; i < _projects.size(); ++i) {
- Item &item = _projects.write[i];
-
- bool item_visible = true;
- if (!_search_term.is_empty()) {
- String search_path;
- if (search_term.contains("/")) {
- // Search path will match the whole path
- search_path = item.path;
- } else {
- // Search path will only match the last path component to make searching more strict
- search_path = item.path.get_file();
- }
-
- bool missing_tags = false;
- for (const String &tag : tags) {
- if (!item.tags.has(tag)) {
- missing_tags = true;
- break;
- }
- }
-
- // When searching, display projects whose name or path contain the search term and whose tags match the searched tags.
- item_visible = !missing_tags && (search_term.is_empty() || item.project_name.findn(search_term) != -1 || search_path.findn(search_term) != -1);
- }
-
- item.control->set_visible(item_visible);
- }
-
- for (int i = 0; i < _projects.size(); ++i) {
- Item &item = _projects.write[i];
- item.control->get_parent()->move_child(item.control, i);
- }
-
- // Rewind the coroutine because order of projects changed
- _update_icons_async();
- update_dock_menu();
-}
-
-const HashSet<String> &ProjectList::get_selected_project_keys() const {
- // Faster if that's all you need
- return _selected_project_paths;
-}
-
-Vector<ProjectList::Item> ProjectList::get_selected_projects() const {
- Vector<Item> items;
- if (_selected_project_paths.size() == 0) {
- return items;
- }
- items.resize(_selected_project_paths.size());
- int j = 0;
- for (int i = 0; i < _projects.size(); ++i) {
- const Item &item = _projects[i];
- if (_selected_project_paths.has(item.path)) {
- items.write[j++] = item;
- }
- }
- ERR_FAIL_COND_V(j != items.size(), items);
- return items;
-}
-
-void ProjectList::ensure_project_visible(int p_index) {
- const Item &item = _projects[p_index];
- ensure_control_visible(item.control);
-}
-
-int ProjectList::get_single_selected_index() const {
- if (_selected_project_paths.size() == 0) {
- // Default selection
- return 0;
- }
- String key;
- if (_selected_project_paths.size() == 1) {
- // Only one selected
- key = *_selected_project_paths.begin();
- } else {
- // Multiple selected, consider the last clicked one as "main"
- key = _last_clicked;
- }
- for (int i = 0; i < _projects.size(); ++i) {
- if (_projects[i].path == key) {
- return i;
- }
- }
- return 0;
-}
-
-void ProjectList::_remove_project(int p_index, bool p_update_config) {
- const Item item = _projects[p_index]; // Take a copy
-
- _selected_project_paths.erase(item.path);
-
- if (_last_clicked == item.path) {
- _last_clicked = "";
- }
-
- memdelete(item.control);
- _projects.remove_at(p_index);
-
- if (p_update_config) {
- _config.erase_section(item.path);
- // Not actually saving the file, in case you are doing more changes to settings
- }
-
- update_dock_menu();
-}
-
-bool ProjectList::is_any_project_missing() const {
- for (int i = 0; i < _projects.size(); ++i) {
- if (_projects[i].missing) {
- return true;
- }
- }
- return false;
-}
-
-void ProjectList::erase_missing_projects() {
- if (_projects.is_empty()) {
- return;
- }
-
- int deleted_count = 0;
- int remaining_count = 0;
-
- for (int i = 0; i < _projects.size(); ++i) {
- const Item &item = _projects[i];
-
- if (item.missing) {
- _remove_project(i, true);
- --i;
- ++deleted_count;
-
- } else {
- ++remaining_count;
- }
- }
-
- print_line("Removed " + itos(deleted_count) + " projects from the list, remaining " + itos(remaining_count) + " projects");
- save_config();
-}
-
-void ProjectList::_scan_folder_recursive(const String &p_path, List<String> *r_projects) {
- Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- Error error = da->change_dir(p_path);
- ERR_FAIL_COND_MSG(error != OK, vformat("Failed to open the path \"%s\" for scanning (code %d).", p_path, error));
-
- da->list_dir_begin();
- String n = da->get_next();
- while (!n.is_empty()) {
- if (da->current_is_dir() && n[0] != '.') {
- _scan_folder_recursive(da->get_current_dir().path_join(n), r_projects);
- } else if (n == "project.godot") {
- r_projects->push_back(da->get_current_dir());
- }
- n = da->get_next();
- }
- da->list_dir_end();
-}
-
-void ProjectList::find_projects(const String &p_path) {
- PackedStringArray paths = { p_path };
- find_projects_multiple(paths);
-}
-
-void ProjectList::find_projects_multiple(const PackedStringArray &p_paths) {
- List<String> projects;
-
- for (int i = 0; i < p_paths.size(); i++) {
- const String &base_path = p_paths.get(i);
- print_verbose(vformat("Scanning for projects in \"%s\".", base_path));
-
- _scan_folder_recursive(base_path, &projects);
- print_verbose(vformat("Found %d project(s).", projects.size()));
- }
-
- for (const String &E : projects) {
- add_project(E, false);
- }
-
- save_config();
- update_project_list();
-}
-
-int ProjectList::refresh_project(const String &dir_path) {
- // Reloads information about a specific project.
- // If it wasn't loaded and should be in the list, it is added (i.e new project).
- // If it isn't in the list anymore, it is removed.
- // If it is in the list but doesn't exist anymore, it is marked as missing.
-
- bool should_be_in_list = _config.has_section(dir_path);
- bool is_favourite = _config.get_value(dir_path, "favorite", false);
-
- bool was_selected = _selected_project_paths.has(dir_path);
-
- // Remove item in any case
- for (int i = 0; i < _projects.size(); ++i) {
- const Item &existing_item = _projects[i];
- if (existing_item.path == dir_path) {
- _remove_project(i, false);
- break;
- }
- }
-
- int index = -1;
- if (should_be_in_list) {
- // Recreate it with updated info
-
- Item item = load_project_data(dir_path, is_favourite);
-
- _projects.push_back(item);
- _create_project_item_control(_projects.size() - 1);
-
- sort_projects();
-
- for (int i = 0; i < _projects.size(); ++i) {
- if (_projects[i].path == dir_path) {
- if (was_selected) {
- select_project(i);
- ensure_project_visible(i);
- }
- _load_project_icon(i);
-
- index = i;
- break;
- }
- }
- }
-
- return index;
-}
-
-void ProjectList::add_project(const String &dir_path, bool favorite) {
- if (!_config.has_section(dir_path)) {
- _config.set_value(dir_path, "favorite", favorite);
- }
-}
-
-void ProjectList::save_config() {
- _config.save(_config_path);
-}
-
-void ProjectList::set_project_version(const String &p_project_path, int p_version) {
- for (ProjectList::Item &E : _projects) {
- if (E.path == p_project_path) {
- E.version = p_version;
- break;
- }
- }
-}
-
-int ProjectList::get_project_count() const {
- return _projects.size();
-}
-
-void ProjectList::_clear_project_selection() {
- Vector<Item> previous_selected_items = get_selected_projects();
- _selected_project_paths.clear();
-
- for (int i = 0; i < previous_selected_items.size(); ++i) {
- previous_selected_items[i].control->set_selected(false);
- }
-}
-
-void ProjectList::_toggle_project(int p_index) {
- // This methods adds to the selection or removes from the
- // selection.
- Item &item = _projects.write[p_index];
-
- if (_selected_project_paths.has(item.path)) {
- _deselect_project_nocheck(p_index);
- } else {
- _select_project_nocheck(p_index);
- }
-}
-
-void ProjectList::_select_project_nocheck(int p_index) {
- Item &item = _projects.write[p_index];
- _selected_project_paths.insert(item.path);
- item.control->set_selected(true);
-}
-
-void ProjectList::_deselect_project_nocheck(int p_index) {
- Item &item = _projects.write[p_index];
- _selected_project_paths.erase(item.path);
- item.control->set_selected(false);
-}
-
-void ProjectList::select_project(int p_index) {
- // This method keeps only one project selected.
- _clear_project_selection();
- _select_project_nocheck(p_index);
-}
-
-void ProjectList::select_first_visible_project() {
- _clear_project_selection();
-
- for (int i = 0; i < _projects.size(); i++) {
- if (_projects[i].control->is_visible()) {
- _select_project_nocheck(i);
- break;
- }
- }
-}
-
-inline void _sort_project_range(int &a, int &b) {
- if (a > b) {
- int temp = a;
- a = b;
- b = temp;
- }
-}
-
-void ProjectList::_select_project_range(int p_begin, int p_end) {
- _clear_project_selection();
-
- _sort_project_range(p_begin, p_end);
- for (int i = p_begin; i <= p_end; ++i) {
- _select_project_nocheck(i);
- }
-}
-
-void ProjectList::erase_selected_projects(bool p_delete_project_contents) {
- if (_selected_project_paths.size() == 0) {
- return;
- }
-
- for (int i = 0; i < _projects.size(); ++i) {
- Item &item = _projects.write[i];
- if (_selected_project_paths.has(item.path) && item.control->is_visible()) {
- _config.erase_section(item.path);
-
- // Comment out for now until we have a better warning system to
- // ensure users delete their project only.
- //if (p_delete_project_contents) {
- // OS::get_singleton()->move_to_trash(item.path);
- //}
-
- memdelete(item.control);
- _projects.remove_at(i);
- --i;
- }
- }
-
- save_config();
- _selected_project_paths.clear();
- _last_clicked = "";
-
- update_dock_menu();
-}
-
-// Input for each item in the list.
-void ProjectList::_panel_input(const Ref<InputEvent> &p_ev, Node *p_hb) {
- Ref<InputEventMouseButton> mb = p_ev;
- int clicked_index = p_hb->get_index();
- const Item &clicked_project = _projects[clicked_index];
-
- if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
- if (mb->is_shift_pressed() && _selected_project_paths.size() > 0 && !_last_clicked.is_empty() && clicked_project.path != _last_clicked) {
- int anchor_index = -1;
- for (int i = 0; i < _projects.size(); ++i) {
- const Item &p = _projects[i];
- if (p.path == _last_clicked) {
- anchor_index = p.control->get_index();
- break;
- }
- }
- CRASH_COND(anchor_index == -1);
- _select_project_range(anchor_index, clicked_index);
-
- } else if (mb->is_command_or_control_pressed()) {
- _toggle_project(clicked_index);
-
- } else {
- _last_clicked = clicked_project.path;
- select_project(clicked_index);
- }
-
- emit_signal(SNAME(SIGNAL_SELECTION_CHANGED));
-
- // Do not allow opening a project more than once using a single project manager instance.
- // Opening the same project in several editor instances at once can lead to various issues.
- if (!mb->is_command_or_control_pressed() && mb->is_double_click() && !project_opening_initiated) {
- emit_signal(SNAME(SIGNAL_PROJECT_ASK_OPEN));
- }
- }
-}
-
-void ProjectList::_favorite_pressed(Node *p_hb) {
- ProjectListItemControl *control = Object::cast_to<ProjectListItemControl>(p_hb);
-
- int index = control->get_index();
- Item item = _projects.write[index]; // Take copy
-
- item.favorite = !item.favorite;
-
- _config.set_value(item.path, "favorite", item.favorite);
- save_config();
-
- _projects.write[index] = item;
-
- control->set_is_favorite(item.favorite);
-
- sort_projects();
-
- if (item.favorite) {
- for (int i = 0; i < _projects.size(); ++i) {
- if (_projects[i].path == item.path) {
- ensure_project_visible(i);
- break;
- }
- }
- }
-
- update_dock_menu();
-}
-
-void ProjectList::_show_project(const String &p_path) {
- OS::get_singleton()->shell_show_in_file_manager(p_path, true);
-}
-
-void ProjectList::_bind_methods() {
- ADD_SIGNAL(MethodInfo(SIGNAL_LIST_CHANGED));
- ADD_SIGNAL(MethodInfo(SIGNAL_SELECTION_CHANGED));
- ADD_SIGNAL(MethodInfo(SIGNAL_PROJECT_ASK_OPEN));
-}
-
-ProjectList::ProjectList() {
- _scroll_children = memnew(VBoxContainer);
- _scroll_children->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- add_child(_scroll_children);
-
- _config_path = EditorPaths::get_singleton()->get_data_dir().path_join("projects.cfg");
- _migrate_config();
-}
-
-/// Project Manager.
-
ProjectManager *ProjectManager::singleton = nullptr;
+// Notifications.
+
void ProjectManager::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_TRANSLATION_CHANGED:
@@ -2102,6 +176,8 @@ void ProjectManager::_notification(int p_what) {
}
}
+// Utility data.
+
Ref<Texture2D> ProjectManager::_file_dialog_get_icon(const String &p_path) {
if (p_path.get_extension().to_lower() == "godot") {
return singleton->icon_type_cache["GodotMonochrome"];
@@ -2129,6 +205,8 @@ void ProjectManager::_build_icon_type_cache(Ref<Theme> p_theme) {
}
}
+// Main layout.
+
void ProjectManager::_update_size_limits() {
const Size2 minimum_size = Size2(680, 450) * EDSCALE;
const Size2 default_size = Size2(1024, 600) * EDSCALE;
@@ -2159,154 +237,116 @@ void ProjectManager::_update_size_limits() {
}
}
-void ProjectManager::_dim_window() {
- // This method must be called before calling `get_tree()->quit()`.
- // Otherwise, its effect won't be visible
-
- // Dim the project manager window while it's quitting to make it clearer that it's busy.
- // No transition is applied, as the effect needs to be visible immediately
- float c = 0.5f;
- Color dim_color = Color(c, c, c);
- set_modulate(dim_color);
+void ProjectManager::_show_about() {
+ about->popup_centered(Size2(780, 500) * EDSCALE);
}
-void ProjectManager::_update_project_buttons() {
- Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects();
- bool empty_selection = selected_projects.is_empty();
+void ProjectManager::_version_button_pressed() {
+ DisplayServer::get_singleton()->clipboard_set(version_btn->get_text());
+}
- bool is_missing_project_selected = false;
- for (int i = 0; i < selected_projects.size(); ++i) {
- if (selected_projects[i].missing) {
- is_missing_project_selected = true;
- break;
- }
+void ProjectManager::_on_tab_changed(int p_tab) {
+#ifndef ANDROID_ENABLED
+ if (p_tab == 0) { // Projects
+ // Automatically grab focus when the user moves from the Templates tab
+ // back to the Projects tab.
+ search_box->grab_focus();
}
- erase_btn->set_disabled(empty_selection);
- open_btn->set_disabled(empty_selection || is_missing_project_selected);
- rename_btn->set_disabled(empty_selection || is_missing_project_selected);
- manage_tags_btn->set_disabled(empty_selection || is_missing_project_selected || selected_projects.size() > 1);
- run_btn->set_disabled(empty_selection || is_missing_project_selected);
+ // The Templates tab's search field is focused on display in the asset
+ // library editor plugin code.
+#endif
+}
- erase_missing_btn->set_disabled(!_project_list->is_any_project_missing());
+void ProjectManager::_open_asset_library() {
+ asset_library->disable_community_support();
+ tabs->set_current_tab(1);
}
-void ProjectManager::shortcut_input(const Ref<InputEvent> &p_ev) {
- ERR_FAIL_COND(p_ev.is_null());
+// Quick settings.
- Ref<InputEventKey> k = p_ev;
+void ProjectManager::_language_selected(int p_id) {
+ String lang = language_btn->get_item_metadata(p_id);
+ EditorSettings::get_singleton()->set("interface/editor/editor_language", lang);
- if (k.is_valid()) {
- if (!k->is_pressed()) {
- return;
- }
+ restart_required_dialog->popup_centered();
+}
- // Pressing Command + Q quits the Project Manager
- // This is handled by the platform implementation on macOS,
- // so only define the shortcut on other platforms
-#ifndef MACOS_ENABLED
- if (k->get_keycode_with_modifiers() == (KeyModifierMask::META | Key::Q)) {
- _dim_window();
- get_tree()->quit();
- }
-#endif
+void ProjectManager::_restart_confirm() {
+ List<String> args = OS::get_singleton()->get_cmdline_args();
+ Error err = OS::get_singleton()->create_instance(args);
+ ERR_FAIL_COND(err);
- if (tabs->get_current_tab() != 0) {
- return;
- }
+ _dim_window();
+ get_tree()->quit();
+}
- bool keycode_handled = true;
+void ProjectManager::_dim_window() {
+ // This method must be called before calling `get_tree()->quit()`.
+ // Otherwise, its effect won't be visible
- switch (k->get_keycode()) {
- case Key::ENTER: {
- _open_selected_projects_ask();
- } break;
- case Key::HOME: {
- if (_project_list->get_project_count() > 0) {
- _project_list->select_project(0);
- _update_project_buttons();
- }
+ // Dim the project manager window while it's quitting to make it clearer that it's busy.
+ // No transition is applied, as the effect needs to be visible immediately
+ float c = 0.5f;
+ Color dim_color = Color(c, c, c);
+ set_modulate(dim_color);
+}
- } break;
- case Key::END: {
- if (_project_list->get_project_count() > 0) {
- _project_list->select_project(_project_list->get_project_count() - 1);
- _update_project_buttons();
- }
+// Project list.
- } break;
- case Key::UP: {
- if (k->is_shift_pressed()) {
- break;
- }
+void ProjectManager::_scan_projects() {
+ scan_dir->popup_file_dialog();
+}
- int index = _project_list->get_single_selected_index();
- if (index > 0) {
- _project_list->select_project(index - 1);
- _project_list->ensure_project_visible(index - 1);
- _update_project_buttons();
- }
+void ProjectManager::_run_project() {
+ const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
- break;
- }
- case Key::DOWN: {
- if (k->is_shift_pressed()) {
- break;
- }
+ if (selected_list.size() < 1) {
+ return;
+ }
- int index = _project_list->get_single_selected_index();
- if (index + 1 < _project_list->get_project_count()) {
- _project_list->select_project(index + 1);
- _project_list->ensure_project_visible(index + 1);
- _update_project_buttons();
- }
+ if (selected_list.size() > 1) {
+ multi_run_ask->set_text(vformat(TTR("Are you sure to run %d projects at once?"), selected_list.size()));
+ multi_run_ask->popup_centered();
+ } else {
+ _run_project_confirm();
+ }
+}
- } break;
- case Key::F: {
- if (k->is_command_or_control_pressed()) {
- this->search_box->grab_focus();
- } else {
- keycode_handled = false;
- }
- } break;
- default: {
- keycode_handled = false;
- } break;
+void ProjectManager::_run_project_confirm() {
+ Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects();
+
+ for (int i = 0; i < selected_list.size(); ++i) {
+ const String &selected_main = selected_list[i].main_scene;
+ if (selected_main.is_empty()) {
+ run_error_diag->set_text(TTR("Can't run project: no main scene defined.\nPlease edit the project and set the main scene in the Project Settings under the \"Application\" category."));
+ run_error_diag->popup_centered();
+ continue;
}
- if (keycode_handled) {
- accept_event();
+ const String &path = selected_list[i].path;
+
+ // `.substr(6)` on `ProjectSettings::get_singleton()->get_imported_files_path()` strips away the leading "res://".
+ if (!DirAccess::exists(path.path_join(ProjectSettings::get_singleton()->get_imported_files_path().substr(6)))) {
+ run_error_diag->set_text(TTR("Can't run project: Assets need to be imported.\nPlease edit the project to trigger the initial import."));
+ run_error_diag->popup_centered();
+ continue;
}
- }
-}
-void ProjectManager::_on_projects_updated() {
- Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects();
- int index = 0;
- for (int i = 0; i < selected_projects.size(); ++i) {
- index = _project_list->refresh_project(selected_projects[i].path);
- }
- if (index != -1) {
- _project_list->ensure_project_visible(index);
- }
+ print_line("Running project: " + path);
- _project_list->update_dock_menu();
-}
+ List<String> args;
-void ProjectManager::_on_project_created(const String &dir) {
- _project_list->add_project(dir, false);
- _project_list->save_config();
- search_box->clear();
- int i = _project_list->refresh_project(dir);
- _project_list->select_project(i);
- _project_list->ensure_project_visible(i);
- _open_selected_projects_ask();
+ for (const String &a : Main::get_forwardable_cli_arguments(Main::CLI_SCOPE_PROJECT)) {
+ args.push_back(a);
+ }
- _project_list->update_dock_menu();
-}
+ args.push_back("--path");
+ args.push_back(path);
-void ProjectManager::_confirm_update_settings() {
- _open_selected_projects();
+ Error err = OS::get_singleton()->create_instance(args);
+ ERR_FAIL_COND(err);
+ }
}
void ProjectManager::_open_selected_projects() {
@@ -2417,7 +457,7 @@ void ProjectManager::_open_selected_projects_ask() {
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");
unsupported_features.remove_at(i);
i--;
- } else if (_project_feature_looks_like_version(feature)) {
+ } else if (ProjectList::project_feature_looks_like_version(feature)) {
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));
unsupported_features.remove_at(i);
i--;
@@ -2438,113 +478,148 @@ void ProjectManager::_open_selected_projects_ask() {
_open_selected_projects();
}
-void ProjectManager::_full_convert_button_pressed() {
- ask_update_settings->hide();
- ask_full_convert_dialog->popup_centered(Size2i(600.0 * EDSCALE, 0));
- ask_full_convert_dialog->get_cancel_button()->grab_focus();
+void ProjectManager::_install_project(const String &p_zip_path, const String &p_title) {
+ npdialog->set_mode(ProjectDialog::MODE_INSTALL);
+ npdialog->set_zip_path(p_zip_path);
+ npdialog->set_zip_title(p_title);
+ npdialog->show_dialog();
}
-void ProjectManager::_perform_full_project_conversion() {
- Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects();
- if (selected_list.is_empty()) {
- return;
- }
+void ProjectManager::_import_project() {
+ npdialog->set_mode(ProjectDialog::MODE_IMPORT);
+ npdialog->ask_for_path_and_show();
+}
- const String &path = selected_list[0].path;
+void ProjectManager::_new_project() {
+ npdialog->set_mode(ProjectDialog::MODE_NEW);
+ npdialog->show_dialog();
+}
- print_line("Converting project: " + path);
- List<String> args;
- args.push_back("--path");
- args.push_back(path);
- args.push_back("--convert-3to4");
- args.push_back("--rendering-driver");
- args.push_back(Main::get_rendering_driver_name());
+void ProjectManager::_rename_project() {
+ const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
- Error err = OS::get_singleton()->create_instance(args);
- ERR_FAIL_COND(err);
+ if (selected_list.size() == 0) {
+ return;
+ }
- _project_list->set_project_version(path, GODOT4_CONFIG_VERSION);
+ for (const String &E : selected_list) {
+ npdialog->set_project_path(E);
+ npdialog->set_mode(ProjectDialog::MODE_RENAME);
+ npdialog->show_dialog();
+ }
}
-void ProjectManager::_run_project_confirm() {
- Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects();
+void ProjectManager::_erase_project() {
+ const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
- for (int i = 0; i < selected_list.size(); ++i) {
- const String &selected_main = selected_list[i].main_scene;
- if (selected_main.is_empty()) {
- run_error_diag->set_text(TTR("Can't run project: no main scene defined.\nPlease edit the project and set the main scene in the Project Settings under the \"Application\" category."));
- run_error_diag->popup_centered();
- continue;
- }
+ if (selected_list.size() == 0) {
+ return;
+ }
- const String &path = selected_list[i].path;
+ String confirm_message;
+ if (selected_list.size() >= 2) {
+ confirm_message = vformat(TTR("Remove %d projects from the list?"), selected_list.size());
+ } else {
+ confirm_message = TTR("Remove this project from the list?");
+ }
- // `.substr(6)` on `ProjectSettings::get_singleton()->get_imported_files_path()` strips away the leading "res://".
- if (!DirAccess::exists(path.path_join(ProjectSettings::get_singleton()->get_imported_files_path().substr(6)))) {
- run_error_diag->set_text(TTR("Can't run project: Assets need to be imported.\nPlease edit the project to trigger the initial import."));
- run_error_diag->popup_centered();
- continue;
- }
+ erase_ask_label->set_text(confirm_message);
+ //delete_project_contents->set_pressed(false);
+ erase_ask->popup_centered();
+}
- print_line("Running project: " + path);
+void ProjectManager::_erase_missing_projects() {
+ erase_missing_ask->set_text(TTR("Remove all missing projects from the list?\nThe project folders' contents won't be modified."));
+ erase_missing_ask->popup_centered();
+}
- List<String> args;
+void ProjectManager::_erase_project_confirm() {
+ _project_list->erase_selected_projects(false);
+ _update_project_buttons();
+}
- for (const String &a : Main::get_forwardable_cli_arguments(Main::CLI_SCOPE_PROJECT)) {
- args.push_back(a);
- }
+void ProjectManager::_erase_missing_projects_confirm() {
+ _project_list->erase_missing_projects();
+ _update_project_buttons();
+}
- args.push_back("--path");
- args.push_back(path);
+void ProjectManager::_update_project_buttons() {
+ Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects();
+ bool empty_selection = selected_projects.is_empty();
- Error err = OS::get_singleton()->create_instance(args);
- ERR_FAIL_COND(err);
+ bool is_missing_project_selected = false;
+ for (int i = 0; i < selected_projects.size(); ++i) {
+ if (selected_projects[i].missing) {
+ is_missing_project_selected = true;
+ break;
+ }
}
-}
-void ProjectManager::_run_project() {
- const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
+ erase_btn->set_disabled(empty_selection);
+ open_btn->set_disabled(empty_selection || is_missing_project_selected);
+ rename_btn->set_disabled(empty_selection || is_missing_project_selected);
+ manage_tags_btn->set_disabled(empty_selection || is_missing_project_selected || selected_projects.size() > 1);
+ run_btn->set_disabled(empty_selection || is_missing_project_selected);
- if (selected_list.size() < 1) {
- return;
- }
+ erase_missing_btn->set_disabled(!_project_list->is_any_project_missing());
+}
- if (selected_list.size() > 1) {
- multi_run_ask->set_text(vformat(TTR("Are you sure to run %d projects at once?"), selected_list.size()));
- multi_run_ask->popup_centered();
- } else {
- _run_project_confirm();
+void ProjectManager::_on_projects_updated() {
+ Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects();
+ int index = 0;
+ for (int i = 0; i < selected_projects.size(); ++i) {
+ index = _project_list->refresh_project(selected_projects[i].path);
+ }
+ if (index != -1) {
+ _project_list->ensure_project_visible(index);
}
-}
-void ProjectManager::_scan_projects() {
- scan_dir->popup_file_dialog();
+ _project_list->update_dock_menu();
}
-void ProjectManager::_new_project() {
- npdialog->set_mode(ProjectDialog::MODE_NEW);
- npdialog->show_dialog();
+void ProjectManager::_on_project_created(const String &dir) {
+ _project_list->add_project(dir, false);
+ _project_list->save_config();
+ search_box->clear();
+ int i = _project_list->refresh_project(dir);
+ _project_list->select_project(i);
+ _project_list->ensure_project_visible(i);
+ _open_selected_projects_ask();
+
+ _project_list->update_dock_menu();
}
-void ProjectManager::_import_project() {
- npdialog->set_mode(ProjectDialog::MODE_IMPORT);
- npdialog->ask_for_path_and_show();
+void ProjectManager::_on_order_option_changed(int p_idx) {
+ if (is_inside_tree()) {
+ _project_list->set_order_option(p_idx);
+ }
}
-void ProjectManager::_rename_project() {
- const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
+void ProjectManager::_on_search_term_changed(const String &p_term) {
+ _project_list->set_search_term(p_term);
+ _project_list->sort_projects();
- if (selected_list.size() == 0) {
+ // Select the first visible project in the list.
+ // This makes it possible to open a project without ever touching the mouse,
+ // as the search field is automatically focused on startup.
+ _project_list->select_first_visible_project();
+ _update_project_buttons();
+}
+
+void ProjectManager::_on_search_term_submitted(const String &p_text) {
+ if (tabs->get_current_tab() != 0) {
return;
}
- for (const String &E : selected_list) {
- npdialog->set_project_path(E);
- npdialog->set_mode(ProjectDialog::MODE_RENAME);
- npdialog->show_dialog();
- }
+ _open_selected_projects_ask();
}
+LineEdit *ProjectManager::get_search_box() {
+ return search_box;
+}
+
+// Project tag management.
+
void ProjectManager::_manage_project_tags() {
for (int i = 0; i < project_tags->get_child_count(); i++) {
project_tags->get_child(i)->queue_free();
@@ -2653,66 +728,135 @@ void ProjectManager::_create_new_tag() {
_add_project_tag(new_tag_name->get_text());
}
-void ProjectManager::_erase_project_confirm() {
- _project_list->erase_selected_projects(false);
- _update_project_buttons();
+void ProjectManager::add_new_tag(const String &p_tag) {
+ if (!tag_set.has(p_tag)) {
+ tag_set.insert(p_tag);
+ ProjectTag *tag_control = memnew(ProjectTag(p_tag));
+ all_tags->add_child(tag_control);
+ all_tags->move_child(tag_control, -2);
+ tag_control->connect_button_to(callable_mp(this, &ProjectManager::_add_project_tag).bind(p_tag));
+ }
}
-void ProjectManager::_erase_missing_projects_confirm() {
- _project_list->erase_missing_projects();
- _update_project_buttons();
-}
+// Project converter/migration tool.
-void ProjectManager::_erase_project() {
- const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
+void ProjectManager::_full_convert_button_pressed() {
+ ask_update_settings->hide();
+ ask_full_convert_dialog->popup_centered(Size2i(600.0 * EDSCALE, 0));
+ ask_full_convert_dialog->get_cancel_button()->grab_focus();
+}
- if (selected_list.size() == 0) {
+void ProjectManager::_perform_full_project_conversion() {
+ Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects();
+ if (selected_list.is_empty()) {
return;
}
- String confirm_message;
- if (selected_list.size() >= 2) {
- confirm_message = vformat(TTR("Remove %d projects from the list?"), selected_list.size());
- } else {
- confirm_message = TTR("Remove this project from the list?");
- }
+ const String &path = selected_list[0].path;
- erase_ask_label->set_text(confirm_message);
- //delete_project_contents->set_pressed(false);
- erase_ask->popup_centered();
-}
+ print_line("Converting project: " + path);
+ List<String> args;
+ args.push_back("--path");
+ args.push_back(path);
+ args.push_back("--convert-3to4");
+ args.push_back("--rendering-driver");
+ args.push_back(Main::get_rendering_driver_name());
-void ProjectManager::_erase_missing_projects() {
- erase_missing_ask->set_text(TTR("Remove all missing projects from the list?\nThe project folders' contents won't be modified."));
- erase_missing_ask->popup_centered();
-}
+ Error err = OS::get_singleton()->create_instance(args);
+ ERR_FAIL_COND(err);
-void ProjectManager::_show_about() {
- about->popup_centered(Size2(780, 500) * EDSCALE);
+ _project_list->set_project_version(path, GODOT4_CONFIG_VERSION);
}
-void ProjectManager::_language_selected(int p_id) {
- String lang = language_btn->get_item_metadata(p_id);
- EditorSettings::get_singleton()->set("interface/editor/editor_language", lang);
+// Input and I/O.
- language_restart_ask->set_text(TTR("Language changed.\nThe interface will update after restarting the editor or project manager."));
- language_restart_ask->popup_centered();
-}
+void ProjectManager::shortcut_input(const Ref<InputEvent> &p_ev) {
+ ERR_FAIL_COND(p_ev.is_null());
-void ProjectManager::_restart_confirm() {
- List<String> args = OS::get_singleton()->get_cmdline_args();
- Error err = OS::get_singleton()->create_instance(args);
- ERR_FAIL_COND(err);
+ Ref<InputEventKey> k = p_ev;
- _dim_window();
- get_tree()->quit();
-}
+ if (k.is_valid()) {
+ if (!k->is_pressed()) {
+ return;
+ }
-void ProjectManager::_install_project(const String &p_zip_path, const String &p_title) {
- npdialog->set_mode(ProjectDialog::MODE_INSTALL);
- npdialog->set_zip_path(p_zip_path);
- npdialog->set_zip_title(p_title);
- npdialog->show_dialog();
+ // Pressing Command + Q quits the Project Manager
+ // This is handled by the platform implementation on macOS,
+ // so only define the shortcut on other platforms
+#ifndef MACOS_ENABLED
+ if (k->get_keycode_with_modifiers() == (KeyModifierMask::META | Key::Q)) {
+ _dim_window();
+ get_tree()->quit();
+ }
+#endif
+
+ if (tabs->get_current_tab() != 0) {
+ return;
+ }
+
+ bool keycode_handled = true;
+
+ switch (k->get_keycode()) {
+ case Key::ENTER: {
+ _open_selected_projects_ask();
+ } break;
+ case Key::HOME: {
+ if (_project_list->get_project_count() > 0) {
+ _project_list->select_project(0);
+ _update_project_buttons();
+ }
+
+ } break;
+ case Key::END: {
+ if (_project_list->get_project_count() > 0) {
+ _project_list->select_project(_project_list->get_project_count() - 1);
+ _update_project_buttons();
+ }
+
+ } break;
+ case Key::UP: {
+ if (k->is_shift_pressed()) {
+ break;
+ }
+
+ int index = _project_list->get_single_selected_index();
+ if (index > 0) {
+ _project_list->select_project(index - 1);
+ _project_list->ensure_project_visible(index - 1);
+ _update_project_buttons();
+ }
+
+ break;
+ }
+ case Key::DOWN: {
+ if (k->is_shift_pressed()) {
+ break;
+ }
+
+ int index = _project_list->get_single_selected_index();
+ if (index + 1 < _project_list->get_project_count()) {
+ _project_list->select_project(index + 1);
+ _project_list->ensure_project_visible(index + 1);
+ _update_project_buttons();
+ }
+
+ } break;
+ case Key::F: {
+ if (k->is_command_or_control_pressed()) {
+ this->search_box->grab_focus();
+ } else {
+ keycode_handled = false;
+ }
+ } break;
+ default: {
+ keycode_handled = false;
+ } break;
+ }
+
+ if (keycode_handled) {
+ accept_event();
+ }
+ }
}
void ProjectManager::_files_dropped(PackedStringArray p_files) {
@@ -2738,99 +882,23 @@ void ProjectManager::_files_dropped(PackedStringArray p_files) {
_project_list->find_projects_multiple(folders);
}
-void ProjectManager::_on_order_option_changed(int p_idx) {
- if (is_inside_tree()) {
- _project_list->set_order_option(p_idx);
- }
-}
-
-void ProjectManager::_on_tab_changed(int p_tab) {
-#ifndef ANDROID_ENABLED
- if (p_tab == 0) { // Projects
- // Automatically grab focus when the user moves from the Templates tab
- // back to the Projects tab.
- search_box->grab_focus();
- }
-
- // The Templates tab's search field is focused on display in the asset
- // library editor plugin code.
-#endif
-}
-
-void ProjectManager::_on_search_term_changed(const String &p_term) {
- _project_list->set_search_term(p_term);
- _project_list->sort_projects();
-
- // Select the first visible project in the list.
- // This makes it possible to open a project without ever touching the mouse,
- // as the search field is automatically focused on startup.
- _project_list->select_first_visible_project();
- _update_project_buttons();
-}
-
-void ProjectManager::_on_search_term_submitted(const String &p_text) {
- if (tabs->get_current_tab() != 0) {
- return;
- }
-
- _open_selected_projects_ask();
-}
-
-void ProjectManager::_open_asset_library() {
- asset_library->disable_community_support();
- tabs->set_current_tab(1);
-}
-
-void ProjectManager::_version_button_pressed() {
- DisplayServer::get_singleton()->clipboard_set(version_btn->get_text());
-}
-
-LineEdit *ProjectManager::get_search_box() {
- return search_box;
-}
-
-void ProjectManager::add_new_tag(const String &p_tag) {
- if (!tag_set.has(p_tag)) {
- tag_set.insert(p_tag);
- ProjectTag *tag_control = memnew(ProjectTag(p_tag));
- all_tags->add_child(tag_control);
- all_tags->move_child(tag_control, -2);
- tag_control->connect_button_to(callable_mp(this, &ProjectManager::_add_project_tag).bind(p_tag));
- }
-}
-
-void ProjectList::add_search_tag(const String &p_tag) {
- const String tag_string = "tag:" + p_tag;
-
- int exists = _search_term.find(tag_string);
- if (exists > -1) {
- _search_term = _search_term.erase(exists, tag_string.length() + 1);
- } else if (_search_term.is_empty() || _search_term.ends_with(" ")) {
- _search_term += tag_string;
- } else {
- _search_term += " " + tag_string;
- }
- ProjectManager::get_singleton()->get_search_box()->set_text(_search_term);
-
- sort_projects();
-}
+// Object methods.
ProjectManager::ProjectManager() {
singleton = this;
- // load settings
- if (!EditorSettings::get_singleton()) {
- EditorSettings::create();
- }
-
// Turn off some servers we aren't going to be using in the Project Manager.
NavigationServer3D::get_singleton()->set_active(false);
PhysicsServer3D::get_singleton()->set_active(false);
PhysicsServer2D::get_singleton()->set_active(false);
- EditorSettings::get_singleton()->set_optimize_save(false); //just write settings as they came
-
+ // Initialize settings.
{
+ if (!EditorSettings::get_singleton()) {
+ EditorSettings::create();
+ }
+ EditorSettings::get_singleton()->set_optimize_save(false); // Just write settings as they come.
+
int display_scale = EDITOR_GET("interface/editor/display_scale");
switch (display_scale) {
@@ -2862,30 +930,41 @@ ProjectManager::ProjectManager() {
}
EditorFileDialog::get_icon_func = &ProjectManager::_file_dialog_get_icon;
EditorFileDialog::get_thumbnail_func = &ProjectManager::_file_dialog_get_thumbnail;
+
+ EditorFileDialog::set_default_show_hidden_files(EDITOR_GET("filesystem/file_dialog/show_hidden_files"));
+ EditorFileDialog::set_default_display_mode((EditorFileDialog::DisplayMode)EDITOR_GET("filesystem/file_dialog/display_mode").operator int());
+
+ int swap_cancel_ok = EDITOR_GET("interface/editor/accept_dialog_cancel_ok_buttons");
+ if (swap_cancel_ok != 0) { // 0 is auto, set in register_scene based on DisplayServer.
+ // Swap on means OK first.
+ AcceptDialog::set_swap_cancel_ok(swap_cancel_ok == 2);
+ }
+
+ OS::get_singleton()->set_low_processor_usage_mode(true);
}
// TRANSLATORS: This refers to the application where users manage their Godot projects.
DisplayServer::get_singleton()->window_set_title(VERSION_NAME + String(" - ") + TTR("Project Manager", "Application"));
- EditorFileDialog::set_default_show_hidden_files(EDITOR_GET("filesystem/file_dialog/show_hidden_files"));
- EditorFileDialog::set_default_display_mode((EditorFileDialog::DisplayMode)EDITOR_GET("filesystem/file_dialog/display_mode").operator int());
+ SceneTree::get_singleton()->get_root()->connect("files_dropped", callable_mp(this, &ProjectManager::_files_dropped));
- int swap_cancel_ok = EDITOR_GET("interface/editor/accept_dialog_cancel_ok_buttons");
- if (swap_cancel_ok != 0) { // 0 is auto, set in register_scene based on DisplayServer.
- // Swap on means OK first.
- AcceptDialog::set_swap_cancel_ok(swap_cancel_ok == 2);
- }
+ // Initialize UI.
+ {
+ int pm_root_dir = EDITOR_GET("interface/editor/ui_layout_direction");
+ Control::set_root_layout_direction(pm_root_dir);
+ Window::set_root_layout_direction(pm_root_dir);
- int pm_root_dir = EDITOR_GET("interface/editor/ui_layout_direction");
- Control::set_root_layout_direction(pm_root_dir);
- Window::set_root_layout_direction(pm_root_dir);
+ EditorThemeManager::initialize();
+ Ref<Theme> theme = EditorThemeManager::generate_theme();
+ DisplayServer::set_early_window_clear_color_override(true, theme->get_color(SNAME("background"), EditorStringName(Editor)));
- EditorThemeManager::initialize();
- Ref<Theme> theme = EditorThemeManager::generate_theme();
- DisplayServer::set_early_window_clear_color_override(true, theme->get_color(SNAME("background"), EditorStringName(Editor)));
+ set_theme(theme);
+ set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
- set_theme(theme);
- set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
+ _build_icon_type_cache(theme);
+ }
+
+ // Project manager layout.
background_panel = memnew(Panel);
add_child(background_panel);
@@ -2904,139 +983,8 @@ ProjectManager::ProjectManager() {
center_box->add_child(tabs);
tabs->connect("tab_changed", callable_mp(this, &ProjectManager::_on_tab_changed));
- local_projects_vb = memnew(VBoxContainer);
- local_projects_vb->set_name(TTR("Local Projects"));
- tabs->add_child(local_projects_vb);
-
+ // Quick settings.
{
- // A bar at top with buttons and options.
- HBoxContainer *hb = memnew(HBoxContainer);
- hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- local_projects_vb->add_child(hb);
-
- create_btn = memnew(Button);
- create_btn->set_text(TTR("New"));
- create_btn->set_shortcut(ED_SHORTCUT("project_manager/new_project", TTR("New Project"), KeyModifierMask::CMD_OR_CTRL | Key::N));
- create_btn->connect("pressed", callable_mp(this, &ProjectManager::_new_project));
- hb->add_child(create_btn);
-
- import_btn = memnew(Button);
- import_btn->set_text(TTR("Import"));
- import_btn->set_shortcut(ED_SHORTCUT("project_manager/import_project", TTR("Import Project"), KeyModifierMask::CMD_OR_CTRL | Key::I));
- import_btn->connect("pressed", callable_mp(this, &ProjectManager::_import_project));
- hb->add_child(import_btn);
-
- scan_btn = memnew(Button);
- scan_btn->set_text(TTR("Scan"));
- scan_btn->set_shortcut(ED_SHORTCUT("project_manager/scan_projects", TTR("Scan Projects"), KeyModifierMask::CMD_OR_CTRL | Key::S));
- scan_btn->connect("pressed", callable_mp(this, &ProjectManager::_scan_projects));
- hb->add_child(scan_btn);
-
- loading_label = memnew(Label(TTR("Loading, please wait...")));
- loading_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- hb->add_child(loading_label);
- // The loading label is shown later.
- loading_label->hide();
-
- search_box = memnew(LineEdit);
- search_box->set_placeholder(TTR("Filter Projects"));
- 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."));
- search_box->set_clear_button_enabled(true);
- search_box->connect("text_changed", callable_mp(this, &ProjectManager::_on_search_term_changed));
- search_box->connect("text_submitted", callable_mp(this, &ProjectManager::_on_search_term_submitted));
- search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- hb->add_child(search_box);
-
- Label *sort_label = memnew(Label);
- sort_label->set_text(TTR("Sort:"));
- hb->add_child(sort_label);
-
- filter_option = memnew(OptionButton);
- filter_option->set_clip_text(true);
- filter_option->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- filter_option->set_stretch_ratio(0.3);
- filter_option->connect("item_selected", callable_mp(this, &ProjectManager::_on_order_option_changed));
- hb->add_child(filter_option);
-
- Vector<String> sort_filter_titles;
- sort_filter_titles.push_back(TTR("Last Edited"));
- sort_filter_titles.push_back(TTR("Name"));
- sort_filter_titles.push_back(TTR("Path"));
- sort_filter_titles.push_back(TTR("Tags"));
-
- for (int i = 0; i < sort_filter_titles.size(); i++) {
- filter_option->add_item(sort_filter_titles[i]);
- }
- }
-
- {
- // A container for the project list and for the side bar with buttons.
- HBoxContainer *search_tree_hb = memnew(HBoxContainer);
- local_projects_vb->add_child(search_tree_hb);
- search_tree_hb->set_v_size_flags(Control::SIZE_EXPAND_FILL);
-
- search_panel = memnew(PanelContainer);
- search_panel->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- search_tree_hb->add_child(search_panel);
-
- _project_list = memnew(ProjectList);
- _project_list->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
- search_panel->add_child(_project_list);
- _project_list->connect(ProjectList::SIGNAL_LIST_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
- _project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
- _project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask));
-
- // The side bar with the edit, run, rename, etc. buttons.
- VBoxContainer *tree_vb = memnew(VBoxContainer);
- tree_vb->set_custom_minimum_size(Size2(120, 120));
- search_tree_hb->add_child(tree_vb);
-
- tree_vb->add_child(memnew(HSeparator));
-
- open_btn = memnew(Button);
- open_btn->set_text(TTR("Edit"));
- open_btn->set_shortcut(ED_SHORTCUT("project_manager/edit_project", TTR("Edit Project"), KeyModifierMask::CMD_OR_CTRL | Key::E));
- open_btn->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects_ask));
- tree_vb->add_child(open_btn);
-
- run_btn = memnew(Button);
- run_btn->set_text(TTR("Run"));
- run_btn->set_shortcut(ED_SHORTCUT("project_manager/run_project", TTR("Run Project"), KeyModifierMask::CMD_OR_CTRL | Key::R));
- run_btn->connect("pressed", callable_mp(this, &ProjectManager::_run_project));
- tree_vb->add_child(run_btn);
-
- rename_btn = memnew(Button);
- rename_btn->set_text(TTR("Rename"));
- // The F2 shortcut isn't overridden with Enter on macOS as Enter is already used to edit a project.
- rename_btn->set_shortcut(ED_SHORTCUT("project_manager/rename_project", TTR("Rename Project"), Key::F2));
- rename_btn->connect("pressed", callable_mp(this, &ProjectManager::_rename_project));
- tree_vb->add_child(rename_btn);
-
- manage_tags_btn = memnew(Button);
- manage_tags_btn->set_text(TTR("Manage Tags"));
- tree_vb->add_child(manage_tags_btn);
-
- erase_btn = memnew(Button);
- erase_btn->set_text(TTR("Remove"));
- erase_btn->set_shortcut(ED_SHORTCUT("project_manager/remove_project", TTR("Remove Project"), Key::KEY_DELETE));
- erase_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_project));
- tree_vb->add_child(erase_btn);
-
- erase_missing_btn = memnew(Button);
- erase_missing_btn->set_text(TTR("Remove Missing"));
- erase_missing_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects));
- tree_vb->add_child(erase_missing_btn);
-
- tree_vb->add_spacer();
-
- about_btn = memnew(Button);
- about_btn->set_text(TTR("About"));
- about_btn->connect("pressed", callable_mp(this, &ProjectManager::_show_about));
- tree_vb->add_child(about_btn);
- }
-
- {
- // Version info and language options
settings_hb = memnew(HBoxContainer);
settings_hb->set_alignment(BoxContainer::ALIGNMENT_END);
settings_hb->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN);
@@ -3106,6 +1054,140 @@ ProjectManager::ProjectManager() {
center_box->add_child(settings_hb);
}
+ // Project list view.
+ {
+ local_projects_vb = memnew(VBoxContainer);
+ local_projects_vb->set_name(TTR("Local Projects"));
+ tabs->add_child(local_projects_vb);
+
+ // Project list's top bar.
+ {
+ HBoxContainer *hb = memnew(HBoxContainer);
+ hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ local_projects_vb->add_child(hb);
+
+ create_btn = memnew(Button);
+ create_btn->set_text(TTR("New"));
+ create_btn->set_shortcut(ED_SHORTCUT("project_manager/new_project", TTR("New Project"), KeyModifierMask::CMD_OR_CTRL | Key::N));
+ create_btn->connect("pressed", callable_mp(this, &ProjectManager::_new_project));
+ hb->add_child(create_btn);
+
+ import_btn = memnew(Button);
+ import_btn->set_text(TTR("Import"));
+ import_btn->set_shortcut(ED_SHORTCUT("project_manager/import_project", TTR("Import Project"), KeyModifierMask::CMD_OR_CTRL | Key::I));
+ import_btn->connect("pressed", callable_mp(this, &ProjectManager::_import_project));
+ hb->add_child(import_btn);
+
+ scan_btn = memnew(Button);
+ scan_btn->set_text(TTR("Scan"));
+ scan_btn->set_shortcut(ED_SHORTCUT("project_manager/scan_projects", TTR("Scan Projects"), KeyModifierMask::CMD_OR_CTRL | Key::S));
+ scan_btn->connect("pressed", callable_mp(this, &ProjectManager::_scan_projects));
+ hb->add_child(scan_btn);
+
+ loading_label = memnew(Label(TTR("Loading, please wait...")));
+ loading_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ hb->add_child(loading_label);
+ // The loading label is shown later.
+ loading_label->hide();
+
+ search_box = memnew(LineEdit);
+ search_box->set_placeholder(TTR("Filter Projects"));
+ 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."));
+ search_box->set_clear_button_enabled(true);
+ search_box->connect("text_changed", callable_mp(this, &ProjectManager::_on_search_term_changed));
+ search_box->connect("text_submitted", callable_mp(this, &ProjectManager::_on_search_term_submitted));
+ search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ hb->add_child(search_box);
+
+ Label *sort_label = memnew(Label);
+ sort_label->set_text(TTR("Sort:"));
+ hb->add_child(sort_label);
+
+ filter_option = memnew(OptionButton);
+ filter_option->set_clip_text(true);
+ filter_option->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ filter_option->set_stretch_ratio(0.3);
+ filter_option->connect("item_selected", callable_mp(this, &ProjectManager::_on_order_option_changed));
+ hb->add_child(filter_option);
+
+ Vector<String> sort_filter_titles;
+ sort_filter_titles.push_back(TTR("Last Edited"));
+ sort_filter_titles.push_back(TTR("Name"));
+ sort_filter_titles.push_back(TTR("Path"));
+ sort_filter_titles.push_back(TTR("Tags"));
+
+ for (int i = 0; i < sort_filter_titles.size(); i++) {
+ filter_option->add_item(sort_filter_titles[i]);
+ }
+ }
+
+ // Project list and its sidebar.
+ {
+ HBoxContainer *search_tree_hb = memnew(HBoxContainer);
+ local_projects_vb->add_child(search_tree_hb);
+ search_tree_hb->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+
+ search_panel = memnew(PanelContainer);
+ search_panel->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ search_tree_hb->add_child(search_panel);
+
+ _project_list = memnew(ProjectList);
+ _project_list->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
+ search_panel->add_child(_project_list);
+ _project_list->connect(ProjectList::SIGNAL_LIST_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
+ _project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
+ _project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask));
+
+ // The side bar with the edit, run, rename, etc. buttons.
+ VBoxContainer *tree_vb = memnew(VBoxContainer);
+ tree_vb->set_custom_minimum_size(Size2(120, 120));
+ search_tree_hb->add_child(tree_vb);
+
+ tree_vb->add_child(memnew(HSeparator));
+
+ open_btn = memnew(Button);
+ open_btn->set_text(TTR("Edit"));
+ open_btn->set_shortcut(ED_SHORTCUT("project_manager/edit_project", TTR("Edit Project"), KeyModifierMask::CMD_OR_CTRL | Key::E));
+ open_btn->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects_ask));
+ tree_vb->add_child(open_btn);
+
+ run_btn = memnew(Button);
+ run_btn->set_text(TTR("Run"));
+ run_btn->set_shortcut(ED_SHORTCUT("project_manager/run_project", TTR("Run Project"), KeyModifierMask::CMD_OR_CTRL | Key::R));
+ run_btn->connect("pressed", callable_mp(this, &ProjectManager::_run_project));
+ tree_vb->add_child(run_btn);
+
+ rename_btn = memnew(Button);
+ rename_btn->set_text(TTR("Rename"));
+ // The F2 shortcut isn't overridden with Enter on macOS as Enter is already used to edit a project.
+ rename_btn->set_shortcut(ED_SHORTCUT("project_manager/rename_project", TTR("Rename Project"), Key::F2));
+ rename_btn->connect("pressed", callable_mp(this, &ProjectManager::_rename_project));
+ tree_vb->add_child(rename_btn);
+
+ manage_tags_btn = memnew(Button);
+ manage_tags_btn->set_text(TTR("Manage Tags"));
+ tree_vb->add_child(manage_tags_btn);
+
+ erase_btn = memnew(Button);
+ erase_btn->set_text(TTR("Remove"));
+ erase_btn->set_shortcut(ED_SHORTCUT("project_manager/remove_project", TTR("Remove Project"), Key::KEY_DELETE));
+ erase_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_project));
+ tree_vb->add_child(erase_btn);
+
+ erase_missing_btn = memnew(Button);
+ erase_missing_btn->set_text(TTR("Remove Missing"));
+ erase_missing_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects));
+ tree_vb->add_child(erase_missing_btn);
+
+ tree_vb->add_spacer();
+
+ about_btn = memnew(Button);
+ about_btn->set_text(TTR("About"));
+ about_btn->connect("pressed", callable_mp(this, &ProjectManager::_show_about));
+ tree_vb->add_child(about_btn);
+ }
+ }
+
if (AssetLibraryEditorPlugin::is_available()) {
asset_library = memnew(EditorAssetLibrary(true));
asset_library->set_name(TTR("Asset Library Projects"));
@@ -3115,13 +1197,14 @@ ProjectManager::ProjectManager() {
print_verbose("Asset Library not available (due to using Web editor, or SSL support disabled).");
}
+ // Dialogs.
{
- // Dialogs
- language_restart_ask = memnew(ConfirmationDialog);
- language_restart_ask->set_ok_button_text(TTR("Restart Now"));
- language_restart_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_restart_confirm));
- language_restart_ask->set_cancel_button_text(TTR("Continue"));
- add_child(language_restart_ask);
+ restart_required_dialog = memnew(ConfirmationDialog);
+ restart_required_dialog->set_ok_button_text(TTR("Restart Now"));
+ restart_required_dialog->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_restart_confirm));
+ restart_required_dialog->set_cancel_button_text(TTR("Continue"));
+ restart_required_dialog->set_text(TTR("Settings changed!\nThe project manager must be restarted for changes to take effect."));
+ add_child(restart_required_dialog);
scan_dir = memnew(EditorFileDialog);
scan_dir->set_previews_enabled(false);
@@ -3166,7 +1249,7 @@ ProjectManager::ProjectManager() {
ask_update_settings = memnew(ConfirmationDialog);
ask_update_settings->set_autowrap(true);
- ask_update_settings->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_confirm_update_settings));
+ ask_update_settings->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects));
full_convert_button = ask_update_settings->add_button(TTR("Convert Full Project"), !GLOBAL_GET("gui/common/swap_cancel_ok"));
full_convert_button->connect("pressed", callable_mp(this, &ProjectManager::_full_convert_button_pressed));
add_child(ask_update_settings);
@@ -3199,12 +1282,10 @@ ProjectManager::ProjectManager() {
about = memnew(EditorAbout);
add_child(about);
-
- _build_icon_type_cache(get_theme());
}
+ // Tag management.
{
- // Tag management.
tag_manage_dialog = memnew(ConfirmationDialog);
add_child(tag_manage_dialog);
tag_manage_dialog->set_title(TTR("Manage Project Tags"));
@@ -3304,10 +1385,6 @@ ProjectManager::ProjectManager() {
}
}
- SceneTree::get_singleton()->get_root()->connect("files_dropped", callable_mp(this, &ProjectManager::_files_dropped));
-
- OS::get_singleton()->set_low_processor_usage_mode(true);
-
_update_size_limits();
}
@@ -3319,42 +1396,3 @@ ProjectManager::~ProjectManager() {
EditorThemeManager::finalize();
}
-
-void ProjectTag::_notification(int p_what) {
- if (display_close && p_what == NOTIFICATION_THEME_CHANGED) {
- button->set_icon(get_theme_icon(SNAME("close"), SNAME("TabBar")));
- }
-}
-
-ProjectTag::ProjectTag(const String &p_text, bool p_display_close) {
- add_theme_constant_override(SNAME("separation"), 0);
- set_v_size_flags(SIZE_SHRINK_CENTER);
- tag_string = p_text;
- display_close = p_display_close;
-
- Color tag_color = Color(1, 0, 0);
- tag_color.set_ok_hsl_s(0.8);
- tag_color.set_ok_hsl_h(float(p_text.hash() * 10001 % UINT32_MAX) / float(UINT32_MAX));
- set_self_modulate(tag_color);
-
- ColorRect *cr = memnew(ColorRect);
- add_child(cr);
- cr->set_custom_minimum_size(Vector2(4, 0) * EDSCALE);
- cr->set_color(tag_color);
-
- button = memnew(Button);
- add_child(button);
- button->set_auto_translate(false);
- button->set_text(p_text.capitalize());
- button->set_focus_mode(FOCUS_NONE);
- button->set_icon_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
- button->set_theme_type_variation(SNAME("ProjectTag"));
-}
-
-void ProjectTag::connect_button_to(const Callable &p_callable) {
- button->connect(SNAME("pressed"), p_callable, CONNECT_DEFERRED);
-}
-
-const String ProjectTag::get_tag() const {
- return tag_string;
-}
diff --git a/editor/project_manager.h b/editor/project_manager.h
index 7b091050bd..7ed8df8a9d 100644
--- a/editor/project_manager.h
+++ b/editor/project_manager.h
@@ -31,315 +31,68 @@
#ifndef PROJECT_MANAGER_H
#define PROJECT_MANAGER_H
-#include "core/io/config_file.h"
-#include "editor/editor_about.h"
#include "scene/gui/dialogs.h"
-#include "scene/gui/file_dialog.h"
#include "scene/gui/scroll_container.h"
class CheckBox;
+class EditorAbout;
class EditorAssetLibrary;
class EditorFileDialog;
class HFlowContainer;
+class LineEdit;
+class LinkButton;
+class OptionButton;
class PanelContainer;
+class ProjectDialog;
class ProjectList;
+class TabContainer;
-class ProjectDialog : public ConfirmationDialog {
- GDCLASS(ProjectDialog, ConfirmationDialog);
-
-public:
- enum Mode {
- MODE_NEW,
- MODE_IMPORT,
- MODE_INSTALL,
- MODE_RENAME,
- };
-
-private:
- enum MessageType {
- MESSAGE_ERROR,
- MESSAGE_WARNING,
- MESSAGE_SUCCESS,
- };
-
- enum InputType {
- PROJECT_PATH,
- INSTALL_PATH,
- };
-
- Mode mode = MODE_NEW;
- bool is_folder_empty = true;
-
- Button *browse = nullptr;
- Button *install_browse = nullptr;
- Button *create_dir = nullptr;
- Container *name_container = nullptr;
- Container *path_container = nullptr;
- Container *install_path_container = nullptr;
-
- Container *renderer_container = nullptr;
- Label *renderer_info = nullptr;
- HBoxContainer *default_files_container = nullptr;
- Ref<ButtonGroup> renderer_button_group;
-
- Label *msg = nullptr;
- LineEdit *project_path = nullptr;
- LineEdit *project_name = nullptr;
- LineEdit *install_path = nullptr;
- TextureRect *status_rect = nullptr;
- TextureRect *install_status_rect = nullptr;
-
- OptionButton *vcs_metadata_selection = nullptr;
-
- EditorFileDialog *fdialog = nullptr;
- EditorFileDialog *fdialog_install = nullptr;
- AcceptDialog *dialog_error = nullptr;
-
- String zip_path;
- String zip_title;
- String fav_dir;
-
- String created_folder_path;
-
- void _set_message(const String &p_msg, MessageType p_type = MESSAGE_SUCCESS, InputType input_type = PROJECT_PATH);
-
- String _test_path();
- void _update_path(const String &p_path);
- void _path_text_changed(const String &p_path);
- void _path_selected(const String &p_path);
- void _file_selected(const String &p_path);
- void _install_path_selected(const String &p_path);
-
- void _browse_path();
- void _browse_install_path();
- void _create_folder();
-
- void _text_changed(const String &p_text);
- void _nonempty_confirmation_ok_pressed();
- void _renderer_selected();
- void _remove_created_folder();
-
- void ok_pressed() override;
- void cancel_pressed() override;
-
-protected:
- void _notification(int p_what);
- static void _bind_methods();
-
-public:
- void set_zip_path(const String &p_path);
- void set_zip_title(const String &p_title);
- void set_mode(Mode p_mode);
- void set_project_path(const String &p_path);
-
- void ask_for_path_and_show();
- void show_dialog();
-
- ProjectDialog();
-};
-
-class ProjectListItemControl : public HBoxContainer {
- GDCLASS(ProjectListItemControl, HBoxContainer)
-
- VBoxContainer *main_vbox = nullptr;
- TextureButton *favorite_button = nullptr;
- Button *explore_button = nullptr;
-
- TextureRect *project_icon = nullptr;
- Label *project_title = nullptr;
- Label *project_path = nullptr;
- TextureRect *project_unsupported_features = nullptr;
- HBoxContainer *tag_container = nullptr;
-
- bool project_is_missing = false;
- bool icon_needs_reload = true;
- bool is_selected = false;
- bool is_hovering = false;
-
- void _favorite_button_pressed();
- void _explore_button_pressed();
-
-protected:
- void _notification(int p_what);
- static void _bind_methods();
-
-public:
- void set_project_title(const String &p_title);
- void set_project_path(const String &p_path);
- void set_tags(const PackedStringArray &p_tags, ProjectList *p_parent_list);
- void set_project_icon(const Ref<Texture2D> &p_icon);
- void set_unsupported_features(PackedStringArray p_features);
-
- bool should_load_project_icon() const;
- void set_selected(bool p_selected);
-
- void set_is_favorite(bool p_favorite);
- void set_is_missing(bool p_missing);
- void set_is_grayed(bool p_grayed);
-
- ProjectListItemControl();
-};
-
-class ProjectList : public ScrollContainer {
- GDCLASS(ProjectList, ScrollContainer)
+class ProjectManager : public Control {
+ GDCLASS(ProjectManager, Control);
- friend class ProjectManager;
+ static ProjectManager *singleton;
-public:
- enum FilterOption {
- EDIT_DATE,
- NAME,
- PATH,
- TAGS,
- };
-
- // Can often be passed by copy
- struct Item {
- String project_name;
- String description;
- PackedStringArray tags;
- String tag_sort_string;
- String path;
- String icon;
- String main_scene;
- PackedStringArray unsupported_features;
- uint64_t last_edited = 0;
- bool favorite = false;
- bool grayed = false;
- bool missing = false;
- int version = 0;
-
- ProjectListItemControl *control = nullptr;
-
- Item() {}
-
- Item(const String &p_name,
- const String &p_description,
- const PackedStringArray &p_tags,
- const String &p_path,
- const String &p_icon,
- const String &p_main_scene,
- const PackedStringArray &p_unsupported_features,
- uint64_t p_last_edited,
- bool p_favorite,
- bool p_grayed,
- bool p_missing,
- int p_version) {
- project_name = p_name;
- description = p_description;
- tags = p_tags;
- path = p_path;
- icon = p_icon;
- main_scene = p_main_scene;
- unsupported_features = p_unsupported_features;
- last_edited = p_last_edited;
- favorite = p_favorite;
- grayed = p_grayed;
- missing = p_missing;
- version = p_version;
- control = nullptr;
-
- PackedStringArray sorted_tags = tags;
- sorted_tags.sort();
- tag_sort_string = String().join(sorted_tags);
- }
-
- _FORCE_INLINE_ bool operator==(const Item &l) const {
- return path == l.path;
- }
- };
-
-private:
- bool project_opening_initiated = false;
-
- String _search_term;
- FilterOption _order_option = FilterOption::EDIT_DATE;
- HashSet<String> _selected_project_paths;
- String _last_clicked; // Project key
- VBoxContainer *_scroll_children = nullptr;
- int _icon_load_index = 0;
-
- Vector<Item> _projects;
-
- ConfigFile _config;
- String _config_path;
-
- void _panel_input(const Ref<InputEvent> &p_ev, Node *p_hb);
- void _favorite_pressed(Node *p_hb);
- void _show_project(const String &p_path);
-
- void _migrate_config();
- void _scan_folder_recursive(const String &p_path, List<String> *r_projects);
-
- void _clear_project_selection();
- void _toggle_project(int p_index);
- void _select_project_nocheck(int p_index);
- void _deselect_project_nocheck(int p_index);
- void _select_project_range(int p_begin, int p_end);
-
- void _create_project_item_control(int p_index);
- void _remove_project(int p_index, bool p_update_settings);
-
- static Item load_project_data(const String &p_property_key, bool p_favorite);
- void _update_icons_async();
- void _load_project_icon(int p_index);
-
- void _global_menu_new_window(const Variant &p_tag);
- void _global_menu_open_project(const Variant &p_tag);
+ // Utility data.
-protected:
- void _notification(int p_what);
- static void _bind_methods();
+ static Ref<Texture2D> _file_dialog_get_icon(const String &p_path);
+ static Ref<Texture2D> _file_dialog_get_thumbnail(const String &p_path);
-public:
- static const char *SIGNAL_LIST_CHANGED;
- static const char *SIGNAL_SELECTION_CHANGED;
- static const char *SIGNAL_PROJECT_ASK_OPEN;
+ HashMap<String, Ref<Texture2D>> icon_type_cache;
- void update_project_list();
- int get_project_count() const;
+ void _build_icon_type_cache(Ref<Theme> p_theme);
- void find_projects(const String &p_path);
- void find_projects_multiple(const PackedStringArray &p_paths);
- void sort_projects();
+ // Main layout.
- void add_project(const String &dir_path, bool favorite);
- void set_project_version(const String &p_project_path, int version);
- int refresh_project(const String &dir_path);
- void ensure_project_visible(int p_index);
+ void _update_size_limits();
- void select_project(int p_index);
- void select_first_visible_project();
- void erase_selected_projects(bool p_delete_project_contents);
- Vector<Item> get_selected_projects() const;
- const HashSet<String> &get_selected_project_keys() const;
- int get_single_selected_index() const;
+ Panel *background_panel = nullptr;
+ Button *about_btn = nullptr;
+ LinkButton *version_btn = nullptr;
- bool is_any_project_missing() const;
- void erase_missing_projects();
+ ConfirmationDialog *open_templates = nullptr;
+ EditorAbout *about = nullptr;
- void set_search_term(String p_search_term);
- void add_search_tag(const String &p_tag);
- void set_order_option(int p_option);
+ void _show_about();
+ void _version_button_pressed();
- void update_dock_menu();
- void save_config();
+ TabContainer *tabs = nullptr;
+ VBoxContainer *local_projects_vb = nullptr;
+ EditorAssetLibrary *asset_library = nullptr;
- ProjectList();
-};
+ void _on_tab_changed(int p_tab);
+ void _open_asset_library();
-class ProjectManager : public Control {
- GDCLASS(ProjectManager, Control);
+ // Quick settings.
- HashMap<String, Ref<Texture2D>> icon_type_cache;
- void _build_icon_type_cache(Ref<Theme> p_theme);
+ OptionButton *language_btn = nullptr;
+ ConfirmationDialog *restart_required_dialog = nullptr;
- static ProjectManager *singleton;
+ void _language_selected(int p_id);
+ void _restart_confirm();
+ void _dim_window();
- void _update_size_limits();
+ // Project list.
- Panel *background_panel = nullptr;
- TabContainer *tabs = nullptr;
ProjectList *_project_list = nullptr;
LineEdit *search_box = nullptr;
@@ -356,29 +109,17 @@ class ProjectManager : public Control {
Button *manage_tags_btn = nullptr;
Button *erase_btn = nullptr;
Button *erase_missing_btn = nullptr;
- Button *about_btn = nullptr;
-
- VBoxContainer *local_projects_vb = nullptr;
- EditorAssetLibrary *asset_library = nullptr;
-
- Ref<StyleBox> tag_stylebox;
EditorFileDialog *scan_dir = nullptr;
- ConfirmationDialog *language_restart_ask = nullptr;
ConfirmationDialog *erase_ask = nullptr;
Label *erase_ask_label = nullptr;
// Comment out for now until we have a better warning system to
// ensure users delete their project only.
//CheckBox *delete_project_contents = nullptr;
-
ConfirmationDialog *erase_missing_ask = nullptr;
ConfirmationDialog *multi_open_ask = nullptr;
ConfirmationDialog *multi_run_ask = nullptr;
- ConfirmationDialog *ask_full_convert_dialog = nullptr;
- ConfirmationDialog *ask_update_settings = nullptr;
- ConfirmationDialog *open_templates = nullptr;
- EditorAbout *about = nullptr;
HBoxContainer *settings_hb = nullptr;
@@ -386,30 +127,13 @@ class ProjectManager : public Control {
AcceptDialog *dialog_error = nullptr;
ProjectDialog *npdialog = nullptr;
- Button *full_convert_button = nullptr;
- OptionButton *language_btn = nullptr;
- LinkButton *version_btn = nullptr;
-
- HashSet<String> tag_set;
- PackedStringArray current_project_tags;
- PackedStringArray forbidden_tag_characters{ "/", "\\", "-" };
- ConfirmationDialog *tag_manage_dialog = nullptr;
- HFlowContainer *project_tags = nullptr;
- HFlowContainer *all_tags = nullptr;
- Label *tag_edit_error = nullptr;
- Button *create_tag_btn = nullptr;
- ConfirmationDialog *create_tag_dialog = nullptr;
- LineEdit *new_tag_name = nullptr;
- Label *tag_error = nullptr;
-
- void _open_asset_library();
void _scan_projects();
void _run_project();
void _run_project_confirm();
void _open_selected_projects();
void _open_selected_projects_ask();
- void _full_convert_button_pressed();
- void _perform_full_project_conversion();
+
+ void _install_project(const String &p_zip_path, const String &p_title);
void _import_project();
void _new_project();
void _rename_project();
@@ -417,11 +141,30 @@ class ProjectManager : public Control {
void _erase_missing_projects();
void _erase_project_confirm();
void _erase_missing_projects_confirm();
- void _show_about();
void _update_project_buttons();
- void _language_selected(int p_id);
- void _restart_confirm();
- void _confirm_update_settings();
+
+ void _on_project_created(const String &dir);
+ void _on_projects_updated();
+
+ void _on_order_option_changed(int p_idx);
+ void _on_search_term_changed(const String &p_term);
+ void _on_search_term_submitted(const String &p_text);
+
+ // Project tag management.
+
+ HashSet<String> tag_set;
+ PackedStringArray current_project_tags;
+ PackedStringArray forbidden_tag_characters{ "/", "\\", "-" };
+
+ ConfirmationDialog *tag_manage_dialog = nullptr;
+ HFlowContainer *project_tags = nullptr;
+ HFlowContainer *all_tags = nullptr;
+ Label *tag_edit_error = nullptr;
+
+ Button *create_tag_btn = nullptr;
+ ConfirmationDialog *create_tag_dialog = nullptr;
+ LineEdit *new_tag_name = nullptr;
+ Label *tag_error = nullptr;
void _manage_project_tags();
void _add_project_tag(const String &p_tag);
@@ -430,23 +173,20 @@ class ProjectManager : public Control {
void _set_new_tag_name(const String p_name);
void _create_new_tag();
- void _on_project_created(const String &dir);
- void _on_projects_updated();
+ // Project converter/migration tool.
- void _install_project(const String &p_zip_path, const String &p_title);
+ ConfirmationDialog *ask_full_convert_dialog = nullptr;
+ ConfirmationDialog *ask_update_settings = nullptr;
+ Button *full_convert_button = nullptr;
- void _dim_window();
- virtual void shortcut_input(const Ref<InputEvent> &p_ev) override;
- void _files_dropped(PackedStringArray p_files);
+ void _full_convert_button_pressed();
+ void _perform_full_project_conversion();
- void _version_button_pressed();
- void _on_order_option_changed(int p_idx);
- void _on_tab_changed(int p_tab);
- void _on_search_term_changed(const String &p_term);
- void _on_search_term_submitted(const String &p_text);
+ // Input and I/O.
- static Ref<Texture2D> _file_dialog_get_icon(const String &p_path);
- static Ref<Texture2D> _file_dialog_get_thumbnail(const String &p_path);
+ virtual void shortcut_input(const Ref<InputEvent> &p_ev) override;
+
+ void _files_dropped(PackedStringArray p_files);
protected:
void _notification(int p_what);
@@ -454,28 +194,16 @@ protected:
public:
static ProjectManager *get_singleton() { return singleton; }
+ // Project list.
+
LineEdit *get_search_box();
+
+ // Project tag management.
+
void add_new_tag(const String &p_tag);
ProjectManager();
~ProjectManager();
};
-class ProjectTag : public HBoxContainer {
- GDCLASS(ProjectTag, HBoxContainer);
-
- String tag_string;
- Button *button = nullptr;
- bool display_close = false;
-
-protected:
- void _notification(int p_what);
-
-public:
- ProjectTag(const String &p_text, bool p_display_close = false);
-
- void connect_button_to(const Callable &p_callable);
- const String get_tag() const;
-};
-
#endif // PROJECT_MANAGER_H
diff --git a/editor/project_manager/SCsub b/editor/project_manager/SCsub
new file mode 100644
index 0000000000..359d04e5df
--- /dev/null
+++ b/editor/project_manager/SCsub
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+
+Import("env")
+
+env.add_source_files(env.editor_sources, "*.cpp")
diff --git a/editor/project_manager/project_dialog.cpp b/editor/project_manager/project_dialog.cpp
new file mode 100644
index 0000000000..f773e6f696
--- /dev/null
+++ b/editor/project_manager/project_dialog.cpp
@@ -0,0 +1,977 @@
+/**************************************************************************/
+/* project_dialog.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "project_dialog.h"
+
+#include "core/config/project_settings.h"
+#include "core/io/dir_access.h"
+#include "core/io/zip_io.h"
+#include "core/version.h"
+#include "editor/editor_settings.h"
+#include "editor/editor_string_names.h"
+#include "editor/editor_vcs_interface.h"
+#include "editor/gui/editor_file_dialog.h"
+#include "editor/themes/editor_icons.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/check_box.h"
+#include "scene/gui/line_edit.h"
+#include "scene/gui/option_button.h"
+#include "scene/gui/separator.h"
+#include "scene/gui/texture_rect.h"
+
+void ProjectDialog::_set_message(const String &p_msg, MessageType p_type, InputType input_type) {
+ msg->set_text(p_msg);
+ Ref<Texture2D> current_path_icon = status_rect->get_texture();
+ Ref<Texture2D> current_install_icon = install_status_rect->get_texture();
+ Ref<Texture2D> new_icon;
+
+ switch (p_type) {
+ case MESSAGE_ERROR: {
+ msg->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
+ msg->set_modulate(Color(1, 1, 1, 1));
+ new_icon = get_editor_theme_icon(SNAME("StatusError"));
+
+ } break;
+ case MESSAGE_WARNING: {
+ msg->add_theme_color_override("font_color", get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
+ msg->set_modulate(Color(1, 1, 1, 1));
+ new_icon = get_editor_theme_icon(SNAME("StatusWarning"));
+
+ } break;
+ case MESSAGE_SUCCESS: {
+ msg->remove_theme_color_override("font_color");
+ msg->set_modulate(Color(1, 1, 1, 0));
+ new_icon = get_editor_theme_icon(SNAME("StatusSuccess"));
+
+ } break;
+ }
+
+ if (current_path_icon != new_icon && input_type == PROJECT_PATH) {
+ status_rect->set_texture(new_icon);
+ } else if (current_install_icon != new_icon && input_type == INSTALL_PATH) {
+ install_status_rect->set_texture(new_icon);
+ }
+}
+
+static bool is_zip_file(Ref<DirAccess> p_d, const String &p_path) {
+ return p_path.ends_with(".zip") && p_d->file_exists(p_path);
+}
+
+String ProjectDialog::_test_path() {
+ Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ const String base_path = project_path->get_text();
+ String valid_path, valid_install_path;
+ bool is_zip = false;
+ if (d->change_dir(base_path) == OK) {
+ valid_path = base_path;
+ } else if (is_zip_file(d, base_path)) {
+ valid_path = base_path;
+ is_zip = true;
+ } else if (d->change_dir(base_path.strip_edges()) == OK) {
+ valid_path = base_path.strip_edges();
+ } else if (is_zip_file(d, base_path.strip_edges())) {
+ valid_path = base_path.strip_edges();
+ is_zip = true;
+ }
+
+ if (valid_path.is_empty()) {
+ _set_message(TTR("The path specified doesn't exist."), MESSAGE_ERROR);
+ get_ok_button()->set_disabled(true);
+ return "";
+ }
+
+ if (mode == MODE_IMPORT && is_zip) {
+ if (d->change_dir(install_path->get_text()) == OK) {
+ valid_install_path = install_path->get_text();
+ } else if (d->change_dir(install_path->get_text().strip_edges()) == OK) {
+ valid_install_path = install_path->get_text().strip_edges();
+ }
+
+ if (valid_install_path.is_empty()) {
+ _set_message(TTR("The install path specified doesn't exist."), MESSAGE_ERROR, INSTALL_PATH);
+ get_ok_button()->set_disabled(true);
+ return "";
+ }
+ }
+
+ if (mode == MODE_IMPORT || mode == MODE_RENAME) {
+ if (!d->file_exists("project.godot")) {
+ if (is_zip) {
+ Ref<FileAccess> io_fa;
+ zlib_filefunc_def io = zipio_create_io(&io_fa);
+
+ unzFile pkg = unzOpen2(valid_path.utf8().get_data(), &io);
+ if (!pkg) {
+ _set_message(TTR("Error opening package file (it's not in ZIP format)."), MESSAGE_ERROR);
+ get_ok_button()->set_disabled(true);
+ unzClose(pkg);
+ return "";
+ }
+
+ int ret = unzGoToFirstFile(pkg);
+ while (ret == UNZ_OK) {
+ unz_file_info info;
+ char fname[16384];
+ ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
+ if (ret != UNZ_OK) {
+ break;
+ }
+
+ if (String::utf8(fname).ends_with("project.godot")) {
+ break;
+ }
+
+ ret = unzGoToNextFile(pkg);
+ }
+
+ if (ret == UNZ_END_OF_LIST_OF_FILE) {
+ _set_message(TTR("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR);
+ get_ok_button()->set_disabled(true);
+ unzClose(pkg);
+ return "";
+ }
+
+ unzClose(pkg);
+
+ // check if the specified install folder is empty, even though this is not an error, it is good to check here
+ d->list_dir_begin();
+ is_folder_empty = true;
+ String n = d->get_next();
+ while (!n.is_empty()) {
+ if (!n.begins_with(".")) {
+ // Allow `.`, `..` (reserved current/parent folder names)
+ // and hidden files/folders to be present.
+ // For instance, this lets users initialize a Git repository
+ // and still be able to create a project in the directory afterwards.
+ is_folder_empty = false;
+ break;
+ }
+ n = d->get_next();
+ }
+ d->list_dir_end();
+
+ if (!is_folder_empty) {
+ _set_message(TTR("Please choose an empty install folder."), MESSAGE_WARNING, INSTALL_PATH);
+ get_ok_button()->set_disabled(true);
+ return "";
+ }
+
+ } else {
+ _set_message(TTR("Please choose a \"project.godot\", a directory with it, or a \".zip\" file."), MESSAGE_ERROR);
+ install_path_container->hide();
+ get_ok_button()->set_disabled(true);
+ return "";
+ }
+
+ } else if (is_zip) {
+ _set_message(TTR("The install directory already contains a Godot project."), MESSAGE_ERROR, INSTALL_PATH);
+ get_ok_button()->set_disabled(true);
+ return "";
+ }
+
+ } else {
+ // Check if the specified folder is empty, even though this is not an error, it is good to check here.
+ d->list_dir_begin();
+ is_folder_empty = true;
+ String n = d->get_next();
+ while (!n.is_empty()) {
+ if (!n.begins_with(".")) {
+ // Allow `.`, `..` (reserved current/parent folder names)
+ // and hidden files/folders to be present.
+ // For instance, this lets users initialize a Git repository
+ // and still be able to create a project in the directory afterwards.
+ is_folder_empty = false;
+ break;
+ }
+ n = d->get_next();
+ }
+ d->list_dir_end();
+
+ if (!is_folder_empty) {
+ 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()) {
+ _set_message(TTR("You cannot save a project in the selected path. Please make a new folder or choose a new path."), MESSAGE_ERROR);
+ get_ok_button()->set_disabled(true);
+ return "";
+ }
+
+ _set_message(TTR("The selected path is not empty. Choosing an empty folder is highly recommended."), MESSAGE_WARNING);
+ get_ok_button()->set_disabled(false);
+ return valid_path;
+ }
+ }
+
+ _set_message("");
+ _set_message("", MESSAGE_SUCCESS, INSTALL_PATH);
+ get_ok_button()->set_disabled(false);
+ return valid_path;
+}
+
+void ProjectDialog::_update_path(const String &p_path) {
+ String sp = _test_path();
+ if (!sp.is_empty()) {
+ // If the project name is empty or default, infer the project name from the selected folder name
+ if (project_name->get_text().strip_edges().is_empty() || project_name->get_text().strip_edges() == TTR("New Game Project")) {
+ sp = sp.replace("\\", "/");
+ int lidx = sp.rfind("/");
+
+ if (lidx != -1) {
+ sp = sp.substr(lidx + 1, sp.length()).capitalize();
+ }
+ if (sp.is_empty() && mode == MODE_IMPORT) {
+ sp = TTR("Imported Project");
+ }
+
+ project_name->set_text(sp);
+ _text_changed(sp);
+ }
+ }
+
+ if (!created_folder_path.is_empty() && created_folder_path != p_path) {
+ _remove_created_folder();
+ }
+}
+
+void ProjectDialog::_path_text_changed(const String &p_path) {
+ Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ if (mode == MODE_IMPORT && is_zip_file(d, p_path)) {
+ install_path->set_text(p_path.get_base_dir());
+ install_path_container->show();
+ } else if (mode == MODE_IMPORT && is_zip_file(d, p_path.strip_edges())) {
+ install_path->set_text(p_path.strip_edges().get_base_dir());
+ install_path_container->show();
+ } else {
+ install_path_container->hide();
+ }
+
+ _update_path(p_path.simplify_path());
+}
+
+void ProjectDialog::_file_selected(const String &p_path) {
+ // If not already shown.
+ show_dialog();
+
+ String p = p_path;
+ if (mode == MODE_IMPORT) {
+ if (p.ends_with("project.godot")) {
+ p = p.get_base_dir();
+ install_path_container->hide();
+ get_ok_button()->set_disabled(false);
+ } else if (p.ends_with(".zip")) {
+ install_path->set_text(p.get_base_dir());
+ install_path_container->show();
+ get_ok_button()->set_disabled(false);
+ } else {
+ _set_message(TTR("Please choose a \"project.godot\" or \".zip\" file."), MESSAGE_ERROR);
+ get_ok_button()->set_disabled(true);
+ return;
+ }
+ }
+
+ String sp = p.simplify_path();
+ project_path->set_text(sp);
+ _update_path(sp);
+ if (p.ends_with(".zip")) {
+ callable_mp((Control *)install_path, &Control::grab_focus).call_deferred();
+ } else {
+ callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred();
+ }
+}
+
+void ProjectDialog::_path_selected(const String &p_path) {
+ // If not already shown.
+ show_dialog();
+
+ String sp = p_path.simplify_path();
+ project_path->set_text(sp);
+ _update_path(sp);
+ callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred();
+}
+
+void ProjectDialog::_install_path_selected(const String &p_path) {
+ String sp = p_path.simplify_path();
+ install_path->set_text(sp);
+ _update_path(sp);
+ callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred();
+}
+
+void ProjectDialog::_browse_path() {
+ fdialog->set_current_dir(project_path->get_text());
+
+ if (mode == MODE_IMPORT) {
+ fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_ANY);
+ fdialog->clear_filters();
+ fdialog->add_filter("project.godot", vformat("%s %s", VERSION_NAME, TTR("Project")));
+ fdialog->add_filter("*.zip", TTR("ZIP File"));
+ } else {
+ fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
+ }
+ fdialog->popup_file_dialog();
+}
+
+void ProjectDialog::_browse_install_path() {
+ fdialog_install->set_current_dir(install_path->get_text());
+ fdialog_install->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
+ fdialog_install->popup_file_dialog();
+}
+
+void ProjectDialog::_create_folder() {
+ const String project_name_no_edges = project_name->get_text().strip_edges();
+ if (project_name_no_edges.is_empty() || !created_folder_path.is_empty() || project_name_no_edges.ends_with(".")) {
+ _set_message(TTR("Invalid project name."), MESSAGE_WARNING);
+ return;
+ }
+
+ Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ if (d->change_dir(project_path->get_text()) == OK) {
+ if (!d->dir_exists(project_name_no_edges)) {
+ if (d->make_dir(project_name_no_edges) == OK) {
+ d->change_dir(project_name_no_edges);
+ String dir_str = d->get_current_dir();
+ project_path->set_text(dir_str);
+ _update_path(dir_str);
+ created_folder_path = d->get_current_dir();
+ create_dir->set_disabled(true);
+ } else {
+ dialog_error->set_text(TTR("Couldn't create folder."));
+ dialog_error->popup_centered();
+ }
+ } else {
+ dialog_error->set_text(TTR("There is already a folder in this path with the specified name."));
+ dialog_error->popup_centered();
+ }
+ }
+}
+
+void ProjectDialog::_text_changed(const String &p_text) {
+ if (mode != MODE_NEW) {
+ return;
+ }
+
+ _test_path();
+
+ if (p_text.strip_edges().is_empty()) {
+ _set_message(TTR("It would be a good idea to name your project."), MESSAGE_ERROR);
+ }
+}
+
+void ProjectDialog::_nonempty_confirmation_ok_pressed() {
+ is_folder_empty = true;
+ ok_pressed();
+}
+
+void ProjectDialog::_renderer_selected() {
+ ERR_FAIL_NULL(renderer_button_group->get_pressed_button());
+
+ String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));
+
+ if (renderer_type == "forward_plus") {
+ renderer_info->set_text(
+ String::utf8("• ") + TTR("Supports desktop platforms only.") +
+ String::utf8("\n• ") + TTR("Advanced 3D graphics available.") +
+ String::utf8("\n• ") + TTR("Can scale to large complex scenes.") +
+ String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +
+ String::utf8("\n• ") + TTR("Slower rendering of simple scenes."));
+ } else if (renderer_type == "mobile") {
+ renderer_info->set_text(
+ String::utf8("• ") + TTR("Supports desktop + mobile platforms.") +
+ String::utf8("\n• ") + TTR("Less advanced 3D graphics.") +
+ String::utf8("\n• ") + TTR("Less scalable for complex scenes.") +
+ String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +
+ String::utf8("\n• ") + TTR("Fast rendering of simple scenes."));
+ } else if (renderer_type == "gl_compatibility") {
+ renderer_info->set_text(
+ String::utf8("• ") + TTR("Supports desktop, mobile + web platforms.") +
+ String::utf8("\n• ") + TTR("Least advanced 3D graphics (currently work-in-progress).") +
+ String::utf8("\n• ") + TTR("Intended for low-end/older devices.") +
+ String::utf8("\n• ") + TTR("Uses OpenGL 3 backend (OpenGL 3.3/ES 3.0/WebGL2).") +
+ String::utf8("\n• ") + TTR("Fastest rendering of simple scenes."));
+ } else {
+ WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
+ }
+}
+
+void ProjectDialog::_remove_created_folder() {
+ if (!created_folder_path.is_empty()) {
+ Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ d->remove(created_folder_path);
+
+ create_dir->set_disabled(false);
+ created_folder_path = "";
+ }
+}
+
+void ProjectDialog::ok_pressed() {
+ String dir = project_path->get_text();
+
+ if (mode == MODE_RENAME) {
+ String dir2 = _test_path();
+ if (dir2.is_empty()) {
+ _set_message(TTR("Invalid project path (changed anything?)."), MESSAGE_ERROR);
+ return;
+ }
+
+ // Load project.godot as ConfigFile to set the new name.
+ ConfigFile cfg;
+ String project_godot = dir2.path_join("project.godot");
+ Error err = cfg.load(project_godot);
+ if (err != OK) {
+ _set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR);
+ } else {
+ cfg.set_value("application", "config/name", project_name->get_text().strip_edges());
+ err = cfg.save(project_godot);
+ if (err != OK) {
+ _set_message(vformat(TTR("Couldn't save project at '%s' (error %d)."), project_godot, err), MESSAGE_ERROR);
+ }
+ }
+
+ hide();
+ emit_signal(SNAME("projects_updated"));
+
+ } else {
+ if (mode == MODE_IMPORT) {
+ if (project_path->get_text().ends_with(".zip")) {
+ mode = MODE_INSTALL;
+ ok_pressed();
+
+ return;
+ }
+
+ } else {
+ if (mode == MODE_NEW) {
+ // Before we create a project, check that the target folder is empty.
+ // If not, we need to ask the user if they're sure they want to do this.
+ if (!is_folder_empty) {
+ ConfirmationDialog *cd = memnew(ConfirmationDialog);
+ cd->set_title(TTR("Warning: This folder is not empty"));
+ cd->set_text(TTR("You are about to create a Godot project in a non-empty folder.\nThe entire contents of this folder will be imported as project resources!\n\nAre you sure you wish to continue?"));
+ cd->get_ok_button()->connect("pressed", callable_mp(this, &ProjectDialog::_nonempty_confirmation_ok_pressed));
+ get_parent()->add_child(cd);
+ cd->popup_centered();
+ cd->grab_focus();
+ return;
+ }
+ PackedStringArray project_features = ProjectSettings::get_required_features();
+ ProjectSettings::CustomMap initial_settings;
+
+ // Be sure to change this code if/when renderers are changed.
+ // Default values are "forward_plus" for the main setting, "mobile" for the mobile override,
+ // and "gl_compatibility" for the web override.
+ String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));
+ initial_settings["rendering/renderer/rendering_method"] = renderer_type;
+
+ EditorSettings::get_singleton()->set("project_manager/default_renderer", renderer_type);
+ EditorSettings::get_singleton()->save();
+
+ if (renderer_type == "forward_plus") {
+ project_features.push_back("Forward Plus");
+ } else if (renderer_type == "mobile") {
+ project_features.push_back("Mobile");
+ } else if (renderer_type == "gl_compatibility") {
+ project_features.push_back("GL Compatibility");
+ // Also change the default rendering method for the mobile override.
+ initial_settings["rendering/renderer/rendering_method.mobile"] = "gl_compatibility";
+ } else {
+ WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
+ }
+
+ project_features.sort();
+ initial_settings["application/config/features"] = project_features;
+ initial_settings["application/config/name"] = project_name->get_text().strip_edges();
+ initial_settings["application/config/icon"] = "res://icon.svg";
+
+ if (ProjectSettings::get_singleton()->save_custom(dir.path_join("project.godot"), initial_settings, Vector<String>(), false) != OK) {
+ _set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR);
+ } else {
+ // Store default project icon in SVG format.
+ Error err;
+ Ref<FileAccess> fa_icon = FileAccess::open(dir.path_join("icon.svg"), FileAccess::WRITE, &err);
+ fa_icon->store_string(get_default_project_icon());
+
+ if (err != OK) {
+ _set_message(TTR("Couldn't create icon.svg in project path."), MESSAGE_ERROR);
+ }
+
+ EditorVCSInterface::create_vcs_metadata_files(EditorVCSInterface::VCSMetadata(vcs_metadata_selection->get_selected()), dir);
+ }
+ } else if (mode == MODE_INSTALL) {
+ if (project_path->get_text().ends_with(".zip")) {
+ dir = install_path->get_text();
+ zip_path = project_path->get_text();
+ }
+
+ Ref<FileAccess> io_fa;
+ zlib_filefunc_def io = zipio_create_io(&io_fa);
+
+ unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io);
+ if (!pkg) {
+ dialog_error->set_text(TTR("Error opening package file, not in ZIP format."));
+ dialog_error->popup_centered();
+ return;
+ }
+
+ // Find the zip_root
+ String zip_root;
+ int ret = unzGoToFirstFile(pkg);
+ while (ret == UNZ_OK) {
+ unz_file_info info;
+ char fname[16384];
+ unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
+
+ String name = String::utf8(fname);
+ if (name.ends_with("project.godot")) {
+ zip_root = name.substr(0, name.rfind("project.godot"));
+ break;
+ }
+
+ ret = unzGoToNextFile(pkg);
+ }
+
+ ret = unzGoToFirstFile(pkg);
+
+ Vector<String> failed_files;
+
+ while (ret == UNZ_OK) {
+ //get filename
+ unz_file_info info;
+ char fname[16384];
+ ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
+ if (ret != UNZ_OK) {
+ break;
+ }
+
+ String path = String::utf8(fname);
+
+ if (path.is_empty() || path == zip_root || !zip_root.is_subsequence_of(path)) {
+ //
+ } else if (path.ends_with("/")) { // a dir
+ path = path.substr(0, path.length() - 1);
+ String rel_path = path.substr(zip_root.length());
+
+ Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ da->make_dir(dir.path_join(rel_path));
+ } else {
+ Vector<uint8_t> uncomp_data;
+ uncomp_data.resize(info.uncompressed_size);
+ String rel_path = path.substr(zip_root.length());
+
+ //read
+ unzOpenCurrentFile(pkg);
+ ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size());
+ ERR_BREAK_MSG(ret < 0, vformat("An error occurred while attempting to read from file: %s. This file will not be used.", rel_path));
+ unzCloseCurrentFile(pkg);
+
+ Ref<FileAccess> f = FileAccess::open(dir.path_join(rel_path), FileAccess::WRITE);
+ if (f.is_valid()) {
+ f->store_buffer(uncomp_data.ptr(), uncomp_data.size());
+ } else {
+ failed_files.push_back(rel_path);
+ }
+ }
+
+ ret = unzGoToNextFile(pkg);
+ }
+
+ unzClose(pkg);
+
+ if (failed_files.size()) {
+ String err_msg = TTR("The following files failed extraction from package:") + "\n\n";
+ for (int i = 0; i < failed_files.size(); i++) {
+ if (i > 15) {
+ err_msg += "\nAnd " + itos(failed_files.size() - i) + " more files.";
+ break;
+ }
+ err_msg += failed_files[i] + "\n";
+ }
+
+ dialog_error->set_text(err_msg);
+ dialog_error->popup_centered();
+
+ } else if (!project_path->get_text().ends_with(".zip")) {
+ dialog_error->set_text(TTR("Package installed successfully!"));
+ dialog_error->popup_centered();
+ }
+ }
+ }
+
+ dir = dir.replace("\\", "/");
+ if (dir.ends_with("/")) {
+ dir = dir.substr(0, dir.length() - 1);
+ }
+
+ hide();
+ emit_signal(SNAME("project_created"), dir);
+ }
+}
+
+void ProjectDialog::cancel_pressed() {
+ _remove_created_folder();
+
+ project_path->clear();
+ _update_path("");
+ project_name->clear();
+ _text_changed("");
+
+ if (status_rect->get_texture() == get_editor_theme_icon(SNAME("StatusError"))) {
+ msg->show();
+ }
+
+ if (install_status_rect->get_texture() == get_editor_theme_icon(SNAME("StatusError"))) {
+ msg->show();
+ }
+}
+
+void ProjectDialog::set_zip_path(const String &p_path) {
+ zip_path = p_path;
+}
+
+void ProjectDialog::set_zip_title(const String &p_title) {
+ zip_title = p_title;
+}
+
+void ProjectDialog::set_mode(Mode p_mode) {
+ mode = p_mode;
+}
+
+void ProjectDialog::set_project_path(const String &p_path) {
+ project_path->set_text(p_path);
+}
+
+void ProjectDialog::ask_for_path_and_show() {
+ // Workaround: for the file selection dialog content to be rendered we need to show its parent dialog.
+ show_dialog();
+ _set_message("");
+
+ _browse_path();
+}
+
+void ProjectDialog::show_dialog() {
+ if (mode == MODE_RENAME) {
+ project_path->set_editable(false);
+ browse->hide();
+ install_browse->hide();
+
+ set_title(TTR("Rename Project"));
+ set_ok_button_text(TTR("Rename"));
+ name_container->show();
+ status_rect->hide();
+ msg->hide();
+ install_path_container->hide();
+ install_status_rect->hide();
+ renderer_container->hide();
+ default_files_container->hide();
+ get_ok_button()->set_disabled(false);
+
+ // Fetch current name from project.godot to prefill the text input.
+ ConfigFile cfg;
+ String project_godot = project_path->get_text().path_join("project.godot");
+ Error err = cfg.load(project_godot);
+ if (err != OK) {
+ _set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR);
+ status_rect->show();
+ msg->show();
+ get_ok_button()->set_disabled(true);
+ } else {
+ String cur_name = cfg.get_value("application", "config/name", "");
+ project_name->set_text(cur_name);
+ _text_changed(cur_name);
+ }
+
+ callable_mp((Control *)project_name, &Control::grab_focus).call_deferred();
+
+ create_dir->hide();
+
+ } else {
+ fav_dir = EDITOR_GET("filesystem/directories/default_project_path");
+ if (!fav_dir.is_empty()) {
+ project_path->set_text(fav_dir);
+ fdialog->set_current_dir(fav_dir);
+ } else {
+ Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ project_path->set_text(d->get_current_dir());
+ fdialog->set_current_dir(d->get_current_dir());
+ }
+
+ if (project_name->get_text().is_empty()) {
+ String proj = TTR("New Game Project");
+ project_name->set_text(proj);
+ _text_changed(proj);
+ }
+
+ project_path->set_editable(true);
+ browse->set_disabled(false);
+ browse->show();
+ install_browse->set_disabled(false);
+ install_browse->show();
+ create_dir->show();
+ status_rect->show();
+ install_status_rect->show();
+ msg->show();
+
+ if (mode == MODE_IMPORT) {
+ set_title(TTR("Import Existing Project"));
+ set_ok_button_text(TTR("Import & Edit"));
+ name_container->hide();
+ install_path_container->hide();
+ renderer_container->hide();
+ default_files_container->hide();
+ project_path->grab_focus();
+
+ } else if (mode == MODE_NEW) {
+ set_title(TTR("Create New Project"));
+ set_ok_button_text(TTR("Create & Edit"));
+ name_container->show();
+ install_path_container->hide();
+ renderer_container->show();
+ default_files_container->show();
+ callable_mp((Control *)project_name, &Control::grab_focus).call_deferred();
+ callable_mp(project_name, &LineEdit::select_all).call_deferred();
+
+ } else if (mode == MODE_INSTALL) {
+ set_title(TTR("Install Project:") + " " + zip_title);
+ set_ok_button_text(TTR("Install & Edit"));
+ project_name->set_text(zip_title);
+ name_container->show();
+ install_path_container->hide();
+ renderer_container->hide();
+ default_files_container->hide();
+ project_path->grab_focus();
+ }
+
+ _test_path();
+ }
+
+ popup_centered(Size2(500, 0) * EDSCALE);
+}
+
+void ProjectDialog::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_WM_CLOSE_REQUEST: {
+ _remove_created_folder();
+ } break;
+ }
+}
+
+void ProjectDialog::_bind_methods() {
+ ADD_SIGNAL(MethodInfo("project_created"));
+ ADD_SIGNAL(MethodInfo("projects_updated"));
+}
+
+ProjectDialog::ProjectDialog() {
+ VBoxContainer *vb = memnew(VBoxContainer);
+ add_child(vb);
+
+ name_container = memnew(VBoxContainer);
+ vb->add_child(name_container);
+
+ Label *l = memnew(Label);
+ l->set_text(TTR("Project Name:"));
+ name_container->add_child(l);
+
+ HBoxContainer *pnhb = memnew(HBoxContainer);
+ name_container->add_child(pnhb);
+
+ project_name = memnew(LineEdit);
+ project_name->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ pnhb->add_child(project_name);
+
+ create_dir = memnew(Button);
+ pnhb->add_child(create_dir);
+ create_dir->set_text(TTR("Create Folder"));
+ create_dir->connect("pressed", callable_mp(this, &ProjectDialog::_create_folder));
+
+ path_container = memnew(VBoxContainer);
+ vb->add_child(path_container);
+
+ l = memnew(Label);
+ l->set_text(TTR("Project Path:"));
+ path_container->add_child(l);
+
+ HBoxContainer *pphb = memnew(HBoxContainer);
+ path_container->add_child(pphb);
+
+ project_path = memnew(LineEdit);
+ project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
+ pphb->add_child(project_path);
+
+ install_path_container = memnew(VBoxContainer);
+ vb->add_child(install_path_container);
+
+ l = memnew(Label);
+ l->set_text(TTR("Project Installation Path:"));
+ install_path_container->add_child(l);
+
+ HBoxContainer *iphb = memnew(HBoxContainer);
+ install_path_container->add_child(iphb);
+
+ install_path = memnew(LineEdit);
+ install_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ install_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
+ iphb->add_child(install_path);
+
+ // status icon
+ status_rect = memnew(TextureRect);
+ status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
+ pphb->add_child(status_rect);
+
+ browse = memnew(Button);
+ browse->set_text(TTR("Browse"));
+ browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_path));
+ pphb->add_child(browse);
+
+ // install status icon
+ install_status_rect = memnew(TextureRect);
+ install_status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
+ iphb->add_child(install_status_rect);
+
+ install_browse = memnew(Button);
+ install_browse->set_text(TTR("Browse"));
+ install_browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_install_path));
+ iphb->add_child(install_browse);
+
+ msg = memnew(Label);
+ msg->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
+ msg->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
+ vb->add_child(msg);
+
+ // Renderer selection.
+ renderer_container = memnew(VBoxContainer);
+ vb->add_child(renderer_container);
+ l = memnew(Label);
+ l->set_text(TTR("Renderer:"));
+ renderer_container->add_child(l);
+ HBoxContainer *rshc = memnew(HBoxContainer);
+ renderer_container->add_child(rshc);
+ renderer_button_group.instantiate();
+
+ // Left hand side, used for checkboxes to select renderer.
+ Container *rvb = memnew(VBoxContainer);
+ rshc->add_child(rvb);
+
+ String default_renderer_type = "forward_plus";
+ if (EditorSettings::get_singleton()->has_setting("project_manager/default_renderer")) {
+ default_renderer_type = EditorSettings::get_singleton()->get_setting("project_manager/default_renderer");
+ }
+
+ Button *rs_button = memnew(CheckBox);
+ rs_button->set_button_group(renderer_button_group);
+ rs_button->set_text(TTR("Forward+"));
+#if defined(WEB_ENABLED)
+ rs_button->set_disabled(true);
+#endif
+ rs_button->set_meta(SNAME("rendering_method"), "forward_plus");
+ rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
+ rvb->add_child(rs_button);
+ if (default_renderer_type == "forward_plus") {
+ rs_button->set_pressed(true);
+ }
+ rs_button = memnew(CheckBox);
+ rs_button->set_button_group(renderer_button_group);
+ rs_button->set_text(TTR("Mobile"));
+#if defined(WEB_ENABLED)
+ rs_button->set_disabled(true);
+#endif
+ rs_button->set_meta(SNAME("rendering_method"), "mobile");
+ rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
+ rvb->add_child(rs_button);
+ if (default_renderer_type == "mobile") {
+ rs_button->set_pressed(true);
+ }
+ rs_button = memnew(CheckBox);
+ rs_button->set_button_group(renderer_button_group);
+ rs_button->set_text(TTR("Compatibility"));
+#if !defined(GLES3_ENABLED)
+ rs_button->set_disabled(true);
+#endif
+ rs_button->set_meta(SNAME("rendering_method"), "gl_compatibility");
+ rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
+ rvb->add_child(rs_button);
+#if defined(GLES3_ENABLED)
+ if (default_renderer_type == "gl_compatibility") {
+ rs_button->set_pressed(true);
+ }
+#endif
+ rshc->add_child(memnew(VSeparator));
+
+ // Right hand side, used for text explaining each choice.
+ rvb = memnew(VBoxContainer);
+ rvb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ rshc->add_child(rvb);
+ renderer_info = memnew(Label);
+ renderer_info->set_modulate(Color(1, 1, 1, 0.7));
+ rvb->add_child(renderer_info);
+ _renderer_selected();
+
+ l = memnew(Label);
+ l->set_text(TTR("The renderer can be changed later, but scenes may need to be adjusted."));
+ // Add some extra spacing to separate it from the list above and the buttons below.
+ l->set_custom_minimum_size(Size2(0, 40) * EDSCALE);
+ l->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
+ l->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
+ l->set_modulate(Color(1, 1, 1, 0.7));
+ renderer_container->add_child(l);
+
+ default_files_container = memnew(HBoxContainer);
+ vb->add_child(default_files_container);
+ l = memnew(Label);
+ l->set_text(TTR("Version Control Metadata:"));
+ default_files_container->add_child(l);
+ vcs_metadata_selection = memnew(OptionButton);
+ vcs_metadata_selection->set_custom_minimum_size(Size2(100, 20));
+ vcs_metadata_selection->add_item(TTR("None"), (int)EditorVCSInterface::VCSMetadata::NONE);
+ vcs_metadata_selection->add_item(TTR("Git"), (int)EditorVCSInterface::VCSMetadata::GIT);
+ vcs_metadata_selection->select((int)EditorVCSInterface::VCSMetadata::GIT);
+ default_files_container->add_child(vcs_metadata_selection);
+ Control *spacer = memnew(Control);
+ spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ default_files_container->add_child(spacer);
+
+ fdialog = memnew(EditorFileDialog);
+ fdialog->set_previews_enabled(false); //Crucial, otherwise the engine crashes.
+ fdialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
+ fdialog_install = memnew(EditorFileDialog);
+ fdialog_install->set_previews_enabled(false); //Crucial, otherwise the engine crashes.
+ fdialog_install->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
+ add_child(fdialog);
+ add_child(fdialog_install);
+
+ project_name->connect("text_changed", callable_mp(this, &ProjectDialog::_text_changed));
+ project_path->connect("text_changed", callable_mp(this, &ProjectDialog::_path_text_changed));
+ install_path->connect("text_changed", callable_mp(this, &ProjectDialog::_update_path));
+ fdialog->connect("dir_selected", callable_mp(this, &ProjectDialog::_path_selected));
+ fdialog->connect("file_selected", callable_mp(this, &ProjectDialog::_file_selected));
+ fdialog_install->connect("dir_selected", callable_mp(this, &ProjectDialog::_install_path_selected));
+ fdialog_install->connect("file_selected", callable_mp(this, &ProjectDialog::_install_path_selected));
+
+ set_hide_on_ok(false);
+
+ dialog_error = memnew(AcceptDialog);
+ add_child(dialog_error);
+}
diff --git a/editor/project_manager/project_dialog.h b/editor/project_manager/project_dialog.h
new file mode 100644
index 0000000000..dcc5cf71f8
--- /dev/null
+++ b/editor/project_manager/project_dialog.h
@@ -0,0 +1,136 @@
+/**************************************************************************/
+/* project_dialog.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef PROJECT_DIALOG_H
+#define PROJECT_DIALOG_H
+
+#include "scene/gui/dialogs.h"
+
+class Button;
+class EditorFileDialog;
+class LineEdit;
+class OptionButton;
+class TextureRect;
+
+class ProjectDialog : public ConfirmationDialog {
+ GDCLASS(ProjectDialog, ConfirmationDialog);
+
+public:
+ enum Mode {
+ MODE_NEW,
+ MODE_IMPORT,
+ MODE_INSTALL,
+ MODE_RENAME,
+ };
+
+private:
+ enum MessageType {
+ MESSAGE_ERROR,
+ MESSAGE_WARNING,
+ MESSAGE_SUCCESS,
+ };
+
+ enum InputType {
+ PROJECT_PATH,
+ INSTALL_PATH,
+ };
+
+ Mode mode = MODE_NEW;
+ bool is_folder_empty = true;
+
+ Button *browse = nullptr;
+ Button *install_browse = nullptr;
+ Button *create_dir = nullptr;
+ VBoxContainer *name_container = nullptr;
+ VBoxContainer *path_container = nullptr;
+ VBoxContainer *install_path_container = nullptr;
+
+ VBoxContainer *renderer_container = nullptr;
+ Label *renderer_info = nullptr;
+ HBoxContainer *default_files_container = nullptr;
+ Ref<ButtonGroup> renderer_button_group;
+
+ Label *msg = nullptr;
+ LineEdit *project_path = nullptr;
+ LineEdit *project_name = nullptr;
+ LineEdit *install_path = nullptr;
+ TextureRect *status_rect = nullptr;
+ TextureRect *install_status_rect = nullptr;
+
+ OptionButton *vcs_metadata_selection = nullptr;
+
+ EditorFileDialog *fdialog = nullptr;
+ EditorFileDialog *fdialog_install = nullptr;
+ AcceptDialog *dialog_error = nullptr;
+
+ String zip_path;
+ String zip_title;
+ String fav_dir;
+
+ String created_folder_path;
+
+ void _set_message(const String &p_msg, MessageType p_type = MESSAGE_SUCCESS, InputType input_type = PROJECT_PATH);
+
+ String _test_path();
+ void _update_path(const String &p_path);
+ void _path_text_changed(const String &p_path);
+ void _path_selected(const String &p_path);
+ void _file_selected(const String &p_path);
+ void _install_path_selected(const String &p_path);
+
+ void _browse_path();
+ void _browse_install_path();
+ void _create_folder();
+
+ void _text_changed(const String &p_text);
+ void _nonempty_confirmation_ok_pressed();
+ void _renderer_selected();
+ void _remove_created_folder();
+
+ void ok_pressed() override;
+ void cancel_pressed() override;
+
+protected:
+ void _notification(int p_what);
+ static void _bind_methods();
+
+public:
+ void set_zip_path(const String &p_path);
+ void set_zip_title(const String &p_title);
+ void set_mode(Mode p_mode);
+ void set_project_path(const String &p_path);
+
+ void ask_for_path_and_show();
+ void show_dialog();
+
+ ProjectDialog();
+};
+
+#endif // PROJECT_DIALOG_H
diff --git a/editor/project_manager/project_list.cpp b/editor/project_manager/project_list.cpp
new file mode 100644
index 0000000000..67aaa85501
--- /dev/null
+++ b/editor/project_manager/project_list.cpp
@@ -0,0 +1,1074 @@
+/**************************************************************************/
+/* project_list.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "project_list.h"
+
+#include "core/config/project_settings.h"
+#include "core/io/dir_access.h"
+#include "editor/editor_paths.h"
+#include "editor/editor_settings.h"
+#include "editor/editor_string_names.h"
+#include "editor/project_manager.h"
+#include "editor/project_manager/project_tag.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/button.h"
+#include "scene/gui/label.h"
+#include "scene/gui/line_edit.h"
+#include "scene/gui/texture_button.h"
+#include "scene/gui/texture_rect.h"
+#include "scene/resources/image_texture.h"
+
+void ProjectListItemControl::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_THEME_CHANGED: {
+ if (icon_needs_reload) {
+ // The project icon may not be loaded by the time the control is displayed,
+ // so use a loading placeholder.
+ project_icon->set_texture(get_editor_theme_icon(SNAME("ProjectIconLoading")));
+ }
+
+ project_title->begin_bulk_theme_override();
+ project_title->add_theme_font_override("font", get_theme_font(SNAME("title"), EditorStringName(EditorFonts)));
+ project_title->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("title_size"), EditorStringName(EditorFonts)));
+ project_title->add_theme_color_override("font_color", get_theme_color(SNAME("font_color"), SNAME("Tree")));
+ project_title->end_bulk_theme_override();
+
+ project_path->add_theme_color_override("font_color", get_theme_color(SNAME("font_color"), SNAME("Tree")));
+ project_unsupported_features->set_texture(get_editor_theme_icon(SNAME("NodeWarning")));
+
+ favorite_button->set_texture_normal(get_editor_theme_icon(SNAME("Favorites")));
+ if (project_is_missing) {
+ explore_button->set_icon(get_editor_theme_icon(SNAME("FileBroken")));
+ } else {
+ explore_button->set_icon(get_editor_theme_icon(SNAME("Load")));
+ }
+ } break;
+
+ case NOTIFICATION_MOUSE_ENTER: {
+ is_hovering = true;
+ queue_redraw();
+ } break;
+
+ case NOTIFICATION_MOUSE_EXIT: {
+ is_hovering = false;
+ queue_redraw();
+ } break;
+
+ case NOTIFICATION_DRAW: {
+ if (is_selected) {
+ draw_style_box(get_theme_stylebox(SNAME("selected"), SNAME("Tree")), Rect2(Point2(), get_size()));
+ }
+ if (is_hovering) {
+ draw_style_box(get_theme_stylebox(SNAME("hover"), SNAME("Tree")), Rect2(Point2(), get_size()));
+ }
+
+ draw_line(Point2(0, get_size().y + 1), Point2(get_size().x, get_size().y + 1), get_theme_color(SNAME("guide_color"), SNAME("Tree")));
+ } break;
+ }
+}
+
+void ProjectListItemControl::_favorite_button_pressed() {
+ emit_signal(SNAME("favorite_pressed"));
+}
+
+void ProjectListItemControl::_explore_button_pressed() {
+ emit_signal(SNAME("explore_pressed"));
+}
+
+void ProjectListItemControl::set_project_title(const String &p_title) {
+ project_title->set_text(p_title);
+}
+
+void ProjectListItemControl::set_project_path(const String &p_path) {
+ project_path->set_text(p_path);
+}
+
+void ProjectListItemControl::set_tags(const PackedStringArray &p_tags, ProjectList *p_parent_list) {
+ for (const String &tag : p_tags) {
+ ProjectTag *tag_control = memnew(ProjectTag(tag));
+ tag_container->add_child(tag_control);
+ tag_control->connect_button_to(callable_mp(p_parent_list, &ProjectList::add_search_tag).bind(tag));
+ }
+}
+
+void ProjectListItemControl::set_project_icon(const Ref<Texture2D> &p_icon) {
+ icon_needs_reload = false;
+
+ // The default project icon is 128×128 to look crisp on hiDPI displays,
+ // but we want the actual displayed size to be 64×64 on loDPI displays.
+ project_icon->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
+ project_icon->set_custom_minimum_size(Size2(64, 64) * EDSCALE);
+ project_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
+
+ project_icon->set_texture(p_icon);
+}
+
+void ProjectListItemControl::set_unsupported_features(PackedStringArray p_features) {
+ if (p_features.size() > 0) {
+ String tooltip_text = "";
+ for (int i = 0; i < p_features.size(); i++) {
+ if (ProjectList::project_feature_looks_like_version(p_features[i])) {
+ tooltip_text += TTR("This project was last edited in a different Godot version: ") + p_features[i] + "\n";
+ p_features.remove_at(i);
+ i--;
+ }
+ }
+ if (p_features.size() > 0) {
+ String unsupported_features_str = String(", ").join(p_features);
+ tooltip_text += TTR("This project uses features unsupported by the current build:") + "\n" + unsupported_features_str;
+ }
+ project_unsupported_features->set_tooltip_text(tooltip_text);
+ project_unsupported_features->show();
+ } else {
+ project_unsupported_features->hide();
+ }
+}
+
+bool ProjectListItemControl::should_load_project_icon() const {
+ return icon_needs_reload;
+}
+
+void ProjectListItemControl::set_selected(bool p_selected) {
+ is_selected = p_selected;
+ queue_redraw();
+}
+
+void ProjectListItemControl::set_is_favorite(bool p_favorite) {
+ favorite_button->set_modulate(p_favorite ? Color(1, 1, 1, 1) : Color(1, 1, 1, 0.2));
+}
+
+void ProjectListItemControl::set_is_missing(bool p_missing) {
+ if (project_is_missing == p_missing) {
+ return;
+ }
+ project_is_missing = p_missing;
+
+ if (project_is_missing) {
+ project_icon->set_modulate(Color(1, 1, 1, 0.5));
+
+ explore_button->set_icon(get_editor_theme_icon(SNAME("FileBroken")));
+ explore_button->set_tooltip_text(TTR("Error: Project is missing on the filesystem."));
+ } else {
+ project_icon->set_modulate(Color(1, 1, 1, 1.0));
+
+ explore_button->set_icon(get_editor_theme_icon(SNAME("Load")));
+#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
+ explore_button->set_tooltip_text(TTR("Show in File Manager"));
+#else
+ // Opening the system file manager is not supported on the Android and web editors.
+ explore_button->hide();
+#endif
+ }
+}
+
+void ProjectListItemControl::set_is_grayed(bool p_grayed) {
+ if (p_grayed) {
+ main_vbox->set_modulate(Color(1, 1, 1, 0.5));
+ // Don't make the icon less prominent if the parent is already grayed out.
+ explore_button->set_modulate(Color(1, 1, 1, 1.0));
+ } else {
+ main_vbox->set_modulate(Color(1, 1, 1, 1.0));
+ explore_button->set_modulate(Color(1, 1, 1, 0.5));
+ }
+}
+
+void ProjectListItemControl::_bind_methods() {
+ ADD_SIGNAL(MethodInfo("favorite_pressed"));
+ ADD_SIGNAL(MethodInfo("explore_pressed"));
+}
+
+ProjectListItemControl::ProjectListItemControl() {
+ set_focus_mode(FocusMode::FOCUS_ALL);
+
+ VBoxContainer *favorite_box = memnew(VBoxContainer);
+ favorite_box->set_alignment(BoxContainer::ALIGNMENT_CENTER);
+ add_child(favorite_box);
+
+ favorite_button = memnew(TextureButton);
+ favorite_button->set_name("FavoriteButton");
+ // This makes the project's "hover" style display correctly when hovering the favorite icon.
+ favorite_button->set_mouse_filter(MOUSE_FILTER_PASS);
+ favorite_box->add_child(favorite_button);
+ favorite_button->connect("pressed", callable_mp(this, &ProjectListItemControl::_favorite_button_pressed));
+
+ project_icon = memnew(TextureRect);
+ project_icon->set_name("ProjectIcon");
+ project_icon->set_v_size_flags(SIZE_SHRINK_CENTER);
+ add_child(project_icon);
+
+ main_vbox = memnew(VBoxContainer);
+ main_vbox->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ add_child(main_vbox);
+
+ Control *ec = memnew(Control);
+ ec->set_custom_minimum_size(Size2(0, 1));
+ ec->set_mouse_filter(MOUSE_FILTER_PASS);
+ main_vbox->add_child(ec);
+
+ // Top half, title, tags and unsupported features labels.
+ {
+ HBoxContainer *title_hb = memnew(HBoxContainer);
+ main_vbox->add_child(title_hb);
+
+ project_title = memnew(Label);
+ project_title->set_auto_translate(false);
+ project_title->set_name("ProjectName");
+ project_title->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ project_title->set_clip_text(true);
+ title_hb->add_child(project_title);
+
+ tag_container = memnew(HBoxContainer);
+ title_hb->add_child(tag_container);
+
+ Control *spacer = memnew(Control);
+ spacer->set_custom_minimum_size(Size2(10, 10));
+ title_hb->add_child(spacer);
+ }
+
+ // Bottom half, containing the path and view folder button.
+ {
+ HBoxContainer *path_hb = memnew(HBoxContainer);
+ path_hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ main_vbox->add_child(path_hb);
+
+ explore_button = memnew(Button);
+ explore_button->set_name("ExploreButton");
+ explore_button->set_flat(true);
+ path_hb->add_child(explore_button);
+ explore_button->connect("pressed", callable_mp(this, &ProjectListItemControl::_explore_button_pressed));
+
+ project_path = memnew(Label);
+ project_path->set_name("ProjectPath");
+ project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
+ project_path->set_clip_text(true);
+ project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ project_path->set_modulate(Color(1, 1, 1, 0.5));
+ path_hb->add_child(project_path);
+
+ project_unsupported_features = memnew(TextureRect);
+ project_unsupported_features->set_name("ProjectUnsupportedFeatures");
+ project_unsupported_features->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
+ path_hb->add_child(project_unsupported_features);
+ project_unsupported_features->hide();
+
+ Control *spacer = memnew(Control);
+ spacer->set_custom_minimum_size(Size2(10, 10));
+ path_hb->add_child(spacer);
+ }
+}
+
+struct ProjectListComparator {
+ ProjectList::FilterOption order_option = ProjectList::FilterOption::EDIT_DATE;
+
+ // operator<
+ _FORCE_INLINE_ bool operator()(const ProjectList::Item &a, const ProjectList::Item &b) const {
+ if (a.favorite && !b.favorite) {
+ return true;
+ }
+ if (b.favorite && !a.favorite) {
+ return false;
+ }
+ switch (order_option) {
+ case ProjectList::PATH:
+ return a.path < b.path;
+ case ProjectList::EDIT_DATE:
+ return a.last_edited > b.last_edited;
+ case ProjectList::TAGS:
+ return a.tag_sort_string < b.tag_sort_string;
+ default:
+ return a.project_name < b.project_name;
+ }
+ }
+};
+
+const char *ProjectList::SIGNAL_LIST_CHANGED = "list_changed";
+const char *ProjectList::SIGNAL_SELECTION_CHANGED = "selection_changed";
+const char *ProjectList::SIGNAL_PROJECT_ASK_OPEN = "project_ask_open";
+
+// Helpers.
+
+bool ProjectList::project_feature_looks_like_version(const String &p_feature) {
+ return p_feature.contains(".") && p_feature.substr(0, 3).is_numeric();
+}
+
+// Notifications.
+
+void ProjectList::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_PROCESS: {
+ // Load icons as a coroutine to speed up launch when you have hundreds of projects
+ if (_icon_load_index < _projects.size()) {
+ Item &item = _projects.write[_icon_load_index];
+ if (item.control->should_load_project_icon()) {
+ _load_project_icon(_icon_load_index);
+ }
+ _icon_load_index++;
+
+ } else {
+ set_process(false);
+ }
+ } break;
+ }
+}
+
+// Initialization & loading.
+
+void ProjectList::_migrate_config() {
+ // Proposal #1637 moved the project list from editor settings to a separate config file
+ // If the new config file doesn't exist, populate it from EditorSettings
+ if (FileAccess::exists(_config_path)) {
+ return;
+ }
+
+ List<PropertyInfo> properties;
+ EditorSettings::get_singleton()->get_property_list(&properties);
+
+ for (const PropertyInfo &E : properties) {
+ // This is actually something like "projects/C:::Documents::Godot::Projects::MyGame"
+ String property_key = E.name;
+ if (!property_key.begins_with("projects/")) {
+ continue;
+ }
+
+ String path = EDITOR_GET(property_key);
+ print_line("Migrating legacy project '" + path + "'.");
+
+ String favoriteKey = "favorite_projects/" + property_key.get_slice("/", 1);
+ bool favorite = EditorSettings::get_singleton()->has_setting(favoriteKey);
+ add_project(path, favorite);
+ if (favorite) {
+ EditorSettings::get_singleton()->erase(favoriteKey);
+ }
+ EditorSettings::get_singleton()->erase(property_key);
+ }
+
+ save_config();
+}
+
+void ProjectList::save_config() {
+ _config.save(_config_path);
+}
+
+// Load project data from p_property_key and return it in a ProjectList::Item.
+// p_favorite is passed directly into the Item.
+ProjectList::Item ProjectList::load_project_data(const String &p_path, bool p_favorite) {
+ String conf = p_path.path_join("project.godot");
+ bool grayed = false;
+ bool missing = false;
+
+ Ref<ConfigFile> cf = memnew(ConfigFile);
+ Error cf_err = cf->load(conf);
+
+ int config_version = 0;
+ String project_name = TTR("Unnamed Project");
+ if (cf_err == OK) {
+ String cf_project_name = cf->get_value("application", "config/name", "");
+ if (!cf_project_name.is_empty()) {
+ project_name = cf_project_name.xml_unescape();
+ }
+ config_version = (int)cf->get_value("", "config_version", 0);
+ }
+
+ if (config_version > ProjectSettings::CONFIG_VERSION) {
+ // Comes from an incompatible (more recent) Godot version, gray it out.
+ grayed = true;
+ }
+
+ const String description = cf->get_value("application", "config/description", "");
+ const PackedStringArray tags = cf->get_value("application", "config/tags", PackedStringArray());
+ const String icon = cf->get_value("application", "config/icon", "");
+ const String main_scene = cf->get_value("application", "run/main_scene", "");
+
+ PackedStringArray project_features = cf->get_value("application", "config/features", PackedStringArray());
+ PackedStringArray unsupported_features = ProjectSettings::get_unsupported_features(project_features);
+
+ uint64_t last_edited = 0;
+ if (cf_err == OK) {
+ // The modification date marks the date the project was last edited.
+ // This is because the `project.godot` file will always be modified
+ // when editing a project (but not when running it).
+ last_edited = FileAccess::get_modified_time(conf);
+
+ String fscache = p_path.path_join(".fscache");
+ if (FileAccess::exists(fscache)) {
+ uint64_t cache_modified = FileAccess::get_modified_time(fscache);
+ if (cache_modified > last_edited) {
+ last_edited = cache_modified;
+ }
+ }
+ } else {
+ grayed = true;
+ missing = true;
+ print_line("Project is missing: " + conf);
+ }
+
+ for (const String &tag : tags) {
+ ProjectManager::get_singleton()->add_new_tag(tag);
+ }
+
+ return Item(project_name, description, tags, p_path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, config_version);
+}
+
+void ProjectList::_update_icons_async() {
+ _icon_load_index = 0;
+ set_process(true);
+}
+
+void ProjectList::_load_project_icon(int p_index) {
+ Item &item = _projects.write[p_index];
+
+ Ref<Texture2D> default_icon = get_editor_theme_icon(SNAME("DefaultProjectIcon"));
+ Ref<Texture2D> icon;
+ if (!item.icon.is_empty()) {
+ Ref<Image> img;
+ img.instantiate();
+ Error err = img->load(item.icon.replace_first("res://", item.path + "/"));
+ if (err == OK) {
+ img->resize(default_icon->get_width(), default_icon->get_height(), Image::INTERPOLATE_LANCZOS);
+ icon = ImageTexture::create_from_image(img);
+ }
+ }
+ if (icon.is_null()) {
+ icon = default_icon;
+ }
+
+ item.control->set_project_icon(icon);
+}
+
+// Project list updates.
+
+void ProjectList::update_project_list() {
+ // This is a full, hard reload of the list. Don't call this unless really required, it's expensive.
+ // If you have 150 projects, it may read through 150 files on your disk at once + load 150 icons.
+ // FIXME: Does it really have to be a full, hard reload? Runtime updates should be made much cheaper.
+
+ // Clear whole list
+ for (int i = 0; i < _projects.size(); ++i) {
+ Item &project = _projects.write[i];
+ CRASH_COND(project.control == nullptr);
+ memdelete(project.control); // Why not queue_free()?
+ }
+ _projects.clear();
+ _last_clicked = "";
+ _selected_project_paths.clear();
+
+ List<String> sections;
+ _config.load(_config_path);
+ _config.get_sections(&sections);
+
+ for (const String &path : sections) {
+ bool favorite = _config.get_value(path, "favorite", false);
+ _projects.push_back(load_project_data(path, favorite));
+ }
+
+ // Create controls
+ for (int i = 0; i < _projects.size(); ++i) {
+ _create_project_item_control(i);
+ }
+
+ sort_projects();
+ _update_icons_async();
+ update_dock_menu();
+
+ set_v_scroll(0);
+ emit_signal(SNAME(SIGNAL_LIST_CHANGED));
+}
+
+void ProjectList::sort_projects() {
+ SortArray<Item, ProjectListComparator> sorter;
+ sorter.compare.order_option = _order_option;
+ sorter.sort(_projects.ptrw(), _projects.size());
+
+ String search_term;
+ PackedStringArray tags;
+
+ if (!_search_term.is_empty()) {
+ PackedStringArray search_parts = _search_term.split(" ");
+ if (search_parts.size() > 1 || search_parts[0].begins_with("tag:")) {
+ PackedStringArray remaining;
+ for (const String &part : search_parts) {
+ if (part.begins_with("tag:")) {
+ tags.push_back(part.get_slice(":", 1));
+ } else {
+ remaining.append(part);
+ }
+ }
+ search_term = String(" ").join(remaining); // Search term without tags.
+ } else {
+ search_term = _search_term;
+ }
+ }
+
+ for (int i = 0; i < _projects.size(); ++i) {
+ Item &item = _projects.write[i];
+
+ bool item_visible = true;
+ if (!_search_term.is_empty()) {
+ String search_path;
+ if (search_term.contains("/")) {
+ // Search path will match the whole path
+ search_path = item.path;
+ } else {
+ // Search path will only match the last path component to make searching more strict
+ search_path = item.path.get_file();
+ }
+
+ bool missing_tags = false;
+ for (const String &tag : tags) {
+ if (!item.tags.has(tag)) {
+ missing_tags = true;
+ break;
+ }
+ }
+
+ // When searching, display projects whose name or path contain the search term and whose tags match the searched tags.
+ item_visible = !missing_tags && (search_term.is_empty() || item.project_name.findn(search_term) != -1 || search_path.findn(search_term) != -1);
+ }
+
+ item.control->set_visible(item_visible);
+ }
+
+ for (int i = 0; i < _projects.size(); ++i) {
+ Item &item = _projects.write[i];
+ item.control->get_parent()->move_child(item.control, i);
+ }
+
+ // Rewind the coroutine because order of projects changed
+ _update_icons_async();
+ update_dock_menu();
+}
+
+int ProjectList::get_project_count() const {
+ return _projects.size();
+}
+
+void ProjectList::find_projects(const String &p_path) {
+ PackedStringArray paths = { p_path };
+ find_projects_multiple(paths);
+}
+
+void ProjectList::find_projects_multiple(const PackedStringArray &p_paths) {
+ List<String> projects;
+
+ for (int i = 0; i < p_paths.size(); i++) {
+ const String &base_path = p_paths.get(i);
+ print_verbose(vformat("Scanning for projects in \"%s\".", base_path));
+
+ _scan_folder_recursive(base_path, &projects);
+ print_verbose(vformat("Found %d project(s).", projects.size()));
+ }
+
+ for (const String &E : projects) {
+ add_project(E, false);
+ }
+
+ save_config();
+ update_project_list();
+}
+
+void ProjectList::_scan_folder_recursive(const String &p_path, List<String> *r_projects) {
+ Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ Error error = da->change_dir(p_path);
+ ERR_FAIL_COND_MSG(error != OK, vformat("Failed to open the path \"%s\" for scanning (code %d).", p_path, error));
+
+ da->list_dir_begin();
+ String n = da->get_next();
+ while (!n.is_empty()) {
+ if (da->current_is_dir() && n[0] != '.') {
+ _scan_folder_recursive(da->get_current_dir().path_join(n), r_projects);
+ } else if (n == "project.godot") {
+ r_projects->push_back(da->get_current_dir());
+ }
+ n = da->get_next();
+ }
+ da->list_dir_end();
+}
+
+// Project list items.
+
+void ProjectList::add_project(const String &dir_path, bool favorite) {
+ if (!_config.has_section(dir_path)) {
+ _config.set_value(dir_path, "favorite", favorite);
+ }
+}
+
+void ProjectList::set_project_version(const String &p_project_path, int p_version) {
+ for (ProjectList::Item &E : _projects) {
+ if (E.path == p_project_path) {
+ E.version = p_version;
+ break;
+ }
+ }
+}
+
+int ProjectList::refresh_project(const String &dir_path) {
+ // Reloads information about a specific project.
+ // If it wasn't loaded and should be in the list, it is added (i.e new project).
+ // If it isn't in the list anymore, it is removed.
+ // If it is in the list but doesn't exist anymore, it is marked as missing.
+
+ bool should_be_in_list = _config.has_section(dir_path);
+ bool is_favourite = _config.get_value(dir_path, "favorite", false);
+
+ bool was_selected = _selected_project_paths.has(dir_path);
+
+ // Remove item in any case
+ for (int i = 0; i < _projects.size(); ++i) {
+ const Item &existing_item = _projects[i];
+ if (existing_item.path == dir_path) {
+ _remove_project(i, false);
+ break;
+ }
+ }
+
+ int index = -1;
+ if (should_be_in_list) {
+ // Recreate it with updated info
+
+ Item item = load_project_data(dir_path, is_favourite);
+
+ _projects.push_back(item);
+ _create_project_item_control(_projects.size() - 1);
+
+ sort_projects();
+
+ for (int i = 0; i < _projects.size(); ++i) {
+ if (_projects[i].path == dir_path) {
+ if (was_selected) {
+ select_project(i);
+ ensure_project_visible(i);
+ }
+ _load_project_icon(i);
+
+ index = i;
+ break;
+ }
+ }
+ }
+
+ return index;
+}
+
+void ProjectList::ensure_project_visible(int p_index) {
+ const Item &item = _projects[p_index];
+ ensure_control_visible(item.control);
+}
+
+void ProjectList::_create_project_item_control(int p_index) {
+ // Will be added last in the list, so make sure indexes match
+ ERR_FAIL_COND(p_index != project_list_vbox->get_child_count());
+
+ Item &item = _projects.write[p_index];
+ ERR_FAIL_COND(item.control != nullptr); // Already created
+
+ ProjectListItemControl *hb = memnew(ProjectListItemControl);
+ hb->add_theme_constant_override("separation", 10 * EDSCALE);
+
+ hb->set_project_title(!item.missing ? item.project_name : TTR("Missing Project"));
+ hb->set_project_path(item.path);
+ hb->set_tooltip_text(item.description);
+ hb->set_tags(item.tags, this);
+ hb->set_unsupported_features(item.unsupported_features.duplicate());
+
+ hb->set_is_favorite(item.favorite);
+ hb->set_is_missing(item.missing);
+ hb->set_is_grayed(item.grayed);
+
+ hb->connect("gui_input", callable_mp(this, &ProjectList::_list_item_input).bind(hb));
+ hb->connect("favorite_pressed", callable_mp(this, &ProjectList::_on_favorite_pressed).bind(hb));
+
+#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
+ hb->connect("explore_pressed", callable_mp(this, &ProjectList::_on_explore_pressed).bind(item.path));
+#endif
+
+ project_list_vbox->add_child(hb);
+ item.control = hb;
+}
+
+void ProjectList::_toggle_project(int p_index) {
+ // This methods adds to the selection or removes from the
+ // selection.
+ Item &item = _projects.write[p_index];
+
+ if (_selected_project_paths.has(item.path)) {
+ _deselect_project_nocheck(p_index);
+ } else {
+ _select_project_nocheck(p_index);
+ }
+}
+
+void ProjectList::_remove_project(int p_index, bool p_update_config) {
+ const Item item = _projects[p_index]; // Take a copy
+
+ _selected_project_paths.erase(item.path);
+
+ if (_last_clicked == item.path) {
+ _last_clicked = "";
+ }
+
+ memdelete(item.control);
+ _projects.remove_at(p_index);
+
+ if (p_update_config) {
+ _config.erase_section(item.path);
+ // Not actually saving the file, in case you are doing more changes to settings
+ }
+
+ update_dock_menu();
+}
+
+void ProjectList::_list_item_input(const Ref<InputEvent> &p_ev, Node *p_hb) {
+ Ref<InputEventMouseButton> mb = p_ev;
+ int clicked_index = p_hb->get_index();
+ const Item &clicked_project = _projects[clicked_index];
+
+ if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
+ if (mb->is_shift_pressed() && _selected_project_paths.size() > 0 && !_last_clicked.is_empty() && clicked_project.path != _last_clicked) {
+ int anchor_index = -1;
+ for (int i = 0; i < _projects.size(); ++i) {
+ const Item &p = _projects[i];
+ if (p.path == _last_clicked) {
+ anchor_index = p.control->get_index();
+ break;
+ }
+ }
+ CRASH_COND(anchor_index == -1);
+ _select_project_range(anchor_index, clicked_index);
+
+ } else if (mb->is_command_or_control_pressed()) {
+ _toggle_project(clicked_index);
+
+ } else {
+ _last_clicked = clicked_project.path;
+ select_project(clicked_index);
+ }
+
+ emit_signal(SNAME(SIGNAL_SELECTION_CHANGED));
+
+ // Do not allow opening a project more than once using a single project manager instance.
+ // Opening the same project in several editor instances at once can lead to various issues.
+ if (!mb->is_command_or_control_pressed() && mb->is_double_click() && !project_opening_initiated) {
+ emit_signal(SNAME(SIGNAL_PROJECT_ASK_OPEN));
+ }
+ }
+}
+
+void ProjectList::_on_favorite_pressed(Node *p_hb) {
+ ProjectListItemControl *control = Object::cast_to<ProjectListItemControl>(p_hb);
+
+ int index = control->get_index();
+ Item item = _projects.write[index]; // Take copy
+
+ item.favorite = !item.favorite;
+
+ _config.set_value(item.path, "favorite", item.favorite);
+ save_config();
+
+ _projects.write[index] = item;
+
+ control->set_is_favorite(item.favorite);
+
+ sort_projects();
+
+ if (item.favorite) {
+ for (int i = 0; i < _projects.size(); ++i) {
+ if (_projects[i].path == item.path) {
+ ensure_project_visible(i);
+ break;
+ }
+ }
+ }
+
+ update_dock_menu();
+}
+
+void ProjectList::_on_explore_pressed(const String &p_path) {
+ OS::get_singleton()->shell_show_in_file_manager(p_path, true);
+}
+
+// Project list selection.
+
+void ProjectList::_clear_project_selection() {
+ Vector<Item> previous_selected_items = get_selected_projects();
+ _selected_project_paths.clear();
+
+ for (int i = 0; i < previous_selected_items.size(); ++i) {
+ previous_selected_items[i].control->set_selected(false);
+ }
+}
+
+void ProjectList::_select_project_nocheck(int p_index) {
+ Item &item = _projects.write[p_index];
+ _selected_project_paths.insert(item.path);
+ item.control->set_selected(true);
+}
+
+void ProjectList::_deselect_project_nocheck(int p_index) {
+ Item &item = _projects.write[p_index];
+ _selected_project_paths.erase(item.path);
+ item.control->set_selected(false);
+}
+
+inline void _sort_project_range(int &a, int &b) {
+ if (a > b) {
+ int temp = a;
+ a = b;
+ b = temp;
+ }
+}
+
+void ProjectList::_select_project_range(int p_begin, int p_end) {
+ _clear_project_selection();
+
+ _sort_project_range(p_begin, p_end);
+ for (int i = p_begin; i <= p_end; ++i) {
+ _select_project_nocheck(i);
+ }
+}
+
+void ProjectList::select_project(int p_index) {
+ // This method keeps only one project selected.
+ _clear_project_selection();
+ _select_project_nocheck(p_index);
+}
+
+void ProjectList::select_first_visible_project() {
+ _clear_project_selection();
+
+ for (int i = 0; i < _projects.size(); i++) {
+ if (_projects[i].control->is_visible()) {
+ _select_project_nocheck(i);
+ break;
+ }
+ }
+}
+
+Vector<ProjectList::Item> ProjectList::get_selected_projects() const {
+ Vector<Item> items;
+ if (_selected_project_paths.size() == 0) {
+ return items;
+ }
+ items.resize(_selected_project_paths.size());
+ int j = 0;
+ for (int i = 0; i < _projects.size(); ++i) {
+ const Item &item = _projects[i];
+ if (_selected_project_paths.has(item.path)) {
+ items.write[j++] = item;
+ }
+ }
+ ERR_FAIL_COND_V(j != items.size(), items);
+ return items;
+}
+
+const HashSet<String> &ProjectList::get_selected_project_keys() const {
+ // Faster if that's all you need
+ return _selected_project_paths;
+}
+
+int ProjectList::get_single_selected_index() const {
+ if (_selected_project_paths.size() == 0) {
+ // Default selection
+ return 0;
+ }
+ String key;
+ if (_selected_project_paths.size() == 1) {
+ // Only one selected
+ key = *_selected_project_paths.begin();
+ } else {
+ // Multiple selected, consider the last clicked one as "main"
+ key = _last_clicked;
+ }
+ for (int i = 0; i < _projects.size(); ++i) {
+ if (_projects[i].path == key) {
+ return i;
+ }
+ }
+ return 0;
+}
+
+void ProjectList::erase_selected_projects(bool p_delete_project_contents) {
+ if (_selected_project_paths.size() == 0) {
+ return;
+ }
+
+ for (int i = 0; i < _projects.size(); ++i) {
+ Item &item = _projects.write[i];
+ if (_selected_project_paths.has(item.path) && item.control->is_visible()) {
+ _config.erase_section(item.path);
+
+ // Comment out for now until we have a better warning system to
+ // ensure users delete their project only.
+ //if (p_delete_project_contents) {
+ // OS::get_singleton()->move_to_trash(item.path);
+ //}
+
+ memdelete(item.control);
+ _projects.remove_at(i);
+ --i;
+ }
+ }
+
+ save_config();
+ _selected_project_paths.clear();
+ _last_clicked = "";
+
+ update_dock_menu();
+}
+
+// Missing projects.
+
+bool ProjectList::is_any_project_missing() const {
+ for (int i = 0; i < _projects.size(); ++i) {
+ if (_projects[i].missing) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void ProjectList::erase_missing_projects() {
+ if (_projects.is_empty()) {
+ return;
+ }
+
+ int deleted_count = 0;
+ int remaining_count = 0;
+
+ for (int i = 0; i < _projects.size(); ++i) {
+ const Item &item = _projects[i];
+
+ if (item.missing) {
+ _remove_project(i, true);
+ --i;
+ ++deleted_count;
+
+ } else {
+ ++remaining_count;
+ }
+ }
+
+ print_line("Removed " + itos(deleted_count) + " projects from the list, remaining " + itos(remaining_count) + " projects");
+ save_config();
+}
+
+// Project list sorting and filtering.
+
+void ProjectList::set_search_term(String p_search_term) {
+ _search_term = p_search_term;
+}
+
+void ProjectList::add_search_tag(const String &p_tag) {
+ const String tag_string = "tag:" + p_tag;
+
+ int exists = _search_term.find(tag_string);
+ if (exists > -1) {
+ _search_term = _search_term.erase(exists, tag_string.length() + 1);
+ } else if (_search_term.is_empty() || _search_term.ends_with(" ")) {
+ _search_term += tag_string;
+ } else {
+ _search_term += " " + tag_string;
+ }
+ ProjectManager::get_singleton()->get_search_box()->set_text(_search_term);
+
+ sort_projects();
+}
+
+void ProjectList::set_order_option(int p_option) {
+ FilterOption selected = (FilterOption)p_option;
+ EditorSettings::get_singleton()->set("project_manager/sorting_order", p_option);
+ EditorSettings::get_singleton()->save();
+ _order_option = selected;
+
+ sort_projects();
+}
+
+// Global menu integration.
+
+void ProjectList::update_dock_menu() {
+ if (!DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_GLOBAL_MENU)) {
+ return;
+ }
+ DisplayServer::get_singleton()->global_menu_clear("_dock");
+
+ int favs_added = 0;
+ int total_added = 0;
+ for (int i = 0; i < _projects.size(); ++i) {
+ if (!_projects[i].grayed && !_projects[i].missing) {
+ if (_projects[i].favorite) {
+ favs_added++;
+ } else {
+ if (favs_added != 0) {
+ DisplayServer::get_singleton()->global_menu_add_separator("_dock");
+ }
+ favs_added = 0;
+ }
+ 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);
+ total_added++;
+ }
+ }
+ if (total_added != 0) {
+ DisplayServer::get_singleton()->global_menu_add_separator("_dock");
+ }
+ DisplayServer::get_singleton()->global_menu_add_item("_dock", TTR("New Window"), callable_mp(this, &ProjectList::_global_menu_new_window));
+}
+
+void ProjectList::_global_menu_new_window(const Variant &p_tag) {
+ List<String> args;
+ args.push_back("-p");
+ OS::get_singleton()->create_instance(args);
+}
+
+void ProjectList::_global_menu_open_project(const Variant &p_tag) {
+ int idx = (int)p_tag;
+
+ if (idx >= 0 && idx < _projects.size()) {
+ String conf = _projects[idx].path.path_join("project.godot");
+ List<String> args;
+ args.push_back(conf);
+ OS::get_singleton()->create_instance(args);
+ }
+}
+
+// Object methods.
+
+void ProjectList::_bind_methods() {
+ ADD_SIGNAL(MethodInfo(SIGNAL_LIST_CHANGED));
+ ADD_SIGNAL(MethodInfo(SIGNAL_SELECTION_CHANGED));
+ ADD_SIGNAL(MethodInfo(SIGNAL_PROJECT_ASK_OPEN));
+}
+
+ProjectList::ProjectList() {
+ project_list_vbox = memnew(VBoxContainer);
+ project_list_vbox->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ add_child(project_list_vbox);
+
+ _config_path = EditorPaths::get_singleton()->get_data_dir().path_join("projects.cfg");
+ _migrate_config();
+}
diff --git a/editor/project_manager/project_list.h b/editor/project_manager/project_list.h
new file mode 100644
index 0000000000..86f1f13bd8
--- /dev/null
+++ b/editor/project_manager/project_list.h
@@ -0,0 +1,264 @@
+/**************************************************************************/
+/* project_list.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef PROJECT_LIST_H
+#define PROJECT_LIST_H
+
+#include "core/io/config_file.h"
+#include "scene/gui/box_container.h"
+#include "scene/gui/scroll_container.h"
+
+class Button;
+class Label;
+class ProjectList;
+class TextureButton;
+class TextureRect;
+
+class ProjectListItemControl : public HBoxContainer {
+ GDCLASS(ProjectListItemControl, HBoxContainer)
+
+ VBoxContainer *main_vbox = nullptr;
+ TextureButton *favorite_button = nullptr;
+ Button *explore_button = nullptr;
+
+ TextureRect *project_icon = nullptr;
+ Label *project_title = nullptr;
+ Label *project_path = nullptr;
+ TextureRect *project_unsupported_features = nullptr;
+ HBoxContainer *tag_container = nullptr;
+
+ bool project_is_missing = false;
+ bool icon_needs_reload = true;
+ bool is_selected = false;
+ bool is_hovering = false;
+
+ void _favorite_button_pressed();
+ void _explore_button_pressed();
+
+protected:
+ void _notification(int p_what);
+ static void _bind_methods();
+
+public:
+ void set_project_title(const String &p_title);
+ void set_project_path(const String &p_path);
+ void set_tags(const PackedStringArray &p_tags, ProjectList *p_parent_list);
+ void set_project_icon(const Ref<Texture2D> &p_icon);
+ void set_unsupported_features(PackedStringArray p_features);
+
+ bool should_load_project_icon() const;
+ void set_selected(bool p_selected);
+
+ void set_is_favorite(bool p_favorite);
+ void set_is_missing(bool p_missing);
+ void set_is_grayed(bool p_grayed);
+
+ ProjectListItemControl();
+};
+
+class ProjectList : public ScrollContainer {
+ GDCLASS(ProjectList, ScrollContainer)
+
+ friend class ProjectManager;
+
+public:
+ enum FilterOption {
+ EDIT_DATE,
+ NAME,
+ PATH,
+ TAGS,
+ };
+
+ // Can often be passed by copy.
+ struct Item {
+ String project_name;
+ String description;
+ PackedStringArray tags;
+ String tag_sort_string;
+ String path;
+ String icon;
+ String main_scene;
+ PackedStringArray unsupported_features;
+ uint64_t last_edited = 0;
+ bool favorite = false;
+ bool grayed = false;
+ bool missing = false;
+ int version = 0;
+
+ ProjectListItemControl *control = nullptr;
+
+ Item() {}
+
+ Item(const String &p_name,
+ const String &p_description,
+ const PackedStringArray &p_tags,
+ const String &p_path,
+ const String &p_icon,
+ const String &p_main_scene,
+ const PackedStringArray &p_unsupported_features,
+ uint64_t p_last_edited,
+ bool p_favorite,
+ bool p_grayed,
+ bool p_missing,
+ int p_version) {
+ project_name = p_name;
+ description = p_description;
+ tags = p_tags;
+ path = p_path;
+ icon = p_icon;
+ main_scene = p_main_scene;
+ unsupported_features = p_unsupported_features;
+ last_edited = p_last_edited;
+ favorite = p_favorite;
+ grayed = p_grayed;
+ missing = p_missing;
+ version = p_version;
+
+ control = nullptr;
+
+ PackedStringArray sorted_tags = tags;
+ sorted_tags.sort();
+ tag_sort_string = String().join(sorted_tags);
+ }
+
+ _FORCE_INLINE_ bool operator==(const Item &l) const {
+ return path == l.path;
+ }
+ };
+
+private:
+ String _config_path;
+ ConfigFile _config;
+
+ Vector<Item> _projects;
+
+ int _icon_load_index = 0;
+ bool project_opening_initiated = false;
+
+ String _search_term;
+ FilterOption _order_option = FilterOption::EDIT_DATE;
+ HashSet<String> _selected_project_paths;
+ String _last_clicked; // Project key
+
+ VBoxContainer *project_list_vbox = nullptr;
+
+ // Initialization & loading.
+
+ void _migrate_config();
+
+ static Item load_project_data(const String &p_property_key, bool p_favorite);
+ void _update_icons_async();
+ void _load_project_icon(int p_index);
+
+ // Project list updates.
+
+ void _scan_folder_recursive(const String &p_path, List<String> *r_projects);
+
+ // Project list items.
+
+ void _create_project_item_control(int p_index);
+ void _toggle_project(int p_index);
+ void _remove_project(int p_index, bool p_update_settings);
+
+ void _list_item_input(const Ref<InputEvent> &p_ev, Node *p_hb);
+ void _on_favorite_pressed(Node *p_hb);
+ void _on_explore_pressed(const String &p_path);
+
+ // Project list selection.
+
+ void _clear_project_selection();
+ void _select_project_nocheck(int p_index);
+ void _deselect_project_nocheck(int p_index);
+ void _select_project_range(int p_begin, int p_end);
+
+ // Global menu integration.
+
+ void _global_menu_new_window(const Variant &p_tag);
+ void _global_menu_open_project(const Variant &p_tag);
+
+protected:
+ void _notification(int p_what);
+ static void _bind_methods();
+
+public:
+ static const char *SIGNAL_LIST_CHANGED;
+ static const char *SIGNAL_SELECTION_CHANGED;
+ static const char *SIGNAL_PROJECT_ASK_OPEN;
+
+ static bool project_feature_looks_like_version(const String &p_feature);
+
+ // Initialization & loading.
+
+ void save_config();
+
+ // Project list updates.
+
+ void update_project_list();
+ void sort_projects();
+ int get_project_count() const;
+
+ void find_projects(const String &p_path);
+ void find_projects_multiple(const PackedStringArray &p_paths);
+
+ // Project list items.
+
+ void add_project(const String &dir_path, bool favorite);
+ void set_project_version(const String &p_project_path, int version);
+ int refresh_project(const String &dir_path);
+ void ensure_project_visible(int p_index);
+
+ // Project list selection.
+
+ void select_project(int p_index);
+ void select_first_visible_project();
+ Vector<Item> get_selected_projects() const;
+ const HashSet<String> &get_selected_project_keys() const;
+ int get_single_selected_index() const;
+ void erase_selected_projects(bool p_delete_project_contents);
+
+ // Missing projects.
+
+ bool is_any_project_missing() const;
+ void erase_missing_projects();
+
+ // Project list sorting and filtering.
+
+ void set_search_term(String p_search_term);
+ void add_search_tag(const String &p_tag);
+ void set_order_option(int p_option);
+
+ // Global menu integration.
+
+ void update_dock_menu();
+
+ ProjectList();
+};
+
+#endif // PROJECT_LIST_H
diff --git a/editor/project_manager/project_tag.cpp b/editor/project_manager/project_tag.cpp
new file mode 100644
index 0000000000..de9213177b
--- /dev/null
+++ b/editor/project_manager/project_tag.cpp
@@ -0,0 +1,74 @@
+/**************************************************************************/
+/* project_tag.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "project_tag.h"
+
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/button.h"
+#include "scene/gui/color_rect.h"
+
+void ProjectTag::_notification(int p_what) {
+ if (display_close && p_what == NOTIFICATION_THEME_CHANGED) {
+ button->set_icon(get_theme_icon(SNAME("close"), SNAME("TabBar")));
+ }
+}
+
+void ProjectTag::connect_button_to(const Callable &p_callable) {
+ button->connect(SNAME("pressed"), p_callable, CONNECT_DEFERRED);
+}
+
+const String ProjectTag::get_tag() const {
+ return tag_string;
+}
+
+ProjectTag::ProjectTag(const String &p_text, bool p_display_close) {
+ add_theme_constant_override(SNAME("separation"), 0);
+ set_v_size_flags(SIZE_SHRINK_CENTER);
+ tag_string = p_text;
+ display_close = p_display_close;
+
+ Color tag_color = Color(1, 0, 0);
+ tag_color.set_ok_hsl_s(0.8);
+ tag_color.set_ok_hsl_h(float(p_text.hash() * 10001 % UINT32_MAX) / float(UINT32_MAX));
+ set_self_modulate(tag_color);
+
+ ColorRect *cr = memnew(ColorRect);
+ add_child(cr);
+ cr->set_custom_minimum_size(Vector2(4, 0) * EDSCALE);
+ cr->set_color(tag_color);
+
+ button = memnew(Button);
+ add_child(button);
+ button->set_auto_translate(false);
+ button->set_text(p_text.capitalize());
+ button->set_focus_mode(FOCUS_NONE);
+ button->set_icon_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
+ button->set_theme_type_variation(SNAME("ProjectTag"));
+}
diff --git a/editor/project_manager/project_tag.h b/editor/project_manager/project_tag.h
new file mode 100644
index 0000000000..2cd15f64b9
--- /dev/null
+++ b/editor/project_manager/project_tag.h
@@ -0,0 +1,56 @@
+/**************************************************************************/
+/* project_tag.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef PROJECT_TAG_H
+#define PROJECT_TAG_H
+
+#include "scene/gui/box_container.h"
+
+class Button;
+
+class ProjectTag : public HBoxContainer {
+ GDCLASS(ProjectTag, HBoxContainer);
+
+ String tag_string;
+ bool display_close = false;
+
+ Button *button = nullptr;
+
+protected:
+ void _notification(int p_what);
+
+public:
+ void connect_button_to(const Callable &p_callable);
+ const String get_tag() const;
+
+ ProjectTag(const String &p_text, bool p_display_close = false);
+};
+
+#endif // PROJECT_TAG_H
diff --git a/editor/themes/editor_fonts.cpp b/editor/themes/editor_fonts.cpp
index fc3631653c..ee61387702 100644
--- a/editor/themes/editor_fonts.cpp
+++ b/editor/themes/editor_fonts.cpp
@@ -107,7 +107,6 @@ Ref<FontVariation> make_bold_font(const Ref<Font> &p_font, double p_embolden, Ty
}
void editor_register_fonts(const Ref<Theme> &p_theme) {
- OS::get_singleton()->benchmark_begin_measure("EditorTheme", "Register Fonts");
Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
TextServer::FontAntialiasing font_antialiasing = (TextServer::FontAntialiasing)(int)EDITOR_GET("interface/editor/font_antialiasing");
@@ -444,6 +443,4 @@ void editor_register_fonts(const Ref<Theme> &p_theme) {
p_theme->set_font_size("status_source_size", EditorStringName(EditorFonts), default_font_size);
p_theme->set_font("status_source", EditorStringName(EditorFonts), mono_other_fc);
-
- OS::get_singleton()->benchmark_end_measure("EditorTheme", "Register Fonts");
}
diff --git a/editor/themes/editor_icons.cpp b/editor/themes/editor_icons.cpp
index bb767747f3..f318b640d0 100644
--- a/editor/themes/editor_icons.cpp
+++ b/editor/themes/editor_icons.cpp
@@ -78,9 +78,8 @@ Ref<ImageTexture> editor_generate_icon(int p_index, float p_scale, float p_satur
return ImageTexture::create_from_image(img);
}
-float get_gizmo_handle_scale(const String &gizmo_handle_name = "") {
- const float scale_gizmo_handles_for_touch = EDITOR_GET("interface/touchscreen/scale_gizmo_handles");
- if (scale_gizmo_handles_for_touch > 1.0f) {
+float get_gizmo_handle_scale(const String &p_gizmo_handle_name, float p_gizmo_handle_scale) {
+ if (p_gizmo_handle_scale > 1.0f) {
// The names of the icons that require additional scaling.
static HashSet<StringName> gizmo_to_scale;
if (gizmo_to_scale.is_empty()) {
@@ -92,18 +91,15 @@ float get_gizmo_handle_scale(const String &gizmo_handle_name = "") {
gizmo_to_scale.insert("EditorPathSmoothHandle");
}
- if (gizmo_to_scale.has(gizmo_handle_name)) {
- return EDSCALE * scale_gizmo_handles_for_touch;
+ if (gizmo_to_scale.has(p_gizmo_handle_name)) {
+ return EDSCALE * p_gizmo_handle_scale;
}
}
return EDSCALE;
}
-void editor_register_icons(const Ref<Theme> &p_theme, bool p_dark_theme, float p_icon_saturation, int p_thumb_size, bool p_only_thumbs) {
- const String benchmark_key = vformat("Generate Icons (%s)", (p_only_thumbs ? "Only Thumbs" : "All"));
- OS::get_singleton()->benchmark_begin_measure("EditorTheme", benchmark_key);
-
+void editor_register_icons(const Ref<Theme> &p_theme, bool p_dark_theme, float p_icon_saturation, int p_thumb_size, float p_gizmo_handle_scale) {
// Before we register the icons, we adjust their colors and saturation.
// Most icons follow the standard rules for color conversion to follow the editor
// theme's polarity (dark/light). We also adjust the saturation for most icons,
@@ -158,13 +154,13 @@ void editor_register_icons(const Ref<Theme> &p_theme, bool p_dark_theme, float p
accent_color_icons.insert("PlayOverlay");
// Generate icons.
- if (!p_only_thumbs) {
+ {
for (int i = 0; i < editor_icons_count; i++) {
Ref<ImageTexture> icon;
const String &editor_icon_name = editor_icons_names[i];
if (accent_color_icons.has(editor_icon_name)) {
- icon = editor_generate_icon(i, get_gizmo_handle_scale(editor_icon_name), 1.0, accent_color_map);
+ icon = editor_generate_icon(i, get_gizmo_handle_scale(editor_icon_name, p_gizmo_handle_scale), 1.0, accent_color_map);
} else {
float saturation = p_icon_saturation;
if (saturation_exceptions.has(editor_icon_name)) {
@@ -172,9 +168,9 @@ void editor_register_icons(const Ref<Theme> &p_theme, bool p_dark_theme, float p
}
if (conversion_exceptions.has(editor_icon_name)) {
- icon = editor_generate_icon(i, get_gizmo_handle_scale(editor_icon_name), saturation);
+ icon = editor_generate_icon(i, get_gizmo_handle_scale(editor_icon_name, p_gizmo_handle_scale), saturation);
} else {
- icon = editor_generate_icon(i, get_gizmo_handle_scale(editor_icon_name), saturation, color_conversion_map);
+ icon = editor_generate_icon(i, get_gizmo_handle_scale(editor_icon_name, p_gizmo_handle_scale), saturation, color_conversion_map);
}
}
@@ -231,7 +227,6 @@ void editor_register_icons(const Ref<Theme> &p_theme, bool p_dark_theme, float p
p_theme->set_icon(editor_icons_names[index], EditorStringName(EditorIcons), icon);
}
}
- OS::get_singleton()->benchmark_end_measure("EditorTheme", benchmark_key);
}
void editor_copy_icons(const Ref<Theme> &p_theme, const Ref<Theme> &p_old_theme) {
diff --git a/editor/themes/editor_icons.h b/editor/themes/editor_icons.h
index 2094ebf27c..447057b5e4 100644
--- a/editor/themes/editor_icons.h
+++ b/editor/themes/editor_icons.h
@@ -34,7 +34,7 @@
#include "scene/resources/theme.h"
void editor_configure_icons(bool p_dark_theme);
-void editor_register_icons(const Ref<Theme> &p_theme, bool p_dark_theme, float p_icon_saturation, int p_thumb_size, bool p_only_thumbs = false);
+void editor_register_icons(const Ref<Theme> &p_theme, bool p_dark_theme, float p_icon_saturation, int p_thumb_size, float p_gizmo_handle_scale);
void editor_copy_icons(const Ref<Theme> &p_theme, const Ref<Theme> &p_old_theme);
String get_default_project_icon();
diff --git a/editor/themes/editor_theme.h b/editor/themes/editor_theme.h
index 41a60fdf96..2cc7ad287e 100644
--- a/editor/themes/editor_theme.h
+++ b/editor/themes/editor_theme.h
@@ -38,6 +38,10 @@ class EditorTheme : public Theme {
static Vector<StringName> editor_theme_types;
+ uint32_t generated_hash = 0;
+ uint32_t generated_fonts_hash = 0;
+ uint32_t generated_icons_hash = 0;
+
public:
virtual Color get_color(const StringName &p_name, const StringName &p_theme_type) const override;
virtual int get_constant(const StringName &p_name, const StringName &p_theme_type) const override;
@@ -46,6 +50,15 @@ public:
virtual Ref<Texture2D> get_icon(const StringName &p_name, const StringName &p_theme_type) const override;
virtual Ref<StyleBox> get_stylebox(const StringName &p_name, const StringName &p_theme_type) const override;
+ void set_generated_hash(uint32_t p_hash) { generated_hash = p_hash; }
+ uint32_t get_generated_hash() const { return generated_hash; }
+
+ void set_generated_fonts_hash(uint32_t p_hash) { generated_fonts_hash = p_hash; }
+ uint32_t get_generated_fonts_hash() const { return generated_fonts_hash; }
+
+ void set_generated_icons_hash(uint32_t p_hash) { generated_icons_hash = p_hash; }
+ uint32_t get_generated_icons_hash() const { return generated_icons_hash; }
+
static void initialize();
static void finalize();
};
diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp
index 4ce323c763..5eb287cc43 100644
--- a/editor/themes/editor_theme_manager.cpp
+++ b/editor/themes/editor_theme_manager.cpp
@@ -45,7 +45,81 @@
#include "scene/resources/style_box_texture.h"
#include "scene/resources/texture.h"
-// Helper methods.
+// Theme configuration.
+
+uint32_t EditorThemeManager::ThemeConfiguration::hash() {
+ uint32_t hash = hash_murmur3_one_float(EDSCALE);
+
+ // Basic properties.
+
+ hash = hash_murmur3_one_32(preset.hash(), hash);
+ hash = hash_murmur3_one_32(spacing_preset.hash(), hash);
+
+ hash = hash_murmur3_one_32(base_color.to_rgba32(), hash);
+ hash = hash_murmur3_one_32(accent_color.to_rgba32(), hash);
+ hash = hash_murmur3_one_float(contrast, hash);
+ hash = hash_murmur3_one_float(icon_saturation, hash);
+
+ // Extra properties.
+
+ hash = hash_murmur3_one_32(base_spacing, hash);
+ hash = hash_murmur3_one_32(extra_spacing, hash);
+ hash = hash_murmur3_one_32(border_width, hash);
+ hash = hash_murmur3_one_32(corner_radius, hash);
+
+ hash = hash_murmur3_one_32((int)draw_extra_borders, hash);
+ hash = hash_murmur3_one_float(relationship_line_opacity, hash);
+ hash = hash_murmur3_one_32(thumb_size, hash);
+ hash = hash_murmur3_one_32(class_icon_size, hash);
+ hash = hash_murmur3_one_32((int)increase_scrollbar_touch_area, hash);
+ hash = hash_murmur3_one_float(gizmo_handle_scale, hash);
+ hash = hash_murmur3_one_32(color_picker_button_height, hash);
+ hash = hash_murmur3_one_float(subresource_hue_tint, hash);
+
+ hash = hash_murmur3_one_float(default_contrast, hash);
+
+ // Generated properties.
+
+ hash = hash_murmur3_one_32((int)dark_theme, hash);
+
+ return hash;
+}
+
+uint32_t EditorThemeManager::ThemeConfiguration::hash_fonts() {
+ uint32_t hash = hash_murmur3_one_float(EDSCALE);
+
+ // TODO: Implement the hash based on what editor_register_fonts() uses.
+
+ return hash;
+}
+
+uint32_t EditorThemeManager::ThemeConfiguration::hash_icons() {
+ uint32_t hash = hash_murmur3_one_float(EDSCALE);
+
+ hash = hash_murmur3_one_32(accent_color.to_rgba32(), hash);
+ hash = hash_murmur3_one_float(icon_saturation, hash);
+
+ hash = hash_murmur3_one_32(thumb_size, hash);
+ hash = hash_murmur3_one_float(gizmo_handle_scale, hash);
+
+ hash = hash_murmur3_one_32((int)dark_theme, hash);
+
+ return hash;
+}
+
+// Benchmarks.
+
+int EditorThemeManager::benchmark_run = 0;
+
+String EditorThemeManager::get_benchmark_key() {
+ if (benchmark_run == 0) {
+ return "EditorTheme (Startup)";
+ }
+
+ return vformat("EditorTheme (Run %d)", benchmark_run);
+}
+
+// Generation helper methods.
Ref<StyleBoxTexture> make_stylebox(Ref<Texture2D> p_texture, float p_left, float p_top, float p_right, float p_bottom, float p_margin_left = -1, float p_margin_top = -1, float p_margin_right = -1, float p_margin_bottom = -1, bool p_draw_center = true) {
Ref<StyleBoxTexture> style(memnew(StyleBoxTexture));
@@ -86,73 +160,71 @@ Ref<StyleBoxLine> make_line_stylebox(Color p_color, int p_thickness = 1, float p
// Theme generation and population routines.
-Ref<Theme> EditorThemeManager::_create_base_theme(const Ref<Theme> &p_old_theme) {
- OS::get_singleton()->benchmark_begin_measure("EditorTheme", "Create Base Theme");
+Ref<EditorTheme> EditorThemeManager::_create_base_theme(const Ref<EditorTheme> &p_old_theme) {
+ OS::get_singleton()->benchmark_begin_measure(get_benchmark_key(), "Create Base Theme");
Ref<EditorTheme> theme = memnew(EditorTheme);
ThemeConfiguration config = _create_theme_config(theme);
+ theme->set_generated_hash(config.hash());
+ theme->set_generated_fonts_hash(config.hash_fonts());
+ theme->set_generated_icons_hash(config.hash_icons());
+
+ print_verbose(vformat("EditorTheme: Generating new theme for the config '%d'.", theme->get_generated_hash()));
+
_create_shared_styles(theme, config);
- // FIXME: Make the comparison more robust and fix imprecision issues by hashing affecting values.
- // TODO: Refactor the icons check into their respective file, and add a similar check for fonts.
-
- // Register editor icons.
- // If settings are comparable to the old theme, then just copy existing icons over.
- // Otherwise, regenerate them. Also check if we need to regenerate "thumb" icons.
- bool keep_old_icons = false;
- bool regenerate_thumb_icons = true;
- if (p_old_theme != nullptr) {
- // We check editor scale, theme dark/light mode, icon saturation, and accent color.
-
- // That doesn't really work as expected, since theme constants are integers, and scales are floats.
- // So this check will never work when changing between 100-199% values.
- const float prev_scale = (float)p_old_theme->get_constant(SNAME("scale"), EditorStringName(Editor));
- const bool prev_dark_theme = (bool)p_old_theme->get_constant(SNAME("dark_theme"), EditorStringName(Editor));
- const Color prev_accent_color = p_old_theme->get_color(SNAME("accent_color"), EditorStringName(Editor));
- const float prev_icon_saturation = p_old_theme->get_color(SNAME("icon_saturation"), EditorStringName(Editor)).r;
- const float prev_gizmo_handle_scale = (float)p_old_theme->get_constant(SNAME("gizmo_handle_scale"), EditorStringName(Editor));
-
- keep_old_icons = (Math::is_equal_approx(prev_scale, EDSCALE) &&
- Math::is_equal_approx(prev_gizmo_handle_scale, config.gizmo_handle_scale) &&
- prev_dark_theme == config.dark_theme &&
- prev_accent_color == config.accent_color &&
- prev_icon_saturation == config.icon_saturation);
-
- const double prev_thumb_size = (double)p_old_theme->get_constant(SNAME("thumb_size"), EditorStringName(Editor));
-
- regenerate_thumb_icons = !Math::is_equal_approx(prev_thumb_size, config.thumb_size);
- }
+ // Register icons.
+ {
+ OS::get_singleton()->benchmark_begin_measure(get_benchmark_key(), "Register Icons");
+
+ // External functions, see editor_icons.cpp.
+ editor_configure_icons(config.dark_theme);
- // External functions, see editor_icons.cpp.
- editor_configure_icons(config.dark_theme);
- if (keep_old_icons) {
- editor_copy_icons(theme, p_old_theme);
- } else {
- editor_register_icons(theme, config.dark_theme, config.icon_saturation, config.thumb_size, false);
+ // If settings are comparable to the old theme, then just copy existing icons over.
+ // Otherwise, regenerate them.
+ bool keep_old_icons = (p_old_theme != nullptr && theme->get_generated_icons_hash() == p_old_theme->get_generated_icons_hash());
+ if (keep_old_icons) {
+ print_verbose("EditorTheme: Can keep old icons, copying.");
+ editor_copy_icons(theme, p_old_theme);
+ } else {
+ print_verbose("EditorTheme: Generating new icons.");
+ editor_register_icons(theme, config.dark_theme, config.icon_saturation, config.thumb_size, config.gizmo_handle_scale);
+ }
+
+ OS::get_singleton()->benchmark_end_measure(get_benchmark_key(), "Register Icons");
}
- if (regenerate_thumb_icons) {
- editor_register_icons(theme, config.dark_theme, config.icon_saturation, config.thumb_size, true);
+
+ // Register fonts.
+ {
+ OS::get_singleton()->benchmark_begin_measure(get_benchmark_key(), "Register Fonts");
+
+ // TODO: Check if existing font definitions from the old theme are usable and copy them.
+
+ // External function, see editor_fonts.cpp.
+ print_verbose("EditorTheme: Generating new fonts.");
+ editor_register_fonts(theme);
+
+ OS::get_singleton()->benchmark_end_measure(get_benchmark_key(), "Register Fonts");
}
- // External function, see editor_fonts.cpp.
- editor_register_fonts(theme);
+ // TODO: Check if existing style definitions from the old theme are usable and copy them.
+ print_verbose("EditorTheme: Generating new styles.");
_populate_standard_styles(theme, config);
_populate_editor_styles(theme, config);
_populate_text_editor_styles(theme, config);
- OS::get_singleton()->benchmark_end_measure("EditorTheme", "Create Base Theme");
+ OS::get_singleton()->benchmark_end_measure(get_benchmark_key(), "Create Base Theme");
return theme;
}
-EditorThemeManager::ThemeConfiguration EditorThemeManager::_create_theme_config(const Ref<Theme> &p_theme) {
+EditorThemeManager::ThemeConfiguration EditorThemeManager::_create_theme_config(const Ref<EditorTheme> &p_theme) {
ThemeConfiguration config;
// Basic properties.
config.preset = EDITOR_GET("interface/theme/preset");
config.spacing_preset = EDITOR_GET("interface/theme/spacing_preset");
- config.dark_theme = EditorSettings::get_singleton()->is_dark_theme();
config.base_color = EDITOR_GET("interface/theme/base_color");
config.accent_color = EDITOR_GET("interface/theme/accent_color");
@@ -174,6 +246,7 @@ EditorThemeManager::ThemeConfiguration EditorThemeManager::_create_theme_config(
config.increase_scrollbar_touch_area = EDITOR_GET("interface/touchscreen/increase_scrollbar_touch_area");
config.gizmo_handle_scale = EDITOR_GET("interface/touchscreen/scale_gizmo_handles");
config.color_picker_button_height = 28 * EDSCALE;
+ config.subresource_hue_tint = EDITOR_GET("docks/property_editor/subresource_hue_tint");
config.default_contrast = 0.3; // Make sure to keep this in sync with the editor settings definition.
@@ -275,6 +348,8 @@ EditorThemeManager::ThemeConfiguration EditorThemeManager::_create_theme_config(
// Generated properties.
+ config.dark_theme = is_dark_theme();
+
config.base_margin = config.base_spacing;
config.increased_margin = config.base_spacing + config.extra_spacing;
config.separation_margin = (config.base_spacing + config.extra_spacing / 2) * EDSCALE;
@@ -292,7 +367,7 @@ EditorThemeManager::ThemeConfiguration EditorThemeManager::_create_theme_config(
return config;
}
-void EditorThemeManager::_create_shared_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config) {
+void EditorThemeManager::_create_shared_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config) {
// Colors.
{
// Base colors.
@@ -556,7 +631,7 @@ void EditorThemeManager::_create_shared_styles(const Ref<Theme> &p_theme, ThemeC
}
}
-void EditorThemeManager::_populate_standard_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config) {
+void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config) {
// Panels.
{
// Panel.
@@ -1508,7 +1583,7 @@ void EditorThemeManager::_populate_standard_styles(const Ref<Theme> &p_theme, Th
}
}
-void EditorThemeManager::_populate_editor_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config) {
+void EditorThemeManager::_populate_editor_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config) {
// Project manager.
{
p_theme->set_stylebox("search_panel", "ProjectManager", p_config.tree_panel_style);
@@ -1789,7 +1864,7 @@ void EditorThemeManager::_populate_editor_styles(const Ref<Theme> &p_theme, Them
float hue_rotate = (i * 2 % 16) / 16.0;
si_base_color.set_hsv(Math::fmod(float(si_base_color.get_h() + hue_rotate), float(1.0)), si_base_color.get_s(), si_base_color.get_v());
- si_base_color = p_config.accent_color.lerp(si_base_color, float(EDITOR_GET("docks/property_editor/subresource_hue_tint")));
+ si_base_color = p_config.accent_color.lerp(si_base_color, p_config.subresource_hue_tint);
// Sub-inspector background.
Ref<StyleBoxFlat> sub_inspector_bg = p_config.base_style->duplicate();
@@ -1823,7 +1898,7 @@ void EditorThemeManager::_populate_editor_styles(const Ref<Theme> &p_theme, Them
style_property_child_bg->set_bg_color(p_config.dark_color_2);
style_property_child_bg->set_border_width_all(0);
- p_theme->set_stylebox("bg", "EditorProperty", Ref<StyleBoxEmpty>(memnew(StyleBoxEmpty)));
+ p_theme->set_stylebox("bg", "EditorProperty", memnew(StyleBoxEmpty));
p_theme->set_stylebox("bg_selected", "EditorProperty", style_property_bg);
p_theme->set_stylebox("child_bg", "EditorProperty", style_property_child_bg);
p_theme->set_constant("font_offset", "EditorProperty", 8 * EDSCALE);
@@ -2126,7 +2201,7 @@ void EditorThemeManager::_generate_text_editor_defaults(ThemeConfiguration &p_co
/* clang-format on */
}
-void EditorThemeManager::_populate_text_editor_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config) {
+void EditorThemeManager::_populate_text_editor_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config) {
String text_editor_color_theme = EditorSettings::get_singleton()->get("text_editor/theme/color_theme");
if (text_editor_color_theme == "Default") {
_generate_text_editor_defaults(p_config);
@@ -2155,7 +2230,7 @@ void EditorThemeManager::_populate_text_editor_styles(const Ref<Theme> &p_theme,
Ref<StyleBoxFlat> code_edit_stylebox = make_flat_stylebox(background_color, p_config.widget_margin.x, p_config.widget_margin.y, p_config.widget_margin.x, p_config.widget_margin.y, p_config.corner_radius);
p_theme->set_stylebox("normal", "CodeEdit", code_edit_stylebox);
p_theme->set_stylebox("read_only", "CodeEdit", code_edit_stylebox);
- p_theme->set_stylebox("focus", "CodeEdit", Ref<StyleBoxEmpty>(memnew(StyleBoxEmpty)));
+ p_theme->set_stylebox("focus", "CodeEdit", memnew(StyleBoxEmpty));
p_theme->set_color("background_color", "CodeEdit", Color(0, 0, 0, 0)); // Unset any color, we use a stylebox.
@@ -2186,10 +2261,12 @@ void EditorThemeManager::_populate_text_editor_styles(const Ref<Theme> &p_theme,
// Public interface for theme generation.
-Ref<Theme> EditorThemeManager::generate_theme(const Ref<Theme> &p_old_theme) {
- OS::get_singleton()->benchmark_begin_measure("EditorTheme", "Generate Theme");
+Ref<EditorTheme> EditorThemeManager::generate_theme(const Ref<EditorTheme> &p_old_theme) {
+ OS::get_singleton()->benchmark_begin_measure(get_benchmark_key(), "Generate Theme");
- Ref<Theme> theme = _create_base_theme(p_old_theme);
+ Ref<EditorTheme> theme = _create_base_theme(p_old_theme);
+
+ OS::get_singleton()->benchmark_begin_measure(get_benchmark_key(), "Merge Custom Theme");
const String custom_theme_path = EDITOR_GET("interface/theme/custom_theme");
if (!custom_theme_path.is_empty()) {
@@ -2199,7 +2276,11 @@ Ref<Theme> EditorThemeManager::generate_theme(const Ref<Theme> &p_old_theme) {
}
}
- OS::get_singleton()->benchmark_end_measure("EditorTheme", "Generate Theme");
+ OS::get_singleton()->benchmark_end_measure(get_benchmark_key(), "Merge Custom Theme");
+
+ OS::get_singleton()->benchmark_end_measure(get_benchmark_key(), "Generate Theme");
+ benchmark_run++;
+
return theme;
}
@@ -2213,12 +2294,25 @@ bool EditorThemeManager::is_generated_theme_outdated() {
EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor/font") ||
EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor/main_font") ||
EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor/code_font") ||
+ EditorSettings::get_singleton()->check_changed_settings_in_group("interface/touchscreen/increase_scrollbar_touch_area") ||
+ EditorSettings::get_singleton()->check_changed_settings_in_group("interface/touchscreen/scale_gizmo_handles") ||
EditorSettings::get_singleton()->check_changed_settings_in_group("text_editor/theme") ||
EditorSettings::get_singleton()->check_changed_settings_in_group("text_editor/help/help") ||
+ EditorSettings::get_singleton()->check_changed_settings_in_group("docks/property_editor/subresource_hue_tint") ||
EditorSettings::get_singleton()->check_changed_settings_in_group("filesystem/file_dialog/thumbnail_size") ||
- EditorSettings::get_singleton()->check_changed_settings_in_group("run/output/font_size") ||
- EditorSettings::get_singleton()->check_changed_settings_in_group("interface/touchscreen/increase_scrollbar_touch_area") ||
- EditorSettings::get_singleton()->check_changed_settings_in_group("interface/touchscreen/scale_gizmo_handles");
+ EditorSettings::get_singleton()->check_changed_settings_in_group("run/output/font_size");
+}
+
+bool EditorThemeManager::is_dark_theme() {
+ // Light color mode for icons and fonts means it's a dark theme, and vice versa.
+ int icon_font_color_setting = EDITOR_GET("interface/theme/icon_and_font_color");
+
+ if (icon_font_color_setting == ColorMode::AUTO_COLOR) {
+ Color base_color = EDITOR_GET("interface/theme/base_color");
+ return base_color.get_luminance() < 0.5;
+ }
+
+ return icon_font_color_setting == ColorMode::LIGHT_COLOR;
}
void EditorThemeManager::initialize() {
diff --git a/editor/themes/editor_theme_manager.h b/editor/themes/editor_theme_manager.h
index 86188ec244..0b30a9c853 100644
--- a/editor/themes/editor_theme_manager.h
+++ b/editor/themes/editor_theme_manager.h
@@ -31,16 +31,25 @@
#ifndef EDITOR_THEME_MANAGER_H
#define EDITOR_THEME_MANAGER_H
+#include "editor/themes/editor_theme.h"
#include "scene/resources/style_box_flat.h"
-#include "scene/resources/theme.h"
class EditorThemeManager {
+ static int benchmark_run;
+
+ static String get_benchmark_key();
+
+ enum ColorMode {
+ AUTO_COLOR,
+ DARK_COLOR,
+ LIGHT_COLOR,
+ };
+
struct ThemeConfiguration {
// Basic properties.
String preset;
String spacing_preset;
- bool dark_theme = false;
Color base_color;
Color accent_color;
@@ -61,11 +70,14 @@ class EditorThemeManager {
bool increase_scrollbar_touch_area = false;
float gizmo_handle_scale = 1.0;
int color_picker_button_height = 28;
+ float subresource_hue_tint = 0.0;
float default_contrast = 1.0;
// Generated properties.
+ bool dark_theme = false;
+
int base_margin = 4;
int increased_margin = 4;
int separation_margin = 4;
@@ -127,22 +139,28 @@ class EditorThemeManager {
Ref<StyleBoxFlat> tree_panel_style;
Vector2 widget_margin;
+
+ uint32_t hash();
+ uint32_t hash_fonts();
+ uint32_t hash_icons();
};
- static Ref<Theme> _create_base_theme(const Ref<Theme> &p_old_theme = nullptr);
- static ThemeConfiguration _create_theme_config(const Ref<Theme> &p_theme);
+ static Ref<EditorTheme> _create_base_theme(const Ref<EditorTheme> &p_old_theme = nullptr);
+ static ThemeConfiguration _create_theme_config(const Ref<EditorTheme> &p_theme);
- static void _create_shared_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config);
- static void _populate_standard_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config);
- static void _populate_editor_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config);
+ static void _create_shared_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config);
+ static void _populate_standard_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config);
+ static void _populate_editor_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config);
static void _generate_text_editor_defaults(ThemeConfiguration &p_config);
- static void _populate_text_editor_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config);
+ static void _populate_text_editor_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config);
public:
- static Ref<Theme> generate_theme(const Ref<Theme> &p_old_theme = nullptr);
+ static Ref<EditorTheme> generate_theme(const Ref<EditorTheme> &p_old_theme = nullptr);
static bool is_generated_theme_outdated();
+ static bool is_dark_theme();
+
static void initialize();
static void finalize();
};
diff --git a/methods.py b/methods.py
index 7c511af930..a55c622ed0 100644
--- a/methods.py
+++ b/methods.py
@@ -1022,6 +1022,11 @@ def get_compiler_version(env):
"metadata1": None,
"metadata2": None,
"date": None,
+ "apple_major": -1,
+ "apple_minor": -1,
+ "apple_patch1": -1,
+ "apple_patch2": -1,
+ "apple_patch3": -1,
}
if not env.msvc:
@@ -1049,8 +1054,32 @@ def get_compiler_version(env):
for key, value in match.groupdict().items():
if value is not None:
ret[key] = value
+
+ match_apple = re.search(
+ r"(?:(?<=clang-)|(?<=\) )|(?<=^))"
+ r"(?P<apple_major>\d+)"
+ r"(?:\.(?P<apple_minor>\d*))?"
+ r"(?:\.(?P<apple_patch1>\d*))?"
+ r"(?:\.(?P<apple_patch2>\d*))?"
+ r"(?:\.(?P<apple_patch3>\d*))?",
+ version,
+ )
+ if match_apple is not None:
+ for key, value in match_apple.groupdict().items():
+ if value is not None:
+ ret[key] = value
+
# Transform semantic versioning to integers
- for key in ["major", "minor", "patch"]:
+ for key in [
+ "major",
+ "minor",
+ "patch",
+ "apple_major",
+ "apple_minor",
+ "apple_patch1",
+ "apple_patch2",
+ "apple_patch3",
+ ]:
ret[key] = int(ret[key] or -1)
return ret
diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml
index 933bfba5ba..b335bf8fae 100644
--- a/modules/gdscript/doc_classes/@GDScript.xml
+++ b/modules/gdscript/doc_classes/@GDScript.xml
@@ -627,7 +627,7 @@
[/codeblock]
[b]Note:[/b] Only the script can have a custom icon. Inner classes are not supported.
[b]Note:[/b] As annotations describe their subject, the [annotation @icon] annotation must be placed before the class definition and inheritance.
- [b]Note:[/b] Unlike other annotations, the argument of the [annotation @icon] annotation must be a string literal (constant expressions are not supported).
+ [b]Note:[/b] Unlike most other annotations, the argument of the [annotation @icon] annotation must be a string literal (constant expressions are not supported).
</description>
</annotation>
<annotation name="@onready">
@@ -681,6 +681,14 @@
[b]Note:[/b] As annotations describe their subject, the [annotation @tool] annotation must be placed before the class definition and inheritance.
</description>
</annotation>
+ <annotation name="@uid">
+ <return type="void" />
+ <param index="0" name="uid" type="String" />
+ <description>
+ Stores information about UID of this script. This annotation is auto-generated when saving the script and must not be modified manually. Only applies to scripts saved as separate files (i.e. not built-in).
+ [b]Note:[/b] Unlike most other annotations, the argument of the [annotation @uid] annotation must be a string literal (constant expressions are not supported).
+ </description>
+ </annotation>
<annotation name="@warning_ignore" qualifiers="vararg">
<return type="void" />
<param index="0" name="warning" type="String" />
diff --git a/modules/gdscript/editor/gdscript_highlighter.cpp b/modules/gdscript/editor/gdscript_highlighter.cpp
index 3df07f9794..1f07def21c 100644
--- a/modules/gdscript/editor/gdscript_highlighter.cpp
+++ b/modules/gdscript/editor/gdscript_highlighter.cpp
@@ -35,6 +35,7 @@
#include "core/config/project_settings.h"
#include "editor/editor_settings.h"
+#include "editor/themes/editor_theme_manager.h"
Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_line) {
Dictionary color_map;
@@ -790,7 +791,7 @@ void GDScriptSyntaxHighlighter::_update_cache() {
const String text_edit_color_theme = EDITOR_GET("text_editor/theme/color_theme");
const bool godot_2_theme = text_edit_color_theme == "Godot 2";
- if (godot_2_theme || EditorSettings::get_singleton()->is_dark_theme()) {
+ if (godot_2_theme || EditorThemeManager::is_dark_theme()) {
function_definition_color = Color(0.4, 0.9, 1.0);
global_function_color = Color(0.64, 0.64, 0.96);
node_path_color = Color(0.72, 0.77, 0.49);
diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp
index 920aa63fbe..7b486f2a35 100644
--- a/modules/gdscript/gdscript.cpp
+++ b/modules/gdscript/gdscript.cpp
@@ -55,6 +55,7 @@
#ifdef TOOLS_ENABLED
#include "editor/editor_paths.h"
+#include "editor/editor_settings.h"
#endif
#include <stdint.h>
@@ -1076,6 +1077,36 @@ Ref<GDScript> GDScript::get_base() const {
return base;
}
+String GDScript::get_raw_source_code(const String &p_path, bool *r_error) {
+ Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);
+ if (f.is_null()) {
+ if (r_error) {
+ *r_error = true;
+ }
+ return String();
+ }
+ return f->get_as_utf8_string();
+}
+
+Vector2i GDScript::get_uid_lines(const String &p_source) {
+ GDScriptParser parser;
+ parser.parse(p_source, "", false);
+ const GDScriptParser::ClassNode *c = parser.get_tree();
+ if (!c) {
+ return Vector2i(-1, -1);
+ }
+ return c->uid_lines;
+}
+
+String GDScript::create_uid_line(const String &p_uid_str) {
+#ifdef TOOLS_ENABLED
+ if (EDITOR_GET("text_editor/completion/use_single_quotes")) {
+ return vformat(R"(@uid('%s') # %s)", p_uid_str, RTR("Generated automatically, do not modify."));
+ }
+#endif
+ return vformat(R"(@uid("%s") # %s)", p_uid_str, RTR("Generated automatically, do not modify."));
+}
+
bool GDScript::inherits_script(const Ref<Script> &p_script) const {
Ref<GDScript> gd = p_script;
if (gd.is_null()) {
@@ -2593,17 +2624,8 @@ bool GDScriptLanguage::handles_global_class_type(const String &p_type) const {
}
String GDScriptLanguage::get_global_class_name(const String &p_path, String *r_base_type, String *r_icon_path) const {
- Error err;
- Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ, &err);
- if (err) {
- return String();
- }
-
- String source = f->get_as_utf8_string();
-
GDScriptParser parser;
- err = parser.parse(source, p_path, false);
-
+ parser.parse(GDScript::get_raw_source_code(p_path), p_path, false);
const GDScriptParser::ClassNode *c = parser.get_tree();
if (!c) {
return String(); // No class parsed.
@@ -2817,6 +2839,22 @@ String ResourceFormatLoaderGDScript::get_resource_type(const String &p_path) con
return "";
}
+ResourceUID::ID ResourceFormatLoaderGDScript::get_resource_uid(const String &p_path) const {
+ String ext = p_path.get_extension().to_lower();
+
+ if (ext != "gd") {
+ return ResourceUID::INVALID_ID;
+ }
+
+ GDScriptParser parser;
+ parser.parse(GDScript::get_raw_source_code(p_path), p_path, false);
+ const GDScriptParser::ClassNode *c = parser.get_tree();
+ if (!c) {
+ return ResourceUID::INVALID_ID;
+ }
+ return ResourceUID::get_singleton()->text_to_id(c->uid_string);
+}
+
void ResourceFormatLoaderGDScript::get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types) {
Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::READ);
ERR_FAIL_COND_MSG(file.is_null(), "Cannot open file '" + p_path + "'.");
@@ -2841,17 +2879,49 @@ Error ResourceFormatSaverGDScript::save(const Ref<Resource> &p_resource, const S
ERR_FAIL_COND_V(sqscr.is_null(), ERR_INVALID_PARAMETER);
String source = sqscr->get_source_code();
+ ResourceUID::ID uid = ResourceSaver::get_resource_id_for_path(p_path, !p_resource->is_built_in());
{
+ bool source_changed = false;
Error err;
Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::WRITE, &err);
ERR_FAIL_COND_V_MSG(err, err, "Cannot save GDScript file '" + p_path + "'.");
- file->store_string(source);
+ if (uid != ResourceUID::INVALID_ID) {
+ GDScriptParser parser;
+ parser.parse(source, "", false);
+ const GDScriptParser::ClassNode *c = parser.get_tree();
+ if (c && ResourceUID::get_singleton()->text_to_id(c->uid_string) != uid) {
+ const Vector2i &uid_idx = c->uid_lines;
+ PackedStringArray lines = source.split("\n");
+
+ if (uid_idx.x > -1) {
+ for (int i = uid_idx.x + 1; i <= uid_idx.y; i++) {
+ // If UID is written across multiple lines, erase extra lines.
+ lines.remove_at(uid_idx.x + 1);
+ }
+ lines.write[uid_idx.x] = GDScript::create_uid_line(ResourceUID::get_singleton()->id_to_text(uid));
+ } else {
+ lines.insert(0, GDScript::create_uid_line(ResourceUID::get_singleton()->id_to_text(uid)));
+ }
+ source = String("\n").join(lines);
+ source_changed = true;
+ file->store_string(String("\n").join(lines));
+ } else {
+ file->store_string(source);
+ }
+ }
+
if (file->get_error() != OK && file->get_error() != ERR_FILE_EOF) {
return ERR_CANT_CREATE;
}
+
+ if (source_changed) {
+ sqscr->set_source_code(source);
+ sqscr->reload();
+ sqscr->emit_changed();
+ }
}
if (ScriptServer::is_reload_scripts_on_save_enabled()) {
@@ -2870,3 +2940,33 @@ void ResourceFormatSaverGDScript::get_recognized_extensions(const Ref<Resource>
bool ResourceFormatSaverGDScript::recognize(const Ref<Resource> &p_resource) const {
return Object::cast_to<GDScript>(*p_resource) != nullptr;
}
+
+Error ResourceFormatSaverGDScript::set_uid(const String &p_path, ResourceUID::ID p_uid) {
+ ERR_FAIL_COND_V(p_path.get_extension() != "gd", ERR_INVALID_PARAMETER);
+ ERR_FAIL_COND_V(p_uid == ResourceUID::INVALID_ID, ERR_INVALID_PARAMETER);
+
+ bool error = false;
+ const String &source_code = GDScript::get_raw_source_code(p_path, &error);
+ if (error) {
+ return ERR_CANT_OPEN;
+ }
+
+ Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);
+ ERR_FAIL_COND_V(f.is_null(), ERR_CANT_OPEN);
+
+ const Vector2i &uid_idx = GDScript::get_uid_lines(source_code);
+ PackedStringArray lines = source_code.split("\n");
+
+ if (uid_idx.x > -1) {
+ for (int i = uid_idx.x + 1; i <= uid_idx.y; i++) {
+ // If UID is written across multiple lines, erase extra lines.
+ lines.remove_at(uid_idx.x + 1);
+ }
+ lines.write[uid_idx.x] = GDScript::create_uid_line(ResourceUID::get_singleton()->id_to_text(p_uid));
+ } else {
+ f->store_line(GDScript::create_uid_line(ResourceUID::get_singleton()->id_to_text(p_uid)));
+ }
+ f->store_string(String("\n").join(lines));
+
+ return OK;
+}
diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h
index 2da9b89eb9..fdfd79f0fc 100644
--- a/modules/gdscript/gdscript.h
+++ b/modules/gdscript/gdscript.h
@@ -262,6 +262,10 @@ public:
bool is_tool() const override { return tool; }
Ref<GDScript> get_base() const;
+ static String get_raw_source_code(const String &p_path, bool *r_error = nullptr);
+ static Vector2i get_uid_lines(const String &p_source);
+ static String create_uid_line(const String &p_uid_str);
+
const HashMap<StringName, MemberInfo> &debug_get_member_indices() const { return member_indices; }
const HashMap<StringName, GDScriptFunction *> &debug_get_member_functions() const; //this is debug only
StringName debug_get_member_by_index(int p_idx) const;
@@ -616,6 +620,7 @@ public:
virtual void get_recognized_extensions(List<String> *p_extensions) const;
virtual bool handles_type(const String &p_type) const;
virtual String get_resource_type(const String &p_path) const;
+ virtual ResourceUID::ID get_resource_uid(const String &p_path) const;
virtual void get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types = false);
};
@@ -624,6 +629,7 @@ public:
virtual Error save(const Ref<Resource> &p_resource, const String &p_path, uint32_t p_flags = 0);
virtual void get_recognized_extensions(const Ref<Resource> &p_resource, List<String> *p_extensions) const;
virtual bool recognize(const Ref<Resource> &p_resource) const;
+ virtual Error set_uid(const String &p_path, ResourceUID::ID p_uid);
};
#endif // GDSCRIPT_H
diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp
index f6633f8bf6..edba6340b9 100644
--- a/modules/gdscript/gdscript_compiler.cpp
+++ b/modules/gdscript/gdscript_compiler.cpp
@@ -37,6 +37,7 @@
#include "core/config/engine.h"
#include "core/config/project_settings.h"
+#include "core/core_string_names.h"
bool GDScriptCompiler::_is_class_member_property(CodeGen &codegen, const StringName &p_name) {
if (codegen.function_node && codegen.function_node->is_static) {
@@ -345,7 +346,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
scr = scr->_base;
}
- if (nc && (ClassDB::has_signal(nc->get_name(), identifier) || ClassDB::has_method(nc->get_name(), identifier))) {
+ if (nc && (identifier == CoreStringNames::get_singleton()->_free || ClassDB::has_signal(nc->get_name(), identifier) || ClassDB::has_method(nc->get_name(), identifier))) {
// Get like it was a property.
GDScriptCodeGenerator::Address temp = codegen.add_temporary(); // TODO: Get type here.
GDScriptCodeGenerator::Address self(GDScriptCodeGenerator::Address::SELF);
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp
index a4a12f8bc4..03cf334bed 100644
--- a/modules/gdscript/gdscript_parser.cpp
+++ b/modules/gdscript/gdscript_parser.cpp
@@ -93,6 +93,7 @@ bool GDScriptParser::annotation_exists(const String &p_annotation_name) const {
GDScriptParser::GDScriptParser() {
// Register valid annotations.
if (unlikely(valid_annotations.is_empty())) {
+ register_annotation(MethodInfo("@uid", PropertyInfo(Variant::STRING, "uid")), AnnotationInfo::SCRIPT, &GDScriptParser::uid_annotation);
register_annotation(MethodInfo("@tool"), AnnotationInfo::SCRIPT, &GDScriptParser::tool_annotation);
register_annotation(MethodInfo("@icon", PropertyInfo(Variant::STRING, "icon_path")), AnnotationInfo::SCRIPT, &GDScriptParser::icon_annotation);
register_annotation(MethodInfo("@static_unload"), AnnotationInfo::SCRIPT, &GDScriptParser::static_unload_annotation);
@@ -520,6 +521,8 @@ void GDScriptParser::parse_program() {
// `@icon` needs to be applied in the parser. See GH-72444.
if (annotation->name == SNAME("@icon")) {
annotation->apply(this, head, nullptr);
+ } else if (annotation->name == SNAME("@uid")) {
+ annotation->apply(this, head, nullptr);
} else {
head->annotations.push_back(annotation);
}
@@ -3834,18 +3837,18 @@ bool GDScriptParser::validate_annotation_arguments(AnnotationNode *p_annotation)
}
// `@icon`'s argument needs to be resolved in the parser. See GH-72444.
- if (p_annotation->name == SNAME("@icon")) {
+ if (p_annotation->name == SNAME("@icon") || p_annotation->name == SNAME("@uid")) {
ExpressionNode *argument = p_annotation->arguments[0];
if (argument->type != Node::LITERAL) {
- push_error(R"(Argument 1 of annotation "@icon" must be a string literal.)", argument);
+ push_error(vformat(R"(Argument 1 of annotation "%s" must be a string literal.)", p_annotation->name), argument);
return false;
}
Variant value = static_cast<LiteralNode *>(argument)->value;
if (value.get_type() != Variant::STRING) {
- push_error(R"(Argument 1 of annotation "@icon" must be a string literal.)", argument);
+ push_error(vformat(R"(Argument 1 of annotation "%s" must be a string literal.)", p_annotation->name), argument);
return false;
}
@@ -3857,6 +3860,35 @@ bool GDScriptParser::validate_annotation_arguments(AnnotationNode *p_annotation)
return true;
}
+bool GDScriptParser::uid_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
+ ERR_FAIL_COND_V_MSG(p_target->type != Node::CLASS, false, R"("@uid" annotation can only be applied to classes.)");
+ ERR_FAIL_COND_V(p_annotation->resolved_arguments.is_empty(), false);
+
+#ifdef DEBUG_ENABLED
+ if (this->_has_uid) {
+ push_error(R"("@uid" annotation can only be used once.)", p_annotation);
+ return false;
+ }
+#endif // DEBUG_ENABLED
+
+ // Assign line range early to allow replacing invalid UIDs.
+ ClassNode *class_node = static_cast<ClassNode *>(p_target);
+ class_node->uid_lines = Vector2i(p_annotation->start_line - 1, p_annotation->end_line - 1); // Lines start from 1, so need to subtract.
+
+ const String &uid_string = p_annotation->resolved_arguments[0];
+#ifdef DEBUG_ENABLED
+ if (ResourceUID::get_singleton()->text_to_id(uid_string) == ResourceUID::INVALID_ID) {
+ push_error(R"(The annotated UID is invalid.)", p_annotation);
+ return false;
+ }
+#endif // DEBUG_ENABLED
+
+ class_node->uid_string = uid_string;
+
+ this->_has_uid = true;
+ return true;
+}
+
bool GDScriptParser::tool_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
#ifdef DEBUG_ENABLED
if (this->_is_tool) {
diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h
index 88b5bdc43f..e058737306 100644
--- a/modules/gdscript/gdscript_parser.h
+++ b/modules/gdscript/gdscript_parser.h
@@ -736,6 +736,8 @@ public:
IdentifierNode *identifier = nullptr;
String icon_path;
String simplified_icon_path;
+ String uid_string;
+ Vector2i uid_lines = Vector2i(-1, -1);
Vector<Member> members;
HashMap<StringName, int> members_indices;
ClassNode *outer = nullptr;
@@ -1318,6 +1320,7 @@ private:
friend class GDScriptAnalyzer;
bool _is_tool = false;
+ bool _has_uid = false;
String script_path;
bool for_completion = false;
bool panic_mode = false;
@@ -1473,6 +1476,7 @@ private:
static bool register_annotation(const MethodInfo &p_info, uint32_t p_target_kinds, AnnotationAction p_apply, const Vector<Variant> &p_default_arguments = Vector<Variant>(), bool p_is_vararg = false);
bool validate_annotation_arguments(AnnotationNode *p_annotation);
void clear_unused_annotations();
+ bool uid_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
bool tool_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
bool icon_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
bool onready_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.gd b/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.gd
new file mode 100644
index 0000000000..4ded8e65db
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.gd
@@ -0,0 +1,5 @@
+@uid("uid://c4ckv3ryprcn4")
+@uid("uid://c4ckv3ryprcn4")
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.out b/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.out
new file mode 100644
index 0000000000..be1061401a
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+"@uid" annotation can only be used once.
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_invalid.gd b/modules/gdscript/tests/scripts/parser/errors/uid_invalid.gd
new file mode 100644
index 0000000000..114d5b7e98
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_invalid.gd
@@ -0,0 +1,4 @@
+@uid("not a valid uid")
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_invalid.out b/modules/gdscript/tests/scripts/parser/errors/uid_invalid.out
new file mode 100644
index 0000000000..83f9f63cbf
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_invalid.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+The annotated UID is invalid.
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_too_late.gd b/modules/gdscript/tests/scripts/parser/errors/uid_too_late.gd
new file mode 100644
index 0000000000..2b332447b7
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_too_late.gd
@@ -0,0 +1,5 @@
+extends Object
+@uid("uid://c4ckv3ryprcn4")
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_too_late.out b/modules/gdscript/tests/scripts/parser/errors/uid_too_late.out
new file mode 100644
index 0000000000..328459923f
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_too_late.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Annotation "@uid" must be at the top of the script, before "extends" and "class_name".
diff --git a/modules/gdscript/tests/scripts/parser/features/uid.gd b/modules/gdscript/tests/scripts/parser/features/uid.gd
new file mode 100644
index 0000000000..4070500608
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/uid.gd
@@ -0,0 +1,5 @@
+@uid("uid://c4ckv3ryprcn4")
+extends Object
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/features/uid.out b/modules/gdscript/tests/scripts/parser/features/uid.out
new file mode 100644
index 0000000000..d73c5eb7cd
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/uid.out
@@ -0,0 +1 @@
+GDTEST_OK
diff --git a/modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd
new file mode 100644
index 0000000000..b9746a8207
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd
@@ -0,0 +1,10 @@
+func test():
+ var node := Node.new()
+ var callable: Callable = node.free
+ callable.call()
+ print(node)
+
+ node = Node.new()
+ callable = node["free"]
+ callable.call()
+ print(node)
diff --git a/modules/gdscript/tests/scripts/runtime/features/free_is_callable.out b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.out
new file mode 100644
index 0000000000..97bfc46d96
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.out
@@ -0,0 +1,3 @@
+GDTEST_OK
+<Freed Object>
+<Freed Object>
diff --git a/modules/gdscript/tests/test_gdscript_uid.h b/modules/gdscript/tests/test_gdscript_uid.h
new file mode 100644
index 0000000000..918fe65890
--- /dev/null
+++ b/modules/gdscript/tests/test_gdscript_uid.h
@@ -0,0 +1,115 @@
+/**************************************************************************/
+/* test_gdscript_uid.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef TEST_GDSCRIPT_UID_H
+#define TEST_GDSCRIPT_UID_H
+
+#ifdef TOOLS_ENABLED
+
+#include "core/io/resource_saver.h"
+#include "core/os/os.h"
+#include "gdscript_test_runner.h"
+
+#include "../gdscript.h"
+#include "tests/test_macros.h"
+
+namespace GDScriptTests {
+
+static HashMap<String, ResourceUID::ID> id_cache;
+
+ResourceUID::ID _resource_saver_get_resource_id_for_path(const String &p_path, bool p_generate) {
+ return ResourceUID::get_singleton()->text_to_id("uid://baba");
+}
+
+static void test_script(const String &p_source, const String &p_target_source) {
+ const String script_path = OS::get_singleton()->get_cache_path().path_join("script.gd");
+
+ Ref<GDScript> script;
+ script.instantiate();
+ script->set_source_code(p_source);
+ ResourceSaver::save(script, script_path);
+
+ Ref<FileAccess> fa = FileAccess::open(script_path, FileAccess::READ);
+ CHECK_EQ(fa->get_as_text(), p_target_source);
+}
+
+TEST_SUITE("[Modules][GDScript][UID]") {
+ TEST_CASE("[ResourceSaver] Adding UID line to script") {
+ init_language("modules/gdscript/tests/scripts");
+ ResourceSaver::set_get_resource_id_for_path(_resource_saver_get_resource_id_for_path);
+
+ const String source = R"(extends Node
+class_name TestClass
+)";
+ const String final_source = R"(@uid("uid://baba") # Generated automatically, do not modify.
+extends Node
+class_name TestClass
+)";
+
+ // Script has no UID, add it.
+ test_script(source, final_source);
+ }
+
+ TEST_CASE("[ResourceSaver] Updating UID line in script") {
+ init_language("modules/gdscript/tests/scripts");
+ ResourceSaver::set_get_resource_id_for_path(_resource_saver_get_resource_id_for_path);
+
+ const String wrong_id_source = R"(
+
+@uid(
+ "uid://dead"
+ ) # G
+extends Node
+class_name TestClass
+)";
+ const String corrected_id_source = R"(
+
+@uid("uid://baba") # Generated automatically, do not modify.
+extends Node
+class_name TestClass
+)";
+ const String correct_id_source = R"(@uid("uid://baba") # G
+extends Node
+class_name TestClass
+)";
+
+ // Script has wrong UID saved. Remove it and add a correct one.
+ // Inserts in the same line, but multiline annotations are flattened.
+ test_script(wrong_id_source, corrected_id_source);
+ // The stored UID is correct, so do not modify it.
+ test_script(correct_id_source, correct_id_source);
+ }
+}
+
+} // namespace GDScriptTests
+
+#endif
+
+#endif // TEST_GDSCRIPT_UID_H
diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp
index 2fff4ce32c..13a0a9f877 100644
--- a/platform/linuxbsd/x11/display_server_x11.cpp
+++ b/platform/linuxbsd/x11/display_server_x11.cpp
@@ -2011,7 +2011,7 @@ void DisplayServerX11::window_set_transient(WindowID p_window, WindowID p_parent
// a subwindow and its parent are both destroyed.
if (!wd_window.no_focus && !wd_window.is_popup && wd_window.focused) {
if ((xwa.map_state == IsViewable) && !wd_parent.no_focus && !wd_window.is_popup && _window_focus_check()) {
- XSetInputFocus(x11_display, wd_parent.x11_window, RevertToPointerRoot, CurrentTime);
+ _set_input_focus(wd_parent.x11_window, RevertToPointerRoot);
}
}
} else {
@@ -2951,7 +2951,7 @@ void DisplayServerX11::window_set_ime_active(const bool p_active, WindowID p_win
XSync(x11_display, False);
XGetWindowAttributes(x11_display, wd.x11_xim_window, &xwa);
if (xwa.map_state == IsViewable && _window_focus_check()) {
- XSetInputFocus(x11_display, wd.x11_xim_window, RevertToParent, CurrentTime);
+ _set_input_focus(wd.x11_xim_window, RevertToParent);
}
XSetICFocus(wd.xic);
} else {
@@ -4024,6 +4024,18 @@ void DisplayServerX11::_send_window_event(const WindowData &wd, WindowEvent p_ev
}
}
+void DisplayServerX11::_set_input_focus(Window p_window, int p_revert_to) {
+ Window focused_window;
+ int focus_ret_state;
+ XGetInputFocus(x11_display, &focused_window, &focus_ret_state);
+
+ // Only attempt to change focus if the window isn't already focused, in order to
+ // prevent issues with Godot stealing input focus with alternative window managers.
+ if (p_window != focused_window) {
+ XSetInputFocus(x11_display, p_window, p_revert_to, CurrentTime);
+ }
+}
+
void DisplayServerX11::_poll_events_thread(void *ud) {
DisplayServerX11 *display_server = static_cast<DisplayServerX11 *>(ud);
display_server->_poll_events();
@@ -4521,7 +4533,7 @@ void DisplayServerX11::process_events() {
// RevertToPointerRoot is used to make sure we don't lose all focus in case
// a subwindow and its parent are both destroyed.
if ((xwa.map_state == IsViewable) && !wd.no_focus && !wd.is_popup && _window_focus_check()) {
- XSetInputFocus(x11_display, wd.x11_window, RevertToPointerRoot, CurrentTime);
+ _set_input_focus(wd.x11_window, RevertToPointerRoot);
}
// Have we failed to set fullscreen while the window was unmapped?
@@ -4697,7 +4709,7 @@ void DisplayServerX11::process_events() {
// RevertToPointerRoot is used to make sure we don't lose all focus in case
// a subwindow and its parent are both destroyed.
if ((xwa.map_state == IsViewable) && !wd.no_focus && !wd.is_popup && _window_focus_check()) {
- XSetInputFocus(x11_display, wd.x11_window, RevertToPointerRoot, CurrentTime);
+ _set_input_focus(wd.x11_window, RevertToPointerRoot);
}
_window_changed(&event);
@@ -4741,7 +4753,7 @@ void DisplayServerX11::process_events() {
// RevertToPointerRoot is used to make sure we don't lose all focus in case
// a subwindow and its parent are both destroyed.
if (!wd.no_focus && !wd.is_popup) {
- XSetInputFocus(x11_display, wd.x11_window, RevertToPointerRoot, CurrentTime);
+ _set_input_focus(wd.x11_window, RevertToPointerRoot);
}
uint64_t diff = OS::get_singleton()->get_ticks_usec() / 1000 - last_click_ms;
diff --git a/platform/linuxbsd/x11/display_server_x11.h b/platform/linuxbsd/x11/display_server_x11.h
index 3b362e5c22..27bf7951ff 100644
--- a/platform/linuxbsd/x11/display_server_x11.h
+++ b/platform/linuxbsd/x11/display_server_x11.h
@@ -359,6 +359,7 @@ class DisplayServerX11 : public DisplayServer {
void _send_window_event(const WindowData &wd, WindowEvent p_event);
static void _dispatch_input_events(const Ref<InputEvent> &p_event);
void _dispatch_input_event(const Ref<InputEvent> &p_event);
+ void _set_input_focus(Window p_window, int p_revert_to);
mutable Mutex events_mutex;
Thread events_thread;
diff --git a/platform/macos/detect.py b/platform/macos/detect.py
index bc14d233bb..4a8e9cd956 100644
--- a/platform/macos/detect.py
+++ b/platform/macos/detect.py
@@ -130,12 +130,12 @@ def configure(env: "Environment"):
env.Append(LINKFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.13"])
cc_version = get_compiler_version(env)
- cc_version_major = cc_version["major"]
- cc_version_minor = cc_version["minor"]
+ cc_version_major = cc_version["apple_major"]
+ cc_version_minor = cc_version["apple_minor"]
vanilla = is_vanilla_clang(env)
# Workaround for Xcode 15 linker bug.
- if not vanilla and cc_version_major == 15 and cc_version_minor == 0:
+ if not vanilla and cc_version_major == 1500 and cc_version_minor == 0:
env.Prepend(LINKFLAGS=["-ld_classic"])
env.Append(CCFLAGS=["-fobjc-arc"])