-
-
Notifications
You must be signed in to change notification settings - Fork 21.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize scanning routines in the project manager #86271
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<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). | ||
|
@@ -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<InputEvent> &p_ev) { | |
} | ||
} | ||
|
||
void ProjectManager::_load_recent_projects() { | ||
_project_list->set_search_term(search_box->get_text().strip_edges()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not really sure why we used to do this before updating the project list. Changing the filter already sets the search term, and during the first scan we don't have any search term anyway, it's all empty. So this bit I simply removed even before removing the method itself. I tested and didn't see issues. The search term was still applied after scanning and importing dropped folders. |
||
_project_list->load_projects(); | ||
|
||
_update_project_buttons(); | ||
|
||
tabs->set_current_tab(0); | ||
} | ||
|
||
void ProjectManager::_on_projects_updated() { | ||
Vector<ProjectList::Item> 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<DirAccess> 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<String> folders_set; | ||
Ref<DirAccess> 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<DirAccess> 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<DirAccess> dir_access = DirAccess::create(DirAccess::AccessType::ACCESS_FILESYSTEM); | ||
// Initialize project list. | ||
{ | ||
Ref<DirAccess> 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)); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated, but these should be StringNames (probably not static then).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I tried making them static StringNames, but it didn't quite work. I didn't want to spend time adding the usual framework to make them accessible outside of the class with this PR, so I just followed the existing pattern. But I will rework this in a future PR. We probably can have a "project manager string names" file, like we do with other contexts.