Browse Source

Merge pull request #40376 from hinlopen/quick-open-improvements

Improve quick open performance and behaviour
Rémi Verschelde 5 years ago
parent
commit
0f369ac2c0
2 changed files with 158 additions and 144 deletions
  1. 136 135
      editor/quick_open.cpp
  2. 22 9
      editor/quick_open.h

+ 136 - 135
editor/quick_open.cpp

@@ -34,183 +34,184 @@
 
 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()));
 
-Vector<String> EditorQuickOpen::get_selected_files() const {
-	Vector<String> files;
+			// 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"));
+			}
+		}
+	}
+}
 
-	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);
+void EditorQuickOpen::_update_search() {
+	const String search_text = search_box->get_text();
+	const bool empty_search = search_text == "";
+
+	// Filter possible candidates.
+	Vector<Entry> 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();
 
-void EditorQuickOpen::_text_changed(const String &p_newtext) {
-	_update_search();
-}
+	if (entries.size() > 0) {
+		if (!empty_search) {
+			SortArray<Entry, EntryComparator> sorter;
+			sorter.sort(entries.ptrw(), entries.size());
+		}
 
-void EditorQuickOpen::_sbox_input(const Ref<InputEvent> &p_ie) {
-	Ref<InputEventKey> k = p_ie;
-	if (k.is_valid()) {
-		switch (k->get_keycode()) {
-			case KEY_UP:
-			case KEY_DOWN:
-			case KEY_PAGEUP:
-			case KEY_PAGEDOWN: {
-				search_options->call("_gui_input", k);
-				search_box->accept_event();
+		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 *root = search_options->get_root();
-				if (!root->get_children()) {
-					break;
-				}
+		TreeItem *to_select = root->get_children();
+		to_select->select(0);
+		to_select->set_as_cursor(0);
+		search_options->scroll_to_item(to_select);
 
-				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);
-				}
+		get_ok()->set_disabled(false);
+	} else {
+		search_options->deselect_all();
 
-				current->select(0);
-				current->set_as_cursor(0);
-			} break;
-		}
+		get_ok()->set_disabled(true);
 	}
 }
 
-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);
+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 1.0f - 0.1f * (float(pos) / file.length());
+		return score * (1.0f - 0.1f * (float(pos) / file.length()));
 	}
 
-	// Positive bias for matches close to the _end of the path_.
-	String base = path.get_base_dir();
-	pos = base.rfindn(search);
+	// Positive bias for matches close to the end of the path.
+	pos = p_path.rfindn(p_search);
 	if (pos != -1) {
-		return 0.9f - 0.1f * (float(base.length() - pos) / base.length());
+		return score * (0.8f - 0.1f * (float(p_path.length() - pos) / p_path.length()));
 	}
 
-	// Results that contain all characters but not the string.
-	return path.similarity(search) * 0.8f;
+	// Remaining results belong to the same class of results.
+	return score * 0.69f;
 }
 
-void EditorQuickOpen::_parse_fs(EditorFileSystemDirectory *efsd, Vector<Pair<String, Ref<Texture2D>>> &list) {
-	for (int i = 0; i < efsd->get_subdir_count(); i++) {
-		_parse_fs(efsd->get_subdir(i), list);
-	}
-
-	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<String, Ref<Texture2D>> 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);
-			}
-		}
+void EditorQuickOpen::_confirmed() {
+	if (!search_options->get_selected()) {
+		return;
 	}
+	_cleanup();
+	emit_signal("quick_open");
+	hide();
 }
 
-Vector<Pair<String, Ref<Texture2D>>> EditorQuickOpen::_sort_fs(Vector<Pair<String, Ref<Texture2D>>> &list) {
-	String search_text = search_box->get_text().to_lower();
-	Vector<Pair<String, Ref<Texture2D>>> sorted_list;
+void EditorQuickOpen::cancel_pressed() {
+	_cleanup();
+}
 
-	if (search_text == String() || list.size() == 0) {
-		return list;
-	}
+void EditorQuickOpen::_cleanup() {
+	files.clear();
+	icons.clear();
+}
 
-	Vector<float> 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());
-	}
+void EditorQuickOpen::_text_changed(const String &p_newtext) {
+	_update_search();
+}
 
-	while (list.size() > 0) {
-		float best_score = 0.0f;
-		int best_idx = 0;
+void EditorQuickOpen::_sbox_input(const Ref<InputEvent> &p_ie) {
+	Ref<InputEventKey> k = p_ie;
+	if (k.is_valid()) {
+		switch (k->get_keycode()) {
+			case KEY_UP:
+			case KEY_DOWN:
+			case KEY_PAGEUP:
+			case KEY_PAGEDOWN: {
+				search_options->call("_gui_input", k);
+				search_box->accept_event();
 
-		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;
-			}
+				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);
+					}
+
+					current->select(0);
+					current->set_as_cursor(0);
+				}
+			} break;
 		}
-
-		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<Pair<String, Ref<Texture2D>>> 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);
+String EditorQuickOpen::get_selected() const {
+	TreeItem *ti = search_options->get_selected();
+	if (!ti) {
+		return String();
 	}
 
-	get_ok()->set_disabled(!result);
+	return "res://" + ti->get_text(0);
 }
 
-void EditorQuickOpen::_confirmed() {
-	if (!search_options->get_selected()) {
-		return;
+Vector<String> EditorQuickOpen::get_selected_files() const {
+	Vector<String> 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);
 	}
-	emit_signal("quick_open");
-	hide();
+
+	return selected_files;
 }
 
-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";
 }

+ 22 - 9
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;
 
-	void _update_search();
+	Vector<String> files;
+	OAHashMap<String, Ref<Texture2D>> icons;
 
-	void _sbox_input(const Ref<InputEvent> &p_ie);
-	void _parse_fs(EditorFileSystemDirectory *efsd, Vector<Pair<String, Ref<Texture2D>>> &list);
-	Vector<Pair<String, Ref<Texture2D>>> _sort_fs(Vector<Pair<String, Ref<Texture2D>>> &list);
-	float _score_path(String search, String path) const;
+	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 _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<InputEvent> &p_ie);
 	void _text_changed(const String &p_newtext);
 
 	void _theme_changed();