1/**************************************************************************/
2/* editor_help_search.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_help_search.h"
32
33#include "core/os/keyboard.h"
34#include "editor/editor_feature_profile.h"
35#include "editor/editor_node.h"
36#include "editor/editor_scale.h"
37#include "editor/editor_settings.h"
38#include "editor/editor_string_names.h"
39
40void EditorHelpSearch::_update_icons() {
41 search_box->set_right_icon(results_tree->get_editor_theme_icon(SNAME("Search")));
42 search_box->set_clear_button_enabled(true);
43 search_box->add_theme_icon_override("right_icon", results_tree->get_editor_theme_icon(SNAME("Search")));
44 case_sensitive_button->set_icon(results_tree->get_editor_theme_icon(SNAME("MatchCase")));
45 hierarchy_button->set_icon(results_tree->get_editor_theme_icon(SNAME("ClassList")));
46
47 if (is_visible()) {
48 _update_results();
49 }
50}
51
52void EditorHelpSearch::_update_results() {
53 String term = search_box->get_text();
54
55 int search_flags = filter_combo->get_selected_id();
56 if (case_sensitive_button->is_pressed()) {
57 search_flags |= SEARCH_CASE_SENSITIVE;
58 }
59 if (hierarchy_button->is_pressed()) {
60 search_flags |= SEARCH_SHOW_HIERARCHY;
61 }
62
63 search = Ref<Runner>(memnew(Runner(results_tree, results_tree, term, search_flags)));
64 set_process(true);
65}
66
67void EditorHelpSearch::_search_box_gui_input(const Ref<InputEvent> &p_event) {
68 // Redirect up and down navigational key events to the results list.
69 Ref<InputEventKey> key = p_event;
70 if (key.is_valid()) {
71 switch (key->get_keycode()) {
72 case Key::UP:
73 case Key::DOWN:
74 case Key::PAGEUP:
75 case Key::PAGEDOWN: {
76 results_tree->gui_input(key);
77 search_box->accept_event();
78 } break;
79 default:
80 break;
81 }
82 }
83}
84
85void EditorHelpSearch::_search_box_text_changed(const String &p_text) {
86 _update_results();
87}
88
89void EditorHelpSearch::_filter_combo_item_selected(int p_option) {
90 _update_results();
91}
92
93void EditorHelpSearch::_confirmed() {
94 TreeItem *item = results_tree->get_selected();
95 if (!item) {
96 return;
97 }
98
99 // Activate the script editor and emit the signal with the documentation link to display.
100 EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT);
101
102 emit_signal(SNAME("go_to_help"), item->get_metadata(0));
103
104 hide();
105}
106
107void EditorHelpSearch::_notification(int p_what) {
108 switch (p_what) {
109 case NOTIFICATION_VISIBILITY_CHANGED: {
110 if (!is_visible()) {
111 results_tree->call_deferred(SNAME("clear")); // Wait for the Tree's mouse event propagation.
112 get_ok_button()->set_disabled(true);
113 EditorSettings::get_singleton()->set_project_metadata("dialog_bounds", "search_help", Rect2(get_position(), get_size()));
114 }
115 } break;
116
117 case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
118 _update_icons();
119 } break;
120
121 case NOTIFICATION_READY: {
122 connect("confirmed", callable_mp(this, &EditorHelpSearch::_confirmed));
123 } break;
124
125 case NOTIFICATION_THEME_CHANGED: {
126 _update_icons();
127 } break;
128
129 case NOTIFICATION_PROCESS: {
130 // Update background search.
131 if (search.is_valid()) {
132 if (search->work()) {
133 // Search done.
134
135 // Only point to the match if it's a new search, and not just reopening a old one.
136 if (!old_search) {
137 results_tree->ensure_cursor_is_visible();
138 } else {
139 old_search = false;
140 }
141
142 get_ok_button()->set_disabled(!results_tree->get_selected());
143
144 search = Ref<Runner>();
145 set_process(false);
146 }
147 } else {
148 set_process(false);
149 }
150 } break;
151 }
152}
153
154void EditorHelpSearch::_bind_methods() {
155 ADD_SIGNAL(MethodInfo("go_to_help"));
156}
157
158void EditorHelpSearch::popup_dialog() {
159 popup_dialog(search_box->get_text());
160}
161
162void EditorHelpSearch::popup_dialog(const String &p_term) {
163 // Restore valid window bounds or pop up at default size.
164 Rect2 saved_size = EditorSettings::get_singleton()->get_project_metadata("dialog_bounds", "search_help", Rect2());
165 if (saved_size != Rect2()) {
166 popup(saved_size);
167 } else {
168 popup_centered_ratio(0.5F);
169 }
170
171 if (p_term.is_empty()) {
172 search_box->clear();
173 } else {
174 if (old_term == p_term) {
175 old_search = true;
176 } else {
177 old_term = p_term;
178 }
179
180 search_box->set_text(p_term);
181 search_box->select_all();
182 }
183 search_box->grab_focus();
184 _update_results();
185}
186
187EditorHelpSearch::EditorHelpSearch() {
188 set_hide_on_ok(false);
189 set_clamp_to_embedder(true);
190
191 set_title(TTR("Search Help"));
192
193 get_ok_button()->set_disabled(true);
194 set_ok_button_text(TTR("Open"));
195
196 // Split search and results area.
197 VBoxContainer *vbox = memnew(VBoxContainer);
198 add_child(vbox);
199
200 // Create the search box and filter controls (at the top).
201 HBoxContainer *hbox = memnew(HBoxContainer);
202 vbox->add_child(hbox);
203
204 search_box = memnew(LineEdit);
205 search_box->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
206 search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL);
207 search_box->connect("gui_input", callable_mp(this, &EditorHelpSearch::_search_box_gui_input));
208 search_box->connect("text_changed", callable_mp(this, &EditorHelpSearch::_search_box_text_changed));
209 register_text_enter(search_box);
210 hbox->add_child(search_box);
211
212 case_sensitive_button = memnew(Button);
213 case_sensitive_button->set_flat(true);
214 case_sensitive_button->set_tooltip_text(TTR("Case Sensitive"));
215 case_sensitive_button->connect("pressed", callable_mp(this, &EditorHelpSearch::_update_results));
216 case_sensitive_button->set_toggle_mode(true);
217 case_sensitive_button->set_focus_mode(Control::FOCUS_NONE);
218 hbox->add_child(case_sensitive_button);
219
220 hierarchy_button = memnew(Button);
221 hierarchy_button->set_flat(true);
222 hierarchy_button->set_tooltip_text(TTR("Show Hierarchy"));
223 hierarchy_button->connect("pressed", callable_mp(this, &EditorHelpSearch::_update_results));
224 hierarchy_button->set_toggle_mode(true);
225 hierarchy_button->set_pressed(true);
226 hierarchy_button->set_focus_mode(Control::FOCUS_NONE);
227 hbox->add_child(hierarchy_button);
228
229 filter_combo = memnew(OptionButton);
230 filter_combo->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
231 filter_combo->set_stretch_ratio(0); // Fixed width.
232 filter_combo->add_item(TTR("Display All"), SEARCH_ALL);
233 filter_combo->add_separator();
234 filter_combo->add_item(TTR("Classes Only"), SEARCH_CLASSES);
235 filter_combo->add_item(TTR("Constructors Only"), SEARCH_CONSTRUCTORS);
236 filter_combo->add_item(TTR("Methods Only"), SEARCH_METHODS);
237 filter_combo->add_item(TTR("Operators Only"), SEARCH_OPERATORS);
238 filter_combo->add_item(TTR("Signals Only"), SEARCH_SIGNALS);
239 filter_combo->add_item(TTR("Annotations Only"), SEARCH_ANNOTATIONS);
240 filter_combo->add_item(TTR("Constants Only"), SEARCH_CONSTANTS);
241 filter_combo->add_item(TTR("Properties Only"), SEARCH_PROPERTIES);
242 filter_combo->add_item(TTR("Theme Properties Only"), SEARCH_THEME_ITEMS);
243 filter_combo->connect("item_selected", callable_mp(this, &EditorHelpSearch::_filter_combo_item_selected));
244 hbox->add_child(filter_combo);
245
246 // Create the results tree.
247 results_tree = memnew(Tree);
248 results_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
249 results_tree->set_columns(2);
250 results_tree->set_column_title(0, TTR("Name"));
251 results_tree->set_column_clip_content(0, true);
252 results_tree->set_column_title(1, TTR("Member Type"));
253 results_tree->set_column_expand(1, false);
254 results_tree->set_column_custom_minimum_width(1, 150 * EDSCALE);
255 results_tree->set_column_clip_content(1, true);
256 results_tree->set_custom_minimum_size(Size2(0, 100) * EDSCALE);
257 results_tree->set_hide_root(true);
258 results_tree->set_select_mode(Tree::SELECT_ROW);
259 results_tree->connect("item_activated", callable_mp(this, &EditorHelpSearch::_confirmed));
260 results_tree->connect("item_selected", callable_mp((BaseButton *)get_ok_button(), &BaseButton::set_disabled).bind(false));
261 vbox->add_child(results_tree, true);
262}
263
264bool EditorHelpSearch::Runner::_is_class_disabled_by_feature_profile(const StringName &p_class) {
265 Ref<EditorFeatureProfile> profile = EditorFeatureProfileManager::get_singleton()->get_current_profile();
266 if (profile.is_null()) {
267 return false;
268 }
269
270 StringName class_name = p_class;
271 while (class_name != StringName()) {
272 if (!ClassDB::class_exists(class_name)) {
273 return false;
274 }
275
276 if (profile->is_class_disabled(class_name)) {
277 return true;
278 }
279 class_name = ClassDB::get_parent_class(class_name);
280 }
281
282 return false;
283}
284
285bool EditorHelpSearch::Runner::_slice() {
286 bool phase_done = false;
287 switch (phase) {
288 case PHASE_MATCH_CLASSES_INIT:
289 phase_done = _phase_match_classes_init();
290 break;
291 case PHASE_MATCH_CLASSES:
292 phase_done = _phase_match_classes();
293 break;
294 case PHASE_CLASS_ITEMS_INIT:
295 phase_done = _phase_class_items_init();
296 break;
297 case PHASE_CLASS_ITEMS:
298 phase_done = _phase_class_items();
299 break;
300 case PHASE_MEMBER_ITEMS_INIT:
301 phase_done = _phase_member_items_init();
302 break;
303 case PHASE_MEMBER_ITEMS:
304 phase_done = _phase_member_items();
305 break;
306 case PHASE_SELECT_MATCH:
307 phase_done = _phase_select_match();
308 break;
309 case PHASE_MAX:
310 return true;
311 default:
312 WARN_PRINT("Invalid or unhandled phase in EditorHelpSearch::Runner, aborting search.");
313 return true;
314 };
315
316 if (phase_done) {
317 phase++;
318 }
319 return false;
320}
321
322bool EditorHelpSearch::Runner::_phase_match_classes_init() {
323 iterator_doc = EditorHelp::get_doc_data()->class_list.begin();
324 matches.clear();
325 matched_item = nullptr;
326 match_highest_score = 0;
327
328 terms = term.split_spaces();
329 if (terms.is_empty()) {
330 terms.append(term);
331 }
332
333 return true;
334}
335
336bool EditorHelpSearch::Runner::_phase_match_classes() {
337 if (!iterator_doc) {
338 return true;
339 }
340
341 DocData::ClassDoc &class_doc = iterator_doc->value;
342 if (class_doc.name.is_empty()) {
343 ++iterator_doc;
344 return false;
345 }
346
347 if (!_is_class_disabled_by_feature_profile(class_doc.name)) {
348 ClassMatch match;
349 match.doc = &class_doc;
350
351 // Match class name.
352 if (search_flags & SEARCH_CLASSES) {
353 // If the search term is empty, add any classes which are not script docs or which don't start with
354 // a double-quotation. This will ensure that only C++ classes and explicitly named classes will
355 // be added.
356 match.name = (term.is_empty() && (!class_doc.is_script_doc || class_doc.name[0] != '\"')) || _match_string(term, class_doc.name);
357 }
358
359 // Match members only if the term is long enough, to avoid slow performance from building a large tree.
360 // Make an exception for annotations, since there are not that many of them.
361 if (term.length() > 1 || term == "@") {
362 if (search_flags & SEARCH_CONSTRUCTORS) {
363 _match_method_name_and_push_back(class_doc.constructors, &match.constructors);
364 }
365 if (search_flags & SEARCH_METHODS) {
366 _match_method_name_and_push_back(class_doc.methods, &match.methods);
367 }
368 if (search_flags & SEARCH_OPERATORS) {
369 _match_method_name_and_push_back(class_doc.operators, &match.operators);
370 }
371 if (search_flags & SEARCH_SIGNALS) {
372 for (int i = 0; i < class_doc.signals.size(); i++) {
373 if (_all_terms_in_name(class_doc.signals[i].name)) {
374 match.signals.push_back(const_cast<DocData::MethodDoc *>(&class_doc.signals[i]));
375 }
376 }
377 }
378 if (search_flags & SEARCH_CONSTANTS) {
379 for (int i = 0; i < class_doc.constants.size(); i++) {
380 if (_all_terms_in_name(class_doc.constants[i].name)) {
381 match.constants.push_back(const_cast<DocData::ConstantDoc *>(&class_doc.constants[i]));
382 }
383 }
384 }
385 if (search_flags & SEARCH_PROPERTIES) {
386 for (int i = 0; i < class_doc.properties.size(); i++) {
387 if (_all_terms_in_name(class_doc.properties[i].name)) {
388 match.properties.push_back(const_cast<DocData::PropertyDoc *>(&class_doc.properties[i]));
389 }
390 }
391 }
392 if (search_flags & SEARCH_THEME_ITEMS) {
393 for (int i = 0; i < class_doc.theme_properties.size(); i++) {
394 if (_all_terms_in_name(class_doc.theme_properties[i].name)) {
395 match.theme_properties.push_back(const_cast<DocData::ThemeItemDoc *>(&class_doc.theme_properties[i]));
396 }
397 }
398 }
399 if (search_flags & SEARCH_ANNOTATIONS) {
400 for (int i = 0; i < class_doc.annotations.size(); i++) {
401 if (_match_string(term, class_doc.annotations[i].name)) {
402 match.annotations.push_back(const_cast<DocData::MethodDoc *>(&class_doc.annotations[i]));
403 }
404 }
405 }
406 }
407 matches[class_doc.name] = match;
408 }
409
410 ++iterator_doc;
411 return !iterator_doc;
412}
413
414bool EditorHelpSearch::Runner::_phase_class_items_init() {
415 iterator_match = matches.begin();
416
417 results_tree->clear();
418 root_item = results_tree->create_item();
419 class_items.clear();
420
421 return true;
422}
423
424bool EditorHelpSearch::Runner::_phase_class_items() {
425 if (!iterator_match) {
426 return true;
427 }
428 ClassMatch &match = iterator_match->value;
429
430 if (search_flags & SEARCH_SHOW_HIERARCHY) {
431 if (match.required()) {
432 _create_class_hierarchy(match);
433 }
434 } else {
435 if (match.name) {
436 _create_class_item(root_item, match.doc, false);
437 }
438 }
439
440 ++iterator_match;
441 return !iterator_match;
442}
443
444bool EditorHelpSearch::Runner::_phase_member_items_init() {
445 iterator_match = matches.begin();
446
447 return true;
448}
449
450bool EditorHelpSearch::Runner::_phase_member_items() {
451 if (!iterator_match) {
452 return true;
453 }
454
455 ClassMatch &match = iterator_match->value;
456
457 if (!match.doc || match.doc->name.is_empty()) {
458 ++iterator_match;
459 return false;
460 }
461
462 TreeItem *parent_item = (search_flags & SEARCH_SHOW_HIERARCHY) ? class_items[match.doc->name] : root_item;
463 bool constructor_created = false;
464 for (int i = 0; i < match.methods.size(); i++) {
465 String text = match.methods[i]->name;
466 if (!constructor_created) {
467 if (match.doc->name == match.methods[i]->name) {
468 text += " " + TTR("(constructors)");
469 constructor_created = true;
470 }
471 } else {
472 if (match.doc->name == match.methods[i]->name) {
473 continue;
474 }
475 }
476 _create_method_item(parent_item, match.doc, text, match.methods[i]);
477 }
478 for (int i = 0; i < match.signals.size(); i++) {
479 _create_signal_item(parent_item, match.doc, match.signals[i]);
480 }
481 for (int i = 0; i < match.constants.size(); i++) {
482 _create_constant_item(parent_item, match.doc, match.constants[i]);
483 }
484 for (int i = 0; i < match.properties.size(); i++) {
485 _create_property_item(parent_item, match.doc, match.properties[i]);
486 }
487 for (int i = 0; i < match.theme_properties.size(); i++) {
488 _create_theme_property_item(parent_item, match.doc, match.theme_properties[i]);
489 }
490 for (int i = 0; i < match.annotations.size(); i++) {
491 // Hide the redundant leading @ symbol.
492 _create_annotation_item(parent_item, match.doc, match.annotations[i]->name.substr(1), match.annotations[i]);
493 }
494
495 ++iterator_match;
496 return !iterator_match;
497}
498
499bool EditorHelpSearch::Runner::_phase_select_match() {
500 if (matched_item) {
501 matched_item->select(0);
502 }
503 return true;
504}
505
506void EditorHelpSearch::Runner::_match_method_name_and_push_back(Vector<DocData::MethodDoc> &p_methods, Vector<DocData::MethodDoc *> *r_match_methods) {
507 // Constructors, Methods, Operators...
508 for (int i = 0; i < p_methods.size(); i++) {
509 String method_name = (search_flags & SEARCH_CASE_SENSITIVE) ? p_methods[i].name : p_methods[i].name.to_lower();
510 if (_all_terms_in_name(method_name) ||
511 (term.begins_with(".") && method_name.begins_with(term.substr(1))) ||
512 (term.ends_with("(") && method_name.ends_with(term.left(term.length() - 1).strip_edges())) ||
513 (term.begins_with(".") && term.ends_with("(") && method_name == term.substr(1, term.length() - 2).strip_edges())) {
514 r_match_methods->push_back(const_cast<DocData::MethodDoc *>(&p_methods[i]));
515 }
516 }
517}
518
519bool EditorHelpSearch::Runner::_all_terms_in_name(String name) {
520 for (int i = 0; i < terms.size(); i++) {
521 if (!_match_string(terms[i], name)) {
522 return false;
523 }
524 }
525 return true;
526}
527
528bool EditorHelpSearch::Runner::_match_string(const String &p_term, const String &p_string) const {
529 if (search_flags & SEARCH_CASE_SENSITIVE) {
530 return p_string.find(p_term) > -1;
531 } else {
532 return p_string.findn(p_term) > -1;
533 }
534}
535
536void EditorHelpSearch::Runner::_match_item(TreeItem *p_item, const String &p_text) {
537 float inverse_length = 1.f / float(p_text.length());
538
539 // Favor types where search term is a substring close to the start of the type.
540 float w = 0.5f;
541 int pos = p_text.findn(term);
542 float score = (pos > -1) ? 1.0f - w * MIN(1, 3 * pos * inverse_length) : MAX(0.f, .9f - w);
543
544 // Favor shorter items: they resemble the search term more.
545 w = 0.1f;
546 score *= (1 - w) + w * (term.length() * inverse_length);
547
548 if (match_highest_score == 0 || score > match_highest_score) {
549 matched_item = p_item;
550 match_highest_score = score;
551 }
552}
553
554String EditorHelpSearch::Runner::_build_method_tooltip(const DocData::ClassDoc *p_class_doc, const DocData::MethodDoc *p_doc) const {
555 String tooltip = p_doc->return_type + " " + p_class_doc->name + "." + p_doc->name + "(";
556 for (int i = 0; i < p_doc->arguments.size(); i++) {
557 const DocData::ArgumentDoc &arg = p_doc->arguments[i];
558 tooltip += arg.type + " " + arg.name;
559 if (!arg.default_value.is_empty()) {
560 tooltip += " = " + arg.default_value;
561 }
562 if (i < p_doc->arguments.size() - 1) {
563 tooltip += ", ";
564 }
565 }
566 tooltip += ")";
567 return tooltip;
568}
569
570TreeItem *EditorHelpSearch::Runner::_create_class_hierarchy(const ClassMatch &p_match) {
571 if (p_match.doc->name.is_empty()) {
572 return nullptr;
573 }
574 if (class_items.has(p_match.doc->name)) {
575 return class_items[p_match.doc->name];
576 }
577
578 // Ensure parent nodes are created first.
579 TreeItem *parent_item = root_item;
580 if (!p_match.doc->inherits.is_empty()) {
581 if (class_items.has(p_match.doc->inherits)) {
582 parent_item = class_items[p_match.doc->inherits];
583 } else {
584 ClassMatch &base_match = matches[p_match.doc->inherits];
585 if (base_match.doc) {
586 parent_item = _create_class_hierarchy(base_match);
587 }
588 }
589 }
590
591 TreeItem *class_item = _create_class_item(parent_item, p_match.doc, !p_match.name);
592 class_items[p_match.doc->name] = class_item;
593 return class_item;
594}
595
596TreeItem *EditorHelpSearch::Runner::_create_class_item(TreeItem *p_parent, const DocData::ClassDoc *p_doc, bool p_gray) {
597 String tooltip = DTR(p_doc->brief_description.strip_edges());
598
599 TreeItem *item = results_tree->create_item(p_parent);
600 item->set_icon(0, EditorNode::get_singleton()->get_class_icon(p_doc->name));
601 item->set_text(0, p_doc->name);
602 item->set_text(1, TTR("Class"));
603 item->set_tooltip_text(0, tooltip);
604 item->set_tooltip_text(1, tooltip);
605 item->set_metadata(0, "class_name:" + p_doc->name);
606 if (p_gray) {
607 item->set_custom_color(0, disabled_color);
608 item->set_custom_color(1, disabled_color);
609 }
610
611 if (p_doc->is_deprecated) {
612 Ref<Texture2D> error_icon = ui_service->get_editor_theme_icon("StatusError");
613 item->add_button(0, error_icon, 0, false, TTR("This class is marked as deprecated."));
614 } else if (p_doc->is_experimental) {
615 Ref<Texture2D> warning_icon = ui_service->get_editor_theme_icon("NodeWarning");
616 item->add_button(0, warning_icon, 0, false, TTR("This class is marked as experimental."));
617 }
618
619 _match_item(item, p_doc->name);
620
621 return item;
622}
623
624TreeItem *EditorHelpSearch::Runner::_create_method_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const String &p_text, const DocData::MethodDoc *p_doc) {
625 String tooltip = _build_method_tooltip(p_class_doc, p_doc);
626 return _create_member_item(p_parent, p_class_doc->name, "MemberMethod", p_doc->name, p_text, TTRC("Method"), "method", tooltip, p_doc->is_deprecated, p_doc->is_experimental);
627}
628
629TreeItem *EditorHelpSearch::Runner::_create_signal_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const DocData::MethodDoc *p_doc) {
630 String tooltip = _build_method_tooltip(p_class_doc, p_doc);
631 return _create_member_item(p_parent, p_class_doc->name, "MemberSignal", p_doc->name, p_doc->name, TTRC("Signal"), "signal", tooltip, p_doc->is_deprecated, p_doc->is_experimental);
632}
633
634TreeItem *EditorHelpSearch::Runner::_create_annotation_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const String &p_text, const DocData::MethodDoc *p_doc) {
635 String tooltip = _build_method_tooltip(p_class_doc, p_doc);
636 return _create_member_item(p_parent, p_class_doc->name, "MemberAnnotation", p_doc->name, p_text, TTRC("Annotation"), "annotation", tooltip, p_doc->is_deprecated, p_doc->is_experimental);
637}
638
639TreeItem *EditorHelpSearch::Runner::_create_constant_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const DocData::ConstantDoc *p_doc) {
640 String tooltip = p_class_doc->name + "." + p_doc->name;
641 return _create_member_item(p_parent, p_class_doc->name, "MemberConstant", p_doc->name, p_doc->name, TTRC("Constant"), "constant", tooltip, p_doc->is_deprecated, p_doc->is_experimental);
642}
643
644TreeItem *EditorHelpSearch::Runner::_create_property_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const DocData::PropertyDoc *p_doc) {
645 String tooltip = p_doc->type + " " + p_class_doc->name + "." + p_doc->name;
646 tooltip += "\n " + p_class_doc->name + "." + p_doc->setter + "(value) setter";
647 tooltip += "\n " + p_class_doc->name + "." + p_doc->getter + "() getter";
648 return _create_member_item(p_parent, p_class_doc->name, "MemberProperty", p_doc->name, p_doc->name, TTRC("Property"), "property", tooltip, p_doc->is_deprecated, p_doc->is_experimental);
649}
650
651TreeItem *EditorHelpSearch::Runner::_create_theme_property_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const DocData::ThemeItemDoc *p_doc) {
652 String tooltip = p_doc->type + " " + p_class_doc->name + "." + p_doc->name;
653 return _create_member_item(p_parent, p_class_doc->name, "MemberTheme", p_doc->name, p_doc->name, TTRC("Theme Property"), "theme_item", tooltip, false, false);
654}
655
656TreeItem *EditorHelpSearch::Runner::_create_member_item(TreeItem *p_parent, const String &p_class_name, const String &p_icon, const String &p_name, const String &p_text, const String &p_type, const String &p_metatype, const String &p_tooltip, bool is_deprecated, bool is_experimental) {
657 Ref<Texture2D> icon;
658 String text;
659 if (search_flags & SEARCH_SHOW_HIERARCHY) {
660 icon = ui_service->get_editor_theme_icon(p_icon);
661 text = p_text;
662 } else {
663 icon = ui_service->get_editor_theme_icon(p_icon);
664 text = p_class_name + "." + p_text;
665 }
666
667 TreeItem *item = results_tree->create_item(p_parent);
668 item->set_icon(0, icon);
669 item->set_text(0, text);
670 item->set_text(1, TTRGET(p_type));
671 item->set_tooltip_text(0, p_tooltip);
672 item->set_tooltip_text(1, p_tooltip);
673 item->set_metadata(0, "class_" + p_metatype + ":" + p_class_name + ":" + p_name);
674
675 if (is_deprecated) {
676 Ref<Texture2D> error_icon = ui_service->get_editor_theme_icon("StatusError");
677 item->add_button(0, error_icon, 0, false, TTR("This member is marked as deprecated."));
678 } else if (is_experimental) {
679 Ref<Texture2D> warning_icon = ui_service->get_editor_theme_icon("NodeWarning");
680 item->add_button(0, warning_icon, 0, false, TTR("This member is marked as experimental."));
681 }
682
683 _match_item(item, p_name);
684
685 return item;
686}
687
688bool EditorHelpSearch::Runner::work(uint64_t slot) {
689 // Return true when the search has been completed, otherwise false.
690 const uint64_t until = OS::get_singleton()->get_ticks_usec() + slot;
691 while (!_slice()) {
692 if (OS::get_singleton()->get_ticks_usec() > until) {
693 return false;
694 }
695 }
696 return true;
697}
698
699EditorHelpSearch::Runner::Runner(Control *p_icon_service, Tree *p_results_tree, const String &p_term, int p_search_flags) :
700 ui_service(p_icon_service),
701 results_tree(p_results_tree),
702 term((p_search_flags & SEARCH_CASE_SENSITIVE) == 0 ? p_term.strip_edges().to_lower() : p_term.strip_edges()),
703 search_flags(p_search_flags),
704 disabled_color(ui_service->get_theme_color(SNAME("disabled_font_color"), EditorStringName(Editor))) {
705}
706