From 3d4b33df3307ff72db9700deb6695569a5d71ea9 Mon Sep 17 00:00:00 2001 From: Yuri Sizov Date: Wed, 20 Dec 2023 13:09:36 +0100 Subject: [PATCH] Optimize scanning routines in the project manager --- editor/project_manager.cpp | 193 +++++++++++++++++-------------------- editor/project_manager.h | 15 ++- 2 files changed, 96 insertions(+), 112 deletions(-) diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 2073b1f374b3..d5fbc095c6ad 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -1249,6 +1249,7 @@ struct ProjectListComparator { } }; +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"; @@ -1356,7 +1357,7 @@ ProjectList::Item ProjectList::load_project_data(const String &p_path, bool p_fa 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() { +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)) { @@ -1388,9 +1389,10 @@ void ProjectList::migrate_config() { save_config(); } -void ProjectList::load_projects() { +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) { @@ -1421,6 +1423,7 @@ void ProjectList::load_projects() { update_dock_menu(); set_v_scroll(0); + emit_signal(SNAME(SIGNAL_LIST_CHANGED)); } void ProjectList::update_dock_menu() { @@ -1678,6 +1681,48 @@ void ProjectList::erase_missing_projects() { save_config(); } +void ProjectList::_scan_folder_recursive(const String &p_path, List *r_projects) { + Ref 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 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). @@ -1916,6 +1961,7 @@ void ProjectList::_show_project(const String &p_path) { } void ProjectList::_bind_methods() { + ADD_SIGNAL(MethodInfo(SIGNAL_LIST_CHANGED)); ADD_SIGNAL(MethodInfo(SIGNAL_SELECTION_CHANGED)); ADD_SIGNAL(MethodInfo(SIGNAL_PROJECT_ASK_OPEN)); } @@ -1926,6 +1972,7 @@ ProjectList::ProjectList() { add_child(_scroll_children); _config_path = EditorPaths::get_singleton()->get_data_dir().path_join("projects.cfg"); + _migrate_config(); } /// Project Manager. @@ -2183,15 +2230,6 @@ void ProjectManager::shortcut_input(const Ref &p_ev) { } } -void ProjectManager::_load_recent_projects() { - _project_list->set_search_term(search_box->get_text().strip_edges()); - _project_list->load_projects(); - - _update_project_buttons(); - - tabs->set_current_tab(0); -} - void ProjectManager::_on_projects_updated() { Vector selected_projects = _project_list->get_selected_projects(); int index = 0; @@ -2429,30 +2467,6 @@ void ProjectManager::_run_project() { } } -void ProjectManager::_scan_dir(const String &path) { - Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - Error error = da->change_dir(path); - ERR_FAIL_COND_MSG(error != OK, "Could not scan directory at: " + path); - da->list_dir_begin(); - String n = da->get_next(); - while (!n.is_empty()) { - if (da->current_is_dir() && !n.begins_with(".")) { - _scan_dir(da->get_current_dir().path_join(n)); - } else if (n == "project.godot") { - _project_list->add_project(da->get_current_dir(), false); - } - n = da->get_next(); - } - da->list_dir_end(); -} - -void ProjectManager::_scan_begin(const String &p_base) { - print_line("Scanning projects at: " + p_base); - _scan_dir(p_base); - _project_list->save_config(); - _load_recent_projects(); -} - void ProjectManager::_scan_projects() { scan_dir->popup_file_dialog(); } @@ -2652,54 +2666,26 @@ void ProjectManager::_install_project(const String &p_zip_path, const String &p_ } void ProjectManager::_files_dropped(PackedStringArray p_files) { + // TODO: Support installing multiple ZIPs at the same time? if (p_files.size() == 1 && p_files[0].ends_with(".zip")) { - const String file = p_files[0].get_file(); - _install_project(p_files[0], file.substr(0, file.length() - 4).capitalize()); + const String &file = p_files[0]; + _install_project(file, file.get_file().get_basename().capitalize()); return; } + HashSet folders_set; Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); for (int i = 0; i < p_files.size(); i++) { - String file = p_files[i]; + const String &file = p_files[i]; folders_set.insert(da->dir_exists(file) ? file : file.get_base_dir()); } - if (folders_set.size() > 0) { - PackedStringArray folders; - for (const String &E : folders_set) { - folders.push_back(E); - } - - bool confirm = true; - if (folders.size() == 1) { - Ref dir = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (dir->change_dir(folders[0]) == OK) { - dir->list_dir_begin(); - String file = dir->get_next(); - while (confirm && !file.is_empty()) { - if (!dir->current_is_dir() && file.ends_with("project.godot")) { - confirm = false; - } - file = dir->get_next(); - } - dir->list_dir_end(); - } - } - if (confirm) { - multi_scan_ask->get_ok_button()->disconnect("pressed", callable_mp(this, &ProjectManager::_scan_multiple_folders)); - multi_scan_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_scan_multiple_folders).bind(folders)); - multi_scan_ask->set_text( - vformat(TTR("Are you sure to scan %s folders for existing Godot projects?\nThis could take a while."), folders.size())); - multi_scan_ask->popup_centered(); - } else { - _scan_multiple_folders(folders); - } - } -} + ERR_FAIL_COND(folders_set.size() == 0); // This can't really happen, we consume every dropped file path above. -void ProjectManager::_scan_multiple_folders(PackedStringArray p_files) { - for (int i = 0; i < p_files.size(); i++) { - _scan_begin(p_files.get(i)); + PackedStringArray folders; + for (const String &E : folders_set) { + folders.push_back(E); } + _project_list->find_projects_multiple(folders); } void ProjectManager::_on_order_option_changed(int p_idx) { @@ -2740,11 +2726,6 @@ void ProjectManager::_on_search_term_submitted(const String &p_text) { _open_selected_projects_ask(); } -void ProjectManager::_bind_methods() { - ClassDB::bind_method("_update_project_buttons", &ProjectManager::_update_project_buttons); - ClassDB::bind_method("_version_button_pressed", &ProjectManager::_version_button_pressed); -} - void ProjectManager::_open_asset_library() { asset_library->disable_community_support(); tabs->set_current_tab(1); @@ -2870,8 +2851,8 @@ ProjectManager::ProjectManager() { vb->add_child(center_box); tabs = memnew(TabContainer); - center_box->add_child(tabs); tabs->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); + center_box->add_child(tabs); tabs->connect("tab_changed", callable_mp(this, &ProjectManager::_on_tab_changed)); local_projects_vb = memnew(VBoxContainer); @@ -2950,10 +2931,11 @@ ProjectManager::ProjectManager() { search_tree_hb->add_child(search_panel); _project_list = memnew(ProjectList); - _project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons)); - _project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask)); _project_list->set_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); @@ -3099,7 +3081,7 @@ ProjectManager::ProjectManager() { scan_dir->set_title(TTR("Select a Folder to Scan")); // must be after mode or it's overridden scan_dir->set_current_dir(EDITOR_GET("filesystem/directories/default_project_path")); add_child(scan_dir); - scan_dir->connect("dir_selected", callable_mp(this, &ProjectManager::_scan_begin)); + scan_dir->connect("dir_selected", callable_mp(_project_list, &ProjectList::find_projects)); erase_missing_ask = memnew(ConfirmationDialog); erase_missing_ask->set_ok_button_text(TTR("Remove All")); @@ -3133,10 +3115,6 @@ ProjectManager::ProjectManager() { multi_run_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_run_project_confirm)); add_child(multi_run_ask); - multi_scan_ask = memnew(ConfirmationDialog); - multi_scan_ask->set_ok_button_text(TTR("Scan")); - add_child(multi_scan_ask); - 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)); @@ -3245,29 +3223,36 @@ ProjectManager::ProjectManager() { create_tag_btn->connect("pressed", callable_mp((Window *)create_tag_dialog, &Window::popup_centered).bind(Vector2i(500, 0) * EDSCALE)); } - _project_list->migrate_config(); - _load_recent_projects(); - - Ref dir_access = DirAccess::create(DirAccess::AccessType::ACCESS_FILESYSTEM); + // Initialize project list. + { + Ref dir_access = DirAccess::create(DirAccess::AccessType::ACCESS_FILESYSTEM); - String default_project_path = EDITOR_GET("filesystem/directories/default_project_path"); - if (!dir_access->dir_exists(default_project_path)) { - Error error = dir_access->make_dir_recursive(default_project_path); - if (error != OK) { - ERR_PRINT("Could not create default project directory at: " + default_project_path); + String default_project_path = EDITOR_GET("filesystem/directories/default_project_path"); + if (!default_project_path.is_empty() && !dir_access->dir_exists(default_project_path)) { + Error error = dir_access->make_dir_recursive(default_project_path); + if (error != OK) { + ERR_PRINT("Could not create default project directory at: " + default_project_path); + } } - } - String autoscan_path = EDITOR_GET("filesystem/directories/autoscan_project_path"); - if (!autoscan_path.is_empty()) { - if (dir_access->dir_exists(autoscan_path)) { - _scan_begin(autoscan_path); - } else { - Error error = dir_access->make_dir_recursive(autoscan_path); - if (error != OK) { - ERR_PRINT("Could not create project autoscan directory at: " + autoscan_path); + bool scanned_for_projects = false; // Scanning will update the list automatically. + + String autoscan_path = EDITOR_GET("filesystem/directories/autoscan_project_path"); + if (!autoscan_path.is_empty()) { + if (dir_access->dir_exists(autoscan_path)) { + _project_list->find_projects(autoscan_path); + scanned_for_projects = true; + } else { + Error error = dir_access->make_dir_recursive(autoscan_path); + if (error != OK) { + ERR_PRINT("Could not create project autoscan directory at: " + autoscan_path); + } } } + + if (!scanned_for_projects) { + _project_list->update_project_list(); + } } SceneTree::get_singleton()->get_root()->connect("files_dropped", callable_mp(this, &ProjectManager::_files_dropped)); diff --git a/editor/project_manager.h b/editor/project_manager.h index 418567905710..4e00cf19f765 100644 --- a/editor/project_manager.h +++ b/editor/project_manager.h @@ -267,6 +267,9 @@ class ProjectList : public ScrollContainer { 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 *r_projects); + void _clear_project_selection(); void _toggle_project(int p_index); void _select_project_nocheck(int p_index); @@ -288,12 +291,15 @@ class ProjectList : public ScrollContainer { static void _bind_methods(); public: + static const char *SIGNAL_LIST_CHANGED; static const char *SIGNAL_SELECTION_CHANGED; static const char *SIGNAL_PROJECT_ASK_OPEN; - void load_projects(); + void update_project_list(); int get_project_count() const; + void find_projects(const String &p_path); + void find_projects_multiple(const PackedStringArray &p_paths); void sort_projects(); void add_project(const String &dir_path, bool favorite); @@ -316,7 +322,6 @@ class ProjectList : public ScrollContainer { void set_order_option(int p_option); void update_dock_menu(); - void migrate_config(); void save_config(); ProjectList(); @@ -367,7 +372,6 @@ class ProjectManager : public Control { ConfirmationDialog *erase_missing_ask = nullptr; ConfirmationDialog *multi_open_ask = nullptr; ConfirmationDialog *multi_run_ask = nullptr; - ConfirmationDialog *multi_scan_ask = nullptr; ConfirmationDialog *ask_full_convert_dialog = nullptr; ConfirmationDialog *ask_update_settings = nullptr; ConfirmationDialog *open_templates = nullptr; @@ -423,12 +427,8 @@ class ProjectManager : public Control { void _set_new_tag_name(const String p_name); void _create_new_tag(); - void _load_recent_projects(); void _on_project_created(const String &dir); void _on_projects_updated(); - void _scan_multiple_folders(PackedStringArray p_files); - void _scan_begin(const String &p_base); - void _scan_dir(const String &path); void _install_project(const String &p_zip_path, const String &p_title); @@ -447,7 +447,6 @@ class ProjectManager : public Control { protected: void _notification(int p_what); - static void _bind_methods(); public: static ProjectManager *get_singleton() { return singleton; }