1/**************************************************************************/
2/* property_selector.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 "property_selector.h"
32
33#include "core/os/keyboard.h"
34#include "editor/doc_tools.h"
35#include "editor/editor_help.h"
36#include "editor/editor_node.h"
37#include "editor/editor_scale.h"
38#include "scene/gui/line_edit.h"
39#include "scene/gui/rich_text_label.h"
40#include "scene/gui/tree.h"
41
42void PropertySelector::_text_changed(const String &p_newtext) {
43 _update_search();
44}
45
46void PropertySelector::_sbox_input(const Ref<InputEvent> &p_ie) {
47 Ref<InputEventKey> k = p_ie;
48
49 if (k.is_valid()) {
50 switch (k->get_keycode()) {
51 case Key::UP:
52 case Key::DOWN:
53 case Key::PAGEUP:
54 case Key::PAGEDOWN: {
55 search_options->gui_input(k);
56 search_box->accept_event();
57
58 TreeItem *root = search_options->get_root();
59 if (!root->get_first_child()) {
60 break;
61 }
62
63 TreeItem *current = search_options->get_selected();
64
65 TreeItem *item = search_options->get_next_selected(root);
66 while (item) {
67 item->deselect(0);
68 item = search_options->get_next_selected(item);
69 }
70
71 current->select(0);
72
73 } break;
74 default:
75 break;
76 }
77 }
78}
79
80void PropertySelector::_update_search() {
81 if (properties) {
82 set_title(TTR("Select Property"));
83 } else if (virtuals_only) {
84 set_title(TTR("Select Virtual Method"));
85 } else {
86 set_title(TTR("Select Method"));
87 }
88
89 search_options->clear();
90 help_bit->set_text("");
91
92 TreeItem *root = search_options->create_item();
93
94 // Allow using spaces in place of underscores in the search string (makes the search more fault-tolerant).
95 const String search_text = search_box->get_text().replace(" ", "_");
96
97 if (properties) {
98 List<PropertyInfo> props;
99
100 if (instance) {
101 instance->get_property_list(&props, true);
102 } else if (type != Variant::NIL) {
103 Variant v;
104 Callable::CallError ce;
105 Variant::construct(type, v, nullptr, 0, ce);
106
107 v.get_property_list(&props);
108 } else {
109 Object *obj = ObjectDB::get_instance(script);
110 if (Object::cast_to<Script>(obj)) {
111 props.push_back(PropertyInfo(Variant::NIL, "Script Variables", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_CATEGORY));
112 Object::cast_to<Script>(obj)->get_script_property_list(&props);
113 }
114
115 StringName base = base_type;
116 while (base) {
117 props.push_back(PropertyInfo(Variant::NIL, base, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_CATEGORY));
118 ClassDB::get_property_list(base, &props, true);
119 base = ClassDB::get_parent_class(base);
120 }
121 }
122
123 TreeItem *category = nullptr;
124
125 bool found = false;
126
127 Ref<Texture2D> type_icons[] = {
128 search_options->get_editor_theme_icon(SNAME("Variant")),
129 search_options->get_editor_theme_icon(SNAME("bool")),
130 search_options->get_editor_theme_icon(SNAME("int")),
131 search_options->get_editor_theme_icon(SNAME("float")),
132 search_options->get_editor_theme_icon(SNAME("String")),
133 search_options->get_editor_theme_icon(SNAME("Vector2")),
134 search_options->get_editor_theme_icon(SNAME("Vector2i")),
135 search_options->get_editor_theme_icon(SNAME("Rect2")),
136 search_options->get_editor_theme_icon(SNAME("Rect2i")),
137 search_options->get_editor_theme_icon(SNAME("Vector3")),
138 search_options->get_editor_theme_icon(SNAME("Vector3i")),
139 search_options->get_editor_theme_icon(SNAME("Transform2D")),
140 search_options->get_editor_theme_icon(SNAME("Vector4")),
141 search_options->get_editor_theme_icon(SNAME("Vector4i")),
142 search_options->get_editor_theme_icon(SNAME("Plane")),
143 search_options->get_editor_theme_icon(SNAME("Quaternion")),
144 search_options->get_editor_theme_icon(SNAME("AABB")),
145 search_options->get_editor_theme_icon(SNAME("Basis")),
146 search_options->get_editor_theme_icon(SNAME("Transform3D")),
147 search_options->get_editor_theme_icon(SNAME("Projection")),
148 search_options->get_editor_theme_icon(SNAME("Color")),
149 search_options->get_editor_theme_icon(SNAME("StringName")),
150 search_options->get_editor_theme_icon(SNAME("NodePath")),
151 search_options->get_editor_theme_icon(SNAME("RID")),
152 search_options->get_editor_theme_icon(SNAME("MiniObject")),
153 search_options->get_editor_theme_icon(SNAME("Callable")),
154 search_options->get_editor_theme_icon(SNAME("Signal")),
155 search_options->get_editor_theme_icon(SNAME("Dictionary")),
156 search_options->get_editor_theme_icon(SNAME("Array")),
157 search_options->get_editor_theme_icon(SNAME("PackedByteArray")),
158 search_options->get_editor_theme_icon(SNAME("PackedInt32Array")),
159 search_options->get_editor_theme_icon(SNAME("PackedInt64Array")),
160 search_options->get_editor_theme_icon(SNAME("PackedFloat32Array")),
161 search_options->get_editor_theme_icon(SNAME("PackedFloat64Array")),
162 search_options->get_editor_theme_icon(SNAME("PackedStringArray")),
163 search_options->get_editor_theme_icon(SNAME("PackedVector2Array")),
164 search_options->get_editor_theme_icon(SNAME("PackedVector3Array")),
165 search_options->get_editor_theme_icon(SNAME("PackedColorArray"))
166 };
167 static_assert((sizeof(type_icons) / sizeof(type_icons[0])) == Variant::VARIANT_MAX, "Number of type icons doesn't match the number of Variant types.");
168
169 for (const PropertyInfo &E : props) {
170 if (E.usage == PROPERTY_USAGE_CATEGORY) {
171 if (category && category->get_first_child() == nullptr) {
172 memdelete(category); //old category was unused
173 }
174 category = search_options->create_item(root);
175 category->set_text(0, E.name);
176 category->set_selectable(0, false);
177
178 Ref<Texture2D> icon;
179 if (E.name == "Script Variables") {
180 icon = search_options->get_editor_theme_icon(SNAME("Script"));
181 } else {
182 icon = EditorNode::get_singleton()->get_class_icon(E.name);
183 }
184 category->set_icon(0, icon);
185 continue;
186 }
187
188 if (!(E.usage & PROPERTY_USAGE_EDITOR) && !(E.usage & PROPERTY_USAGE_SCRIPT_VARIABLE)) {
189 continue;
190 }
191
192 if (!search_box->get_text().is_empty() && E.name.findn(search_text) == -1) {
193 continue;
194 }
195
196 if (type_filter.size() && !type_filter.has(E.type)) {
197 continue;
198 }
199
200 TreeItem *item = search_options->create_item(category ? category : root);
201 item->set_text(0, E.name);
202 item->set_metadata(0, E.name);
203 item->set_icon(0, type_icons[E.type]);
204
205 if (!found && !search_box->get_text().is_empty() && E.name.findn(search_text) != -1) {
206 item->select(0);
207 found = true;
208 }
209
210 item->set_selectable(0, true);
211 }
212
213 if (category && category->get_first_child() == nullptr) {
214 memdelete(category); //old category was unused
215 }
216 } else {
217 List<MethodInfo> methods;
218
219 if (type != Variant::NIL) {
220 Variant v;
221 Callable::CallError ce;
222 Variant::construct(type, v, nullptr, 0, ce);
223 v.get_method_list(&methods);
224 } else {
225 Ref<Script> script_ref = Object::cast_to<Script>(ObjectDB::get_instance(script));
226 if (script_ref.is_valid()) {
227 methods.push_back(MethodInfo("*Script Methods"));
228 if (script_ref->is_built_in()) {
229 script_ref->reload(true);
230 }
231 script_ref->get_script_method_list(&methods);
232 }
233
234 StringName base = base_type;
235 while (base) {
236 methods.push_back(MethodInfo("*" + String(base)));
237 ClassDB::get_method_list(base, &methods, true, true);
238 base = ClassDB::get_parent_class(base);
239 }
240 }
241
242 TreeItem *category = nullptr;
243
244 bool found = false;
245 bool script_methods = false;
246
247 for (MethodInfo &mi : methods) {
248 if (mi.name.begins_with("*")) {
249 if (category && category->get_first_child() == nullptr) {
250 memdelete(category); //old category was unused
251 }
252 category = search_options->create_item(root);
253 category->set_text(0, mi.name.replace_first("*", ""));
254 category->set_selectable(0, false);
255
256 Ref<Texture2D> icon;
257 script_methods = false;
258 String rep = mi.name.replace("*", "");
259 if (mi.name == "*Script Methods") {
260 icon = search_options->get_editor_theme_icon(SNAME("Script"));
261 script_methods = true;
262 } else {
263 icon = EditorNode::get_singleton()->get_class_icon(rep);
264 }
265 category->set_icon(0, icon);
266
267 continue;
268 }
269
270 String name = mi.name.get_slice(":", 0);
271 if (!script_methods && name.begins_with("_") && !(mi.flags & METHOD_FLAG_VIRTUAL)) {
272 continue;
273 }
274
275 if (virtuals_only && !(mi.flags & METHOD_FLAG_VIRTUAL)) {
276 continue;
277 }
278
279 if (!virtuals_only && (mi.flags & METHOD_FLAG_VIRTUAL)) {
280 continue;
281 }
282
283 if (!search_box->get_text().is_empty() && name.findn(search_text) == -1) {
284 continue;
285 }
286
287 TreeItem *item = search_options->create_item(category ? category : root);
288
289 String desc;
290 if (mi.name.contains(":")) {
291 desc = mi.name.get_slice(":", 1) + " ";
292 mi.name = mi.name.get_slice(":", 0);
293 } else if (mi.return_val.type != Variant::NIL) {
294 desc = Variant::get_type_name(mi.return_val.type);
295 } else {
296 desc = "void";
297 }
298
299 desc += vformat(" %s(", mi.name);
300
301 for (int i = 0; i < mi.arguments.size(); i++) {
302 if (i > 0) {
303 desc += ", ";
304 }
305
306 desc += mi.arguments[i].name;
307
308 if (mi.arguments[i].type == Variant::NIL) {
309 desc += ": Variant";
310 } else if (mi.arguments[i].name.contains(":")) {
311 desc += vformat(": %s", mi.arguments[i].name.get_slice(":", 1));
312 mi.arguments[i].name = mi.arguments[i].name.get_slice(":", 0);
313 } else {
314 desc += vformat(": %s", Variant::get_type_name(mi.arguments[i].type));
315 }
316 }
317
318 desc += ")";
319
320 if (mi.flags & METHOD_FLAG_CONST) {
321 desc += " const";
322 }
323
324 if (mi.flags & METHOD_FLAG_VIRTUAL) {
325 desc += " virtual";
326 }
327
328 item->set_text(0, desc);
329 item->set_metadata(0, name);
330 item->set_selectable(0, true);
331
332 if (!found && !search_box->get_text().is_empty() && name.findn(search_text) != -1) {
333 item->select(0);
334 found = true;
335 }
336 }
337
338 if (category && category->get_first_child() == nullptr) {
339 memdelete(category); //old category was unused
340 }
341 }
342
343 get_ok_button()->set_disabled(root->get_first_child() == nullptr);
344}
345
346void PropertySelector::_confirmed() {
347 TreeItem *ti = search_options->get_selected();
348 if (!ti) {
349 return;
350 }
351 emit_signal(SNAME("selected"), ti->get_metadata(0));
352 hide();
353}
354
355void PropertySelector::_item_selected() {
356 help_bit->set_text("");
357
358 TreeItem *item = search_options->get_selected();
359 if (!item) {
360 return;
361 }
362 String name = item->get_metadata(0);
363
364 String class_type;
365 if (type != Variant::NIL) {
366 class_type = Variant::get_type_name(type);
367 } else if (!base_type.is_empty()) {
368 class_type = base_type;
369 } else if (instance) {
370 class_type = instance->get_class();
371 }
372
373 DocTools *dd = EditorHelp::get_doc_data();
374 String text;
375 if (properties) {
376 while (!class_type.is_empty()) {
377 HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_type);
378 if (E) {
379 for (int i = 0; i < E->value.properties.size(); i++) {
380 if (E->value.properties[i].name == name) {
381 text = DTR(E->value.properties[i].description);
382 break;
383 }
384 }
385 }
386
387 if (!text.is_empty()) {
388 break;
389 }
390
391 // The property may be from a parent class, keep looking.
392 class_type = ClassDB::get_parent_class(class_type);
393 }
394 } else {
395 while (!class_type.is_empty()) {
396 HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_type);
397 if (E) {
398 for (int i = 0; i < E->value.methods.size(); i++) {
399 if (E->value.methods[i].name == name) {
400 text = DTR(E->value.methods[i].description);
401 break;
402 }
403 }
404 }
405
406 if (!text.is_empty()) {
407 break;
408 }
409
410 // The method may be from a parent class, keep looking.
411 class_type = ClassDB::get_parent_class(class_type);
412 }
413 }
414
415 if (!text.is_empty()) {
416 // Display both property name and description, since the help bit may be displayed
417 // far away from the location (especially if the dialog was resized to be taller).
418 help_bit->set_text(vformat("[b]%s[/b]: %s", name, text));
419 help_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1));
420 } else {
421 // Use nested `vformat()` as translators shouldn't interfere with BBCode tags.
422 help_bit->set_text(vformat(TTR("No description available for %s."), vformat("[b]%s[/b]", name)));
423 help_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5));
424 }
425}
426
427void PropertySelector::_hide_requested() {
428 _cancel_pressed(); // From AcceptDialog.
429}
430
431void PropertySelector::_notification(int p_what) {
432 switch (p_what) {
433 case NOTIFICATION_ENTER_TREE: {
434 connect("confirmed", callable_mp(this, &PropertySelector::_confirmed));
435 } break;
436
437 case NOTIFICATION_EXIT_TREE: {
438 disconnect("confirmed", callable_mp(this, &PropertySelector::_confirmed));
439 } break;
440 }
441}
442
443void PropertySelector::select_method_from_base_type(const String &p_base, const String &p_current, bool p_virtuals_only) {
444 base_type = p_base;
445 selected = p_current;
446 type = Variant::NIL;
447 script = ObjectID();
448 properties = false;
449 instance = nullptr;
450 virtuals_only = p_virtuals_only;
451
452 popup_centered_ratio(0.6);
453 search_box->set_text("");
454 search_box->grab_focus();
455 _update_search();
456}
457
458void PropertySelector::select_method_from_script(const Ref<Script> &p_script, const String &p_current) {
459 ERR_FAIL_COND(p_script.is_null());
460 base_type = p_script->get_instance_base_type();
461 selected = p_current;
462 type = Variant::NIL;
463 script = p_script->get_instance_id();
464 properties = false;
465 instance = nullptr;
466 virtuals_only = false;
467
468 popup_centered_ratio(0.6);
469 search_box->set_text("");
470 search_box->grab_focus();
471 _update_search();
472}
473
474void PropertySelector::select_method_from_basic_type(Variant::Type p_type, const String &p_current) {
475 ERR_FAIL_COND(p_type == Variant::NIL);
476 base_type = "";
477 selected = p_current;
478 type = p_type;
479 script = ObjectID();
480 properties = false;
481 instance = nullptr;
482 virtuals_only = false;
483
484 popup_centered_ratio(0.6);
485 search_box->set_text("");
486 search_box->grab_focus();
487 _update_search();
488}
489
490void PropertySelector::select_method_from_instance(Object *p_instance, const String &p_current) {
491 base_type = p_instance->get_class();
492 selected = p_current;
493 type = Variant::NIL;
494 script = ObjectID();
495 {
496 Ref<Script> scr = p_instance->get_script();
497 if (scr.is_valid()) {
498 script = scr->get_instance_id();
499 }
500 }
501 properties = false;
502 instance = nullptr;
503 virtuals_only = false;
504
505 popup_centered_ratio(0.6);
506 search_box->set_text("");
507 search_box->grab_focus();
508 _update_search();
509}
510
511void PropertySelector::select_property_from_base_type(const String &p_base, const String &p_current) {
512 base_type = p_base;
513 selected = p_current;
514 type = Variant::NIL;
515 script = ObjectID();
516 properties = true;
517 instance = nullptr;
518 virtuals_only = false;
519
520 popup_centered_ratio(0.6);
521 search_box->set_text("");
522 search_box->grab_focus();
523 _update_search();
524}
525
526void PropertySelector::select_property_from_script(const Ref<Script> &p_script, const String &p_current) {
527 ERR_FAIL_COND(p_script.is_null());
528
529 base_type = p_script->get_instance_base_type();
530 selected = p_current;
531 type = Variant::NIL;
532 script = p_script->get_instance_id();
533 properties = true;
534 instance = nullptr;
535 virtuals_only = false;
536
537 popup_centered_ratio(0.6);
538 search_box->set_text("");
539 search_box->grab_focus();
540 _update_search();
541}
542
543void PropertySelector::select_property_from_basic_type(Variant::Type p_type, const String &p_current) {
544 ERR_FAIL_COND(p_type == Variant::NIL);
545 base_type = "";
546 selected = p_current;
547 type = p_type;
548 script = ObjectID();
549 properties = true;
550 instance = nullptr;
551 virtuals_only = false;
552
553 popup_centered_ratio(0.6);
554 search_box->set_text("");
555 search_box->grab_focus();
556 _update_search();
557}
558
559void PropertySelector::select_property_from_instance(Object *p_instance, const String &p_current) {
560 base_type = "";
561 selected = p_current;
562 type = Variant::NIL;
563 script = ObjectID();
564 properties = true;
565 instance = p_instance;
566 virtuals_only = false;
567
568 popup_centered_ratio(0.6);
569 search_box->set_text("");
570 search_box->grab_focus();
571 _update_search();
572}
573
574void PropertySelector::set_type_filter(const Vector<Variant::Type> &p_type_filter) {
575 type_filter = p_type_filter;
576}
577
578void PropertySelector::_bind_methods() {
579 ADD_SIGNAL(MethodInfo("selected", PropertyInfo(Variant::STRING, "name")));
580}
581
582PropertySelector::PropertySelector() {
583 VBoxContainer *vbc = memnew(VBoxContainer);
584 add_child(vbc);
585 //set_child_rect(vbc);
586 search_box = memnew(LineEdit);
587 vbc->add_margin_child(TTR("Search:"), search_box);
588 search_box->connect("text_changed", callable_mp(this, &PropertySelector::_text_changed));
589 search_box->connect("gui_input", callable_mp(this, &PropertySelector::_sbox_input));
590 search_options = memnew(Tree);
591 vbc->add_margin_child(TTR("Matches:"), search_options, true);
592 set_ok_button_text(TTR("Open"));
593 get_ok_button()->set_disabled(true);
594 register_text_enter(search_box);
595 set_hide_on_ok(false);
596 search_options->connect("item_activated", callable_mp(this, &PropertySelector::_confirmed));
597 search_options->connect("cell_selected", callable_mp(this, &PropertySelector::_item_selected));
598 search_options->set_hide_root(true);
599 search_options->set_hide_folding(true);
600
601 help_bit = memnew(EditorHelpBit);
602 vbc->add_margin_child(TTR("Description:"), help_bit);
603 help_bit->connect("request_hide", callable_mp(this, &PropertySelector::_hide_requested));
604}
605