1/**************************************************************************/
2/* editor_quick_open.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "editor_quick_open.h"
32
33#include "core/os/keyboard.h"
34#include "editor/editor_node.h"
35#include "editor/editor_scale.h"
36
37Rect2i EditorQuickOpen::prev_rect = Rect2i();
38bool EditorQuickOpen::was_showed = false;
39
40void EditorQuickOpen::popup_dialog(const String &p_base, bool p_enable_multi, bool p_dont_clear) {
41 base_type = p_base;
42 allow_multi_select = p_enable_multi;
43 search_options->set_select_mode(allow_multi_select ? Tree::SELECT_MULTI : Tree::SELECT_SINGLE);
44
45 if (was_showed) {
46 popup(prev_rect);
47 } else {
48 popup_centered_clamped(Size2(600, 440) * EDSCALE, 0.8f);
49 }
50
51 EditorFileSystemDirectory *efsd = EditorFileSystem::get_singleton()->get_filesystem();
52 _build_search_cache(efsd);
53
54 if (p_dont_clear) {
55 search_box->select_all();
56 _update_search();
57 } else {
58 search_box->clear(); // This will emit text_changed.
59 }
60 search_box->grab_focus();
61}
62
63void EditorQuickOpen::_build_search_cache(EditorFileSystemDirectory *p_efsd) {
64 for (int i = 0; i < p_efsd->get_subdir_count(); i++) {
65 _build_search_cache(p_efsd->get_subdir(i));
66 }
67
68 Vector<String> base_types = base_type.split(",");
69 for (int i = 0; i < p_efsd->get_file_count(); i++) {
70 String file = p_efsd->get_file_path(i);
71 String engine_type = p_efsd->get_file_type(i);
72 String script_type = p_efsd->get_file_resource_script_class(i);
73 String actual_type = script_type.is_empty() ? engine_type : script_type;
74
75 // Iterate all possible base types.
76 for (String &parent_type : base_types) {
77 if (ClassDB::is_parent_class(engine_type, parent_type) || EditorNode::get_editor_data().script_class_is_parent(script_type, parent_type)) {
78 files.push_back(file.substr(6, file.length()));
79
80 // Store refs to used icons.
81 String ext = file.get_extension();
82 if (!icons.has(ext)) {
83 icons.insert(ext, EditorNode::get_singleton()->get_class_icon(actual_type, "Object"));
84 }
85
86 // Stop testing base types as soon as we got a match.
87 break;
88 }
89 }
90 }
91}
92
93void EditorQuickOpen::_update_search() {
94 const String search_text = search_box->get_text();
95 const bool empty_search = search_text.is_empty();
96
97 // Filter possible candidates.
98 Vector<Entry> entries;
99 for (int i = 0; i < files.size(); i++) {
100 if (empty_search || search_text.is_subsequence_ofn(files[i])) {
101 Entry r;
102 r.path = files[i];
103 r.score = empty_search ? 0 : _score_path(search_text, files[i].to_lower());
104 entries.push_back(r);
105 }
106 }
107
108 // Display results
109 TreeItem *root = search_options->get_root();
110 root->clear_children();
111
112 if (entries.size() > 0) {
113 if (!empty_search) {
114 SortArray<Entry, EntryComparator> sorter;
115 sorter.sort(entries.ptrw(), entries.size());
116 }
117
118 const int entry_limit = MIN(entries.size(), 300);
119 for (int i = 0; i < entry_limit; i++) {
120 TreeItem *ti = search_options->create_item(root);
121 ti->set_text(0, entries[i].path);
122 ti->set_icon(0, *icons.lookup_ptr(entries[i].path.get_extension()));
123 }
124
125 TreeItem *to_select = root->get_first_child();
126 to_select->select(0);
127 to_select->set_as_cursor(0);
128 search_options->scroll_to_item(to_select);
129
130 get_ok_button()->set_disabled(false);
131 } else {
132 search_options->deselect_all();
133
134 get_ok_button()->set_disabled(true);
135 }
136}
137
138float EditorQuickOpen::_score_path(const String &p_search, const String &p_path) {
139 float score = 0.9f + .1f * (p_search.length() / (float)p_path.length());
140
141 // Exact match.
142 if (p_search == p_path) {
143 return 1.2f;
144 }
145
146 // Positive bias for matches close to the beginning of the file name.
147 String file = p_path.get_file();
148 int pos = file.findn(p_search);
149 if (pos != -1) {
150 return score * (1.0f - 0.1f * (float(pos) / file.length()));
151 }
152
153 // Similarity
154 return p_path.to_lower().similarity(p_search.to_lower());
155}
156
157void EditorQuickOpen::_confirmed() {
158 if (!search_options->get_selected()) {
159 return;
160 }
161 _cleanup();
162 hide();
163 emit_signal(SNAME("quick_open"));
164}
165
166void EditorQuickOpen::cancel_pressed() {
167 _cleanup();
168}
169
170void EditorQuickOpen::_cleanup() {
171 files.clear();
172 icons.clear();
173}
174
175void EditorQuickOpen::_text_changed(const String &p_newtext) {
176 _update_search();
177}
178
179void EditorQuickOpen::_sbox_input(const Ref<InputEvent> &p_ie) {
180 Ref<InputEventKey> k = p_ie;
181 if (k.is_valid()) {
182 switch (k->get_keycode()) {
183 case Key::UP:
184 case Key::DOWN:
185 case Key::PAGEUP:
186 case Key::PAGEDOWN: {
187 search_options->gui_input(k);
188 search_box->accept_event();
189
190 if (allow_multi_select) {
191 TreeItem *root = search_options->get_root();
192 if (!root->get_first_child()) {
193 break;
194 }
195
196 TreeItem *current = search_options->get_selected();
197 TreeItem *item = search_options->get_next_selected(root);
198 while (item) {
199 item->deselect(0);
200 item = search_options->get_next_selected(item);
201 }
202
203 current->select(0);
204 current->set_as_cursor(0);
205 }
206 } break;
207 default:
208 break;
209 }
210 }
211}
212
213String EditorQuickOpen::get_selected() const {
214 TreeItem *ti = search_options->get_selected();
215 if (!ti) {
216 return String();
217 }
218
219 return "res://" + ti->get_text(0);
220}
221
222Vector<String> EditorQuickOpen::get_selected_files() const {
223 Vector<String> selected_files;
224
225 TreeItem *item = search_options->get_next_selected(search_options->get_root());
226 while (item) {
227 selected_files.push_back("res://" + item->get_text(0));
228 item = search_options->get_next_selected(item);
229 }
230
231 return selected_files;
232}
233
234String EditorQuickOpen::get_base_type() const {
235 return base_type;
236}
237
238void EditorQuickOpen::_notification(int p_what) {
239 switch (p_what) {
240 case NOTIFICATION_ENTER_TREE: {
241 connect("confirmed", callable_mp(this, &EditorQuickOpen::_confirmed));
242
243 search_box->set_clear_button_enabled(true);
244 } break;
245
246 case NOTIFICATION_VISIBILITY_CHANGED: {
247 if (!is_visible()) {
248 prev_rect = Rect2i(get_position(), get_size());
249 was_showed = true;
250 }
251 } break;
252
253 case NOTIFICATION_EXIT_TREE: {
254 disconnect("confirmed", callable_mp(this, &EditorQuickOpen::_confirmed));
255 } break;
256 }
257}
258
259void EditorQuickOpen::_theme_changed() {
260 search_box->set_right_icon(search_options->get_editor_theme_icon(SNAME("Search")));
261}
262
263void EditorQuickOpen::_bind_methods() {
264 ADD_SIGNAL(MethodInfo("quick_open"));
265}
266
267EditorQuickOpen::EditorQuickOpen() {
268 VBoxContainer *vbc = memnew(VBoxContainer);
269 vbc->connect("theme_changed", callable_mp(this, &EditorQuickOpen::_theme_changed));
270 add_child(vbc);
271
272 search_box = memnew(LineEdit);
273 search_box->connect("text_changed", callable_mp(this, &EditorQuickOpen::_text_changed));
274 search_box->connect("gui_input", callable_mp(this, &EditorQuickOpen::_sbox_input));
275 vbc->add_margin_child(TTR("Search:"), search_box);
276 register_text_enter(search_box);
277
278 search_options = memnew(Tree);
279 search_options->connect("item_activated", callable_mp(this, &EditorQuickOpen::_confirmed));
280 search_options->create_item();
281 search_options->set_hide_root(true);
282 search_options->set_hide_folding(true);
283 search_options->add_theme_constant_override("draw_guides", 1);
284 vbc->add_margin_child(TTR("Matches:"), search_options, true);
285
286 set_ok_button_text(TTR("Open"));
287 set_hide_on_ok(false);
288}
289