From 269ed93271bc3e6a6eaecc6db4e1bfbcec69c1a8 Mon Sep 17 00:00:00 2001 From: Stijn Hinlopen Date: Mon, 13 Jul 2020 21:23:50 +0200 Subject: [PATCH] Improve quick open performance and update some behaviour - Cache possible files and icons at popup - Sort files with heapsort instead of selection sort - Always scroll back to top (selection) upon refresh - Scoring function: fix second case and remove expensive similarity computation for insignificant results. - Only show a max amount of files (currently set at 300) --- editor/quick_open.cpp | 273 +++++++++++++++++++++--------------------- editor/quick_open.h | 31 +++-- 2 files changed, 159 insertions(+), 145 deletions(-) diff --git a/editor/quick_open.cpp b/editor/quick_open.cpp index 4af6fb20532..e1308b4895f 100644 --- a/editor/quick_open.cpp +++ b/editor/quick_open.cpp @@ -34,38 +34,123 @@ void EditorQuickOpen::popup_dialog(const StringName &p_base, bool p_enable_multi, bool p_dontclear) { base_type = p_base; - search_options->set_select_mode(p_enable_multi ? Tree::SELECT_MULTI : Tree::SELECT_SINGLE); - popup_centered_ratio(0.4); + allow_multi_select = p_enable_multi; + search_options->set_select_mode(allow_multi_select ? Tree::SELECT_MULTI : Tree::SELECT_SINGLE); + popup_centered_clamped(Size2i(600, 440), 0.8f); + + EditorFileSystemDirectory *efsd = EditorFileSystem::get_singleton()->get_filesystem(); + _build_search_cache(efsd); if (p_dontclear) { search_box->select_all(); + _update_search(); } else { - search_box->clear(); + search_box->clear(); // This will emit text_changed. } - search_box->grab_focus(); - _update_search(); } -String EditorQuickOpen::get_selected() const { - TreeItem *ti = search_options->get_selected(); - if (!ti) { - return String(); +void EditorQuickOpen::_build_search_cache(EditorFileSystemDirectory *p_efsd) { + for (int i = 0; i < p_efsd->get_subdir_count(); i++) { + _build_search_cache(p_efsd->get_subdir(i)); } - return "res://" + ti->get_text(0); + for (int i = 0; i < p_efsd->get_file_count(); i++) { + String file_type = p_efsd->get_file_type(i); + if (ClassDB::is_parent_class(file_type, base_type)) { + String file = p_efsd->get_file_path(i); + files.push_back(file.substr(6, file.length())); + + // Store refs to used icons. + String ext = file.get_extension(); + if (!icons.has(ext)) { + icons.insert(ext, get_theme_icon((has_theme_icon(file_type, "EditorIcons") ? file_type : "Object"), "EditorIcons")); + } + } + } } -Vector EditorQuickOpen::get_selected_files() const { - Vector files; +void EditorQuickOpen::_update_search() { + const String search_text = search_box->get_text(); + const bool empty_search = search_text == ""; - TreeItem *item = search_options->get_next_selected(search_options->get_root()); - while (item) { - files.push_back("res://" + item->get_text(0)); - item = search_options->get_next_selected(item); + // Filter possible candidates. + Vector entries; + for (int i = 0; i < files.size(); i++) { + if (empty_search || search_text.is_subsequence_ofi(files[i])) { + Entry r; + r.path = files[i]; + r.score = empty_search ? 0 : _score_path(search_text, files[i].to_lower()); + entries.push_back(r); + } } - return files; + // Display results + TreeItem *root = search_options->get_root(); + root->clear_children(); + + if (entries.size() > 0) { + if (!empty_search) { + SortArray sorter; + sorter.sort(entries.ptrw(), entries.size()); + } + + const int entry_limit = MIN(entries.size(), 300); + for (int i = 0; i < entry_limit; i++) { + TreeItem *ti = search_options->create_item(root); + ti->set_text(0, entries[i].path); + ti->set_icon(0, *icons.lookup_ptr(entries[i].path.get_extension())); + } + + TreeItem *to_select = root->get_children(); + to_select->select(0); + to_select->set_as_cursor(0); + search_options->scroll_to_item(to_select); + + get_ok()->set_disabled(false); + } else { + search_options->deselect_all(); + + get_ok()->set_disabled(true); + } +} + +float EditorQuickOpen::_score_path(const String &p_search, const String &p_path) { + float score = 0.9f + .1f * (p_search.length() / (float)p_path.length()); + + // Positive bias for matches close to the beginning of the file name. + String file = p_path.get_file(); + int pos = file.findn(p_search); + if (pos != -1) { + return score * (1.0f - 0.1f * (float(pos) / file.length())); + } + + // Positive bias for matches close to the end of the path. + pos = p_path.rfindn(p_search); + if (pos != -1) { + return score * (0.8f - 0.1f * (float(p_path.length() - pos) / p_path.length())); + } + + // Remaining results belong to the same class of results. + return score * 0.69f; +} + +void EditorQuickOpen::_confirmed() { + if (!search_options->get_selected()) { + return; + } + _cleanup(); + emit_signal("quick_open"); + hide(); +} + +void EditorQuickOpen::cancel_pressed() { + _cleanup(); +} + +void EditorQuickOpen::_cleanup() { + files.clear(); + icons.clear(); } void EditorQuickOpen::_text_changed(const String &p_newtext) { @@ -83,134 +168,50 @@ void EditorQuickOpen::_sbox_input(const Ref &p_ie) { search_options->call("_gui_input", k); search_box->accept_event(); - TreeItem *root = search_options->get_root(); - if (!root->get_children()) { - break; - } + if (allow_multi_select) { + TreeItem *root = search_options->get_root(); + if (!root->get_children()) { + break; + } - TreeItem *current = search_options->get_selected(); - TreeItem *item = search_options->get_next_selected(root); - while (item) { - item->deselect(0); - item = search_options->get_next_selected(item); - } + TreeItem *current = search_options->get_selected(); + TreeItem *item = search_options->get_next_selected(root); + while (item) { + item->deselect(0); + item = search_options->get_next_selected(item); + } - current->select(0); - current->set_as_cursor(0); + current->select(0); + current->set_as_cursor(0); + } } break; } } } -float EditorQuickOpen::_score_path(String search, String path) const { - // Positive bias for matches close to the _beginning of the file name_. - String file = path.get_file(); - int pos = file.findn(search); - if (pos != -1) { - return 1.0f - 0.1f * (float(pos) / file.length()); +String EditorQuickOpen::get_selected() const { + TreeItem *ti = search_options->get_selected(); + if (!ti) { + return String(); } - // Positive bias for matches close to the _end of the path_. - String base = path.get_base_dir(); - pos = base.rfindn(search); - if (pos != -1) { - return 0.9f - 0.1f * (float(base.length() - pos) / base.length()); - } - - // Results that contain all characters but not the string. - return path.similarity(search) * 0.8f; + return "res://" + ti->get_text(0); } -void EditorQuickOpen::_parse_fs(EditorFileSystemDirectory *efsd, Vector>> &list) { - for (int i = 0; i < efsd->get_subdir_count(); i++) { - _parse_fs(efsd->get_subdir(i), list); +Vector EditorQuickOpen::get_selected_files() const { + Vector selected_files; + + TreeItem *item = search_options->get_next_selected(search_options->get_root()); + while (item) { + selected_files.push_back("res://" + item->get_text(0)); + item = search_options->get_next_selected(item); } - for (int i = 0; i < efsd->get_file_count(); i++) { - StringName file_type = efsd->get_file_type(i); - - if (ClassDB::is_parent_class(file_type, base_type)) { - String file = efsd->get_file_path(i); - file = file.substr(6, file.length()); - - if (search_box->get_text().is_subsequence_ofi(file)) { - Pair> pair; - pair.first = file; - pair.second = search_options->get_theme_icon(search_options->has_theme_icon(file_type, ei) ? file_type : ot, ei); - list.push_back(pair); - } - } - } + return selected_files; } -Vector>> EditorQuickOpen::_sort_fs(Vector>> &list) { - String search_text = search_box->get_text().to_lower(); - Vector>> sorted_list; - - if (search_text == String() || list.size() == 0) { - return list; - } - - Vector scores; - scores.resize(list.size()); - for (int i = 0; i < list.size(); i++) { - scores.write[i] = _score_path(search_text, list[i].first.to_lower()); - } - - while (list.size() > 0) { - float best_score = 0.0f; - int best_idx = 0; - - for (int i = 0; i < list.size(); i++) { - float current_score = scores[i]; - if (current_score > best_score) { - best_score = current_score; - best_idx = i; - } - } - - sorted_list.push_back(list[best_idx]); - list.remove(best_idx); - scores.remove(best_idx); - } - - return sorted_list; -} - -void EditorQuickOpen::_update_search() { - search_options->clear(); - TreeItem *root = search_options->create_item(); - EditorFileSystemDirectory *efsd = EditorFileSystem::get_singleton()->get_filesystem(); - Vector>> list; - - _parse_fs(efsd, list); - list = _sort_fs(list); - - for (int i = 0; i < list.size(); i++) { - TreeItem *ti = search_options->create_item(root); - ti->set_text(0, list[i].first); - ti->set_icon(0, list[i].second); - } - - TreeItem *result = root->get_children(); - if (result) { - result->select(0); - result->set_as_cursor(0); - } - - get_ok()->set_disabled(!result); -} - -void EditorQuickOpen::_confirmed() { - if (!search_options->get_selected()) { - return; - } - emit_signal("quick_open"); - hide(); -} - -void EditorQuickOpen::_theme_changed() { - search_box->set_right_icon(search_options->get_theme_icon("Search", ei)); +StringName EditorQuickOpen::get_base_type() const { + return base_type; } void EditorQuickOpen::_notification(int p_what) { @@ -226,8 +227,8 @@ void EditorQuickOpen::_notification(int p_what) { } } -StringName EditorQuickOpen::get_base_type() const { - return base_type; +void EditorQuickOpen::_theme_changed() { + search_box->set_right_icon(search_options->get_theme_icon("Search", "EditorIcons")); } void EditorQuickOpen::_bind_methods() { @@ -235,6 +236,8 @@ void EditorQuickOpen::_bind_methods() { } EditorQuickOpen::EditorQuickOpen() { + allow_multi_select = false; + VBoxContainer *vbc = memnew(VBoxContainer); vbc->connect("theme_changed", callable_mp(this, &EditorQuickOpen::_theme_changed)); add_child(vbc); @@ -243,18 +246,16 @@ EditorQuickOpen::EditorQuickOpen() { search_box->connect("text_changed", callable_mp(this, &EditorQuickOpen::_text_changed)); search_box->connect("gui_input", callable_mp(this, &EditorQuickOpen::_sbox_input)); vbc->add_margin_child(TTR("Search:"), search_box); + register_text_enter(search_box); search_options = memnew(Tree); search_options->connect("item_activated", callable_mp(this, &EditorQuickOpen::_confirmed)); + search_options->create_item(); search_options->set_hide_root(true); search_options->set_hide_folding(true); search_options->add_theme_constant_override("draw_guides", 1); vbc->add_margin_child(TTR("Matches:"), search_options, true); get_ok()->set_text(TTR("Open")); - register_text_enter(search_box); set_hide_on_ok(false); - - ei = "EditorIcons"; - ot = "Object"; } diff --git a/editor/quick_open.h b/editor/quick_open.h index 5bcdfc7bf22..6486ee02214 100644 --- a/editor/quick_open.h +++ b/editor/quick_open.h @@ -31,7 +31,7 @@ #ifndef EDITOR_QUICK_OPEN_H #define EDITOR_QUICK_OPEN_H -#include "core/pair.h" +#include "core/oa_hash_map.h" #include "editor_file_system.h" #include "scene/gui/dialogs.h" #include "scene/gui/tree.h" @@ -41,19 +41,32 @@ class EditorQuickOpen : public ConfirmationDialog { LineEdit *search_box; Tree *search_options; - StringName base_type; - StringName ei; - StringName ot; + bool allow_multi_select; + + Vector files; + OAHashMap> icons; + + struct Entry { + String path; + float score; + }; + + struct EntryComparator { + _FORCE_INLINE_ bool operator()(const Entry &A, const Entry &B) const { + return A.score > B.score; + } + }; void _update_search(); - - void _sbox_input(const Ref &p_ie); - void _parse_fs(EditorFileSystemDirectory *efsd, Vector>> &list); - Vector>> _sort_fs(Vector>> &list); - float _score_path(String search, String path) const; + void _build_search_cache(EditorFileSystemDirectory *p_efsd); + float _score_path(const String &p_search, const String &p_path); void _confirmed(); + virtual void cancel_pressed() override; + void _cleanup(); + + void _sbox_input(const Ref &p_ie); void _text_changed(const String &p_newtext); void _theme_changed();