Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/editor/doc/editor_help.cpp
9903 views
1
/**************************************************************************/
2
/* editor_help.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.h"
32
33
#include "core/config/project_settings.h"
34
#include "core/core_constants.h"
35
#include "core/extension/gdextension.h"
36
#include "core/input/input.h"
37
#include "core/io/json.h"
38
#include "core/object/script_language.h"
39
#include "core/os/keyboard.h"
40
#include "core/string/string_builder.h"
41
#include "core/version.h"
42
#include "editor/doc/doc_data_compressed.gen.h"
43
#include "editor/docks/filesystem_dock.h"
44
#include "editor/editor_main_screen.h"
45
#include "editor/editor_node.h"
46
#include "editor/editor_string_names.h"
47
#include "editor/file_system/editor_file_system.h"
48
#include "editor/file_system/editor_paths.h"
49
#include "editor/gui/editor_toaster.h"
50
#include "editor/inspector/editor_property_name_processor.h"
51
#include "editor/script/script_editor_plugin.h"
52
#include "editor/settings/editor_settings.h"
53
#include "editor/themes/editor_scale.h"
54
#include "scene/gui/line_edit.h"
55
56
#include "modules/modules_enabled.gen.h" // For gdscript, mono.
57
58
// For syntax highlighting.
59
#ifdef MODULE_GDSCRIPT_ENABLED
60
#include "modules/gdscript/editor/gdscript_highlighter.h"
61
#include "modules/gdscript/gdscript.h"
62
#endif
63
64
// For syntax highlighting.
65
#ifdef MODULE_MONO_ENABLED
66
#include "modules/mono/csharp_script.h"
67
#endif
68
69
#define CONTRIBUTE_URL vformat("%s/contributing/documentation/updating_the_class_reference.html", GODOT_VERSION_DOCS_URL)
70
71
#ifdef MODULE_MONO_ENABLED
72
// Sync with the types mentioned in https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_differences.html
73
const Vector<String> classes_with_csharp_differences = {
74
"@GlobalScope",
75
"String",
76
"NodePath",
77
"Signal",
78
"Callable",
79
"RID",
80
"Basis",
81
"Transform2D",
82
"Transform3D",
83
"Rect2",
84
"Rect2i",
85
"AABB",
86
"Quaternion",
87
"Projection",
88
"Color",
89
"Array",
90
"Dictionary",
91
"PackedByteArray",
92
"PackedColorArray",
93
"PackedFloat32Array",
94
"PackedFloat64Array",
95
"PackedInt32Array",
96
"PackedInt64Array",
97
"PackedStringArray",
98
"PackedVector2Array",
99
"PackedVector3Array",
100
"PackedVector4Array",
101
"Variant",
102
};
103
#endif
104
105
static const char32_t nbsp_chr = 160;
106
static const String nbsp = String::chr(nbsp_chr);
107
static const String nbsp_equal_nbsp = nbsp + "=" + nbsp;
108
static const String colon_nbsp = ":" + nbsp;
109
110
const Vector<String> packed_array_types = {
111
"PackedByteArray",
112
"PackedColorArray",
113
"PackedFloat32Array",
114
"PackedFloat64Array",
115
"PackedInt32Array",
116
"PackedInt64Array",
117
"PackedStringArray",
118
"PackedVector2Array",
119
"PackedVector3Array",
120
"PackedVector4Array",
121
};
122
123
static String _replace_nbsp_with_space(const String &p_string) {
124
return p_string.replace_char(nbsp_chr, ' ');
125
}
126
127
static String _fix_constant(const String &p_constant) {
128
if (p_constant.strip_edges() == "4294967295") {
129
return "0xFFFFFFFF";
130
}
131
132
if (p_constant.strip_edges() == "2147483647") {
133
return "0x7FFFFFFF";
134
}
135
136
if (p_constant.strip_edges() == "1048575") {
137
return "0xFFFFF";
138
}
139
140
return p_constant;
141
}
142
143
static void _add_qualifiers_to_rt(const String &p_qualifiers, RichTextLabel *p_rt) {
144
for (const String &qualifier : p_qualifiers.split_spaces()) {
145
String hint;
146
if (qualifier == "vararg") {
147
hint = TTR("This method supports a variable number of arguments.");
148
} else if (qualifier == "virtual") {
149
hint = TTR("This method is called by the engine.\nIt can be overridden to customize built-in behavior.");
150
} else if (qualifier == "required") {
151
hint = TTR("This method is required to be overridden when extending its base class.");
152
} else if (qualifier == "const") {
153
hint = TTR("This method has no side effects.\nIt does not modify the object in any way.");
154
} else if (qualifier == "static") {
155
hint = TTR("This method does not need an instance to be called.\nIt can be called directly using the class name.");
156
} else if (qualifier == "abstract") {
157
hint = TTR("This method must be implemented to complete the abstract class.");
158
}
159
160
p_rt->add_text(" ");
161
if (hint.is_empty()) {
162
p_rt->add_text(qualifier);
163
} else {
164
p_rt->push_hint(hint);
165
p_rt->add_text(qualifier);
166
p_rt->pop(); // hint
167
}
168
}
169
}
170
171
// Removes unnecessary prefix from `p_class_specifier` when within the `p_edited_class` context.
172
static String _contextualize_class_specifier(const String &p_class_specifier, const String &p_edited_class) {
173
// If this is a completely different context than the current class, then keep full path.
174
if (!p_class_specifier.begins_with(p_edited_class)) {
175
return p_class_specifier;
176
}
177
178
// Here equal `length()` and `begins_with()` from above implies `p_class_specifier == p_edited_class`.
179
if (p_class_specifier.length() == p_edited_class.length()) {
180
int rfind = p_class_specifier.rfind_char('.');
181
if (rfind == -1) { // Single identifier.
182
return p_class_specifier;
183
}
184
// Multiple specifiers: keep last one only.
185
return p_class_specifier.substr(rfind + 1);
186
}
187
188
// They share a _name_ prefix but not a _class specifier_ prefix, e.g. `Tree` and `TreeItem`.
189
// `begins_with()` and `length()`s being different implies `p_class_specifier.length() > p_edited_class.length()` so this is safe.
190
if (p_class_specifier[p_edited_class.length()] != '.') {
191
return p_class_specifier;
192
}
193
194
// Remove class specifier prefix.
195
return p_class_specifier.substr(p_edited_class.length() + 1);
196
}
197
198
/// EditorHelp ///
199
200
void EditorHelp::_update_theme_item_cache() {
201
VBoxContainer::_update_theme_item_cache();
202
203
theme_cache.text_color = get_theme_color(SNAME("text_color"), SNAME("EditorHelp"));
204
theme_cache.title_color = get_theme_color(SNAME("title_color"), SNAME("EditorHelp"));
205
theme_cache.headline_color = get_theme_color(SNAME("headline_color"), SNAME("EditorHelp"));
206
theme_cache.comment_color = get_theme_color(SNAME("comment_color"), SNAME("EditorHelp"));
207
theme_cache.symbol_color = get_theme_color(SNAME("symbol_color"), SNAME("EditorHelp"));
208
theme_cache.value_color = get_theme_color(SNAME("value_color"), SNAME("EditorHelp"));
209
theme_cache.qualifier_color = get_theme_color(SNAME("qualifier_color"), SNAME("EditorHelp"));
210
theme_cache.type_color = get_theme_color(SNAME("type_color"), SNAME("EditorHelp"));
211
theme_cache.override_color = get_theme_color(SNAME("override_color"), SNAME("EditorHelp"));
212
213
theme_cache.doc_font = get_theme_font(SNAME("doc"), EditorStringName(EditorFonts));
214
theme_cache.doc_bold_font = get_theme_font(SNAME("doc_bold"), EditorStringName(EditorFonts));
215
theme_cache.doc_italic_font = get_theme_font(SNAME("doc_italic"), EditorStringName(EditorFonts));
216
theme_cache.doc_title_font = get_theme_font(SNAME("doc_title"), EditorStringName(EditorFonts));
217
theme_cache.doc_code_font = get_theme_font(SNAME("doc_source"), EditorStringName(EditorFonts));
218
theme_cache.doc_kbd_font = get_theme_font(SNAME("doc_keyboard"), EditorStringName(EditorFonts));
219
220
theme_cache.doc_font_size = get_theme_font_size(SNAME("doc_size"), EditorStringName(EditorFonts));
221
theme_cache.doc_title_font_size = get_theme_font_size(SNAME("doc_title_size"), EditorStringName(EditorFonts));
222
theme_cache.doc_code_font_size = get_theme_font_size(SNAME("doc_source_size"), EditorStringName(EditorFonts));
223
theme_cache.doc_kbd_font_size = get_theme_font_size(SNAME("doc_keyboard_size"), EditorStringName(EditorFonts));
224
225
theme_cache.background_style = get_theme_stylebox(SNAME("background"), SNAME("EditorHelp"));
226
227
class_desc->begin_bulk_theme_override();
228
class_desc->add_theme_font_override("normal_font", theme_cache.doc_font);
229
class_desc->add_theme_font_size_override("normal_font_size", theme_cache.doc_font_size);
230
231
class_desc->add_theme_constant_override(SceneStringName(line_separation), get_theme_constant(SceneStringName(line_separation), SNAME("EditorHelp")));
232
class_desc->add_theme_constant_override("table_h_separation", get_theme_constant(SNAME("table_h_separation"), SNAME("EditorHelp")));
233
class_desc->add_theme_constant_override("table_v_separation", get_theme_constant(SNAME("table_v_separation"), SNAME("EditorHelp")));
234
class_desc->add_theme_constant_override("text_highlight_h_padding", get_theme_constant(SNAME("text_highlight_h_padding"), SNAME("EditorHelp")));
235
class_desc->add_theme_constant_override("text_highlight_v_padding", get_theme_constant(SNAME("text_highlight_v_padding"), SNAME("EditorHelp")));
236
class_desc->end_bulk_theme_override();
237
}
238
239
void EditorHelp::_search(bool p_search_previous) {
240
if (p_search_previous) {
241
find_bar->search_prev();
242
} else {
243
find_bar->search_next();
244
}
245
}
246
247
void EditorHelp::_class_desc_finished() {
248
if (scroll_to >= 0) {
249
class_desc->connect(SceneStringName(draw), callable_mp(class_desc, &RichTextLabel::scroll_to_paragraph).bind(scroll_to), CONNECT_ONE_SHOT | CONNECT_DEFERRED);
250
}
251
scroll_to = -1;
252
}
253
254
void EditorHelp::_class_list_select(const String &p_select) {
255
_goto_desc(p_select);
256
}
257
258
void EditorHelp::_class_desc_select(const String &p_select) {
259
if (p_select.begins_with("$")) { // Enum.
260
const String link = p_select.substr(1);
261
262
String enum_class_name;
263
String enum_name;
264
if (CoreConstants::is_global_enum(link)) {
265
enum_class_name = "@GlobalScope";
266
enum_name = link;
267
} else {
268
const int dot_pos = link.rfind_char('.');
269
if (dot_pos >= 0) {
270
enum_class_name = link.left(dot_pos);
271
enum_name = link.substr(dot_pos + 1);
272
} else {
273
enum_class_name = edited_class;
274
enum_name = link;
275
}
276
}
277
278
emit_signal(SNAME("go_to_help"), "class_enum:" + enum_class_name + ":" + enum_name);
279
} else if (p_select.begins_with("#")) { // Class.
280
emit_signal(SNAME("go_to_help"), "class_name:" + p_select.substr(1));
281
} else if (p_select.begins_with("@")) { // Member.
282
const int tag_end = p_select.find_char(' ');
283
const String tag = p_select.substr(1, tag_end - 1);
284
const String link = p_select.substr(tag_end + 1).lstrip(" ");
285
286
String topic;
287
const HashMap<String, int> *table = nullptr;
288
289
if (tag == "method") {
290
topic = "class_method";
291
table = &method_line;
292
} else if (tag == "constructor") {
293
topic = "class_method";
294
table = &method_line;
295
} else if (tag == "operator") {
296
topic = "class_method";
297
table = &method_line;
298
} else if (tag == "member") {
299
topic = "class_property";
300
table = &property_line;
301
} else if (tag == "enum") {
302
topic = "class_enum";
303
table = &enum_line;
304
} else if (tag == "signal") {
305
topic = "class_signal";
306
table = &signal_line;
307
} else if (tag == "constant") {
308
topic = "class_constant";
309
table = &constant_line;
310
} else if (tag == "annotation") {
311
topic = "class_annotation";
312
table = &annotation_line;
313
} else if (tag == "theme_item") {
314
topic = "class_theme_item";
315
table = &theme_property_line;
316
} else {
317
return;
318
}
319
320
// Case order is important here to correctly handle edge cases like `Variant.Type` in `@GlobalScope`.
321
if (table->has(link)) {
322
// Found in the current page.
323
if (class_desc->is_finished()) {
324
emit_signal(SNAME("request_save_history"));
325
class_desc->scroll_to_paragraph((*table)[link]);
326
} else {
327
scroll_to = (*table)[link];
328
}
329
} else {
330
// Look for link in `@GlobalScope`.
331
if (topic == "class_enum") {
332
const DocData::ClassDoc &cd = doc->class_list["@GlobalScope"];
333
const String enum_link = link.trim_prefix("@GlobalScope.");
334
335
for (const DocData::ConstantDoc &constant : cd.constants) {
336
if (constant.enumeration == enum_link) {
337
// Found in `@GlobalScope`.
338
emit_signal(SNAME("go_to_help"), topic + ":@GlobalScope:" + enum_link);
339
return;
340
}
341
}
342
} else if (topic == "class_constant") {
343
const DocData::ClassDoc &cd = doc->class_list["@GlobalScope"];
344
345
for (const DocData::ConstantDoc &constant : cd.constants) {
346
if (constant.name == link) {
347
// Found in `@GlobalScope`.
348
emit_signal(SNAME("go_to_help"), topic + ":@GlobalScope:" + link);
349
return;
350
}
351
}
352
}
353
354
if (link.contains_char('.')) {
355
const int class_end = link.rfind_char('.');
356
emit_signal(SNAME("go_to_help"), topic + ":" + link.left(class_end) + ":" + link.substr(class_end + 1));
357
}
358
}
359
} else if (p_select.begins_with("http:") || p_select.begins_with("https:")) {
360
OS::get_singleton()->shell_open(p_select);
361
} else if (p_select.begins_with("^")) { // Copy button.
362
DisplayServer::get_singleton()->clipboard_set(p_select.substr(1));
363
EditorToaster::get_singleton()->popup_str(TTR("Code snippet copied to clipboard."), EditorToaster::SEVERITY_INFO);
364
}
365
}
366
367
void EditorHelp::_class_desc_input(const Ref<InputEvent> &p_input) {
368
}
369
370
void EditorHelp::_class_desc_resized(bool p_force_update_theme) {
371
// Add extra horizontal margins for better readability.
372
// The margins increase as the width of the editor help container increases.
373
real_t char_width = theme_cache.doc_code_font->get_char_size('x', theme_cache.doc_code_font_size).width;
374
const int new_display_margin = MAX(30 * EDSCALE, get_parent_anchorable_rect().size.width - char_width * 120 * EDSCALE) * 0.5;
375
if (display_margin != new_display_margin || p_force_update_theme) {
376
display_margin = new_display_margin;
377
378
Ref<StyleBox> class_desc_stylebox = theme_cache.background_style->duplicate();
379
class_desc_stylebox->set_content_margin(SIDE_LEFT, display_margin);
380
class_desc_stylebox->set_content_margin(SIDE_RIGHT, display_margin);
381
class_desc->add_theme_style_override(CoreStringName(normal), class_desc_stylebox);
382
class_desc->add_theme_style_override("focused", class_desc_stylebox);
383
}
384
}
385
386
static void _add_type_to_rt(const String &p_type, const String &p_enum, bool p_is_bitfield, RichTextLabel *p_rt, const Control *p_owner_node, const String &p_class) {
387
const Color type_color = p_owner_node->get_theme_color(SNAME("type_color"), SNAME("EditorHelp"));
388
389
if (p_type.is_empty() || p_type == "void") {
390
p_rt->push_color(Color(type_color, 0.5));
391
p_rt->push_hint(TTR("No return value."));
392
p_rt->add_text("void");
393
p_rt->pop(); // hint
394
p_rt->pop(); // color
395
return;
396
}
397
398
bool is_enum_type = !p_enum.is_empty();
399
bool is_bitfield = p_is_bitfield && is_enum_type;
400
bool can_ref = !p_type.contains_char('*') || is_enum_type;
401
402
String link_t = p_type; // For links in metadata
403
String display_t; // For display purposes.
404
if (is_enum_type) {
405
link_t = p_enum; // The link for enums is always the full enum description
406
display_t = _contextualize_class_specifier(p_enum, p_class);
407
} else {
408
display_t = _contextualize_class_specifier(p_type, p_class);
409
}
410
411
p_rt->push_color(type_color);
412
bool add_typed_container = false;
413
if (can_ref) {
414
if (link_t.ends_with("[]")) {
415
add_typed_container = true;
416
link_t = link_t.trim_suffix("[]");
417
display_t = display_t.trim_suffix("[]");
418
419
p_rt->push_meta("#Array", RichTextLabel::META_UNDERLINE_ON_HOVER); // class
420
p_rt->add_text("Array");
421
p_rt->pop(); // meta
422
p_rt->add_text("[");
423
} else if (link_t.begins_with("Dictionary[")) {
424
add_typed_container = true;
425
link_t = link_t.trim_prefix("Dictionary[").trim_suffix("]");
426
display_t = display_t.trim_prefix("Dictionary[").trim_suffix("]");
427
428
p_rt->push_meta("#Dictionary", RichTextLabel::META_UNDERLINE_ON_HOVER); // class
429
p_rt->add_text("Dictionary");
430
p_rt->pop(); // meta
431
p_rt->add_text("[");
432
p_rt->push_meta("#" + link_t.get_slice(", ", 0), RichTextLabel::META_UNDERLINE_ON_HOVER); // class
433
p_rt->add_text(_contextualize_class_specifier(display_t.get_slice(", ", 0), p_class));
434
p_rt->pop(); // meta
435
p_rt->add_text(", ");
436
437
link_t = link_t.get_slice(", ", 1);
438
display_t = _contextualize_class_specifier(display_t.get_slice(", ", 1), p_class);
439
} else if (is_bitfield) {
440
p_rt->push_color(Color(type_color, 0.5));
441
p_rt->push_hint(TTR("This value is an integer composed as a bitmask of the following flags."));
442
p_rt->add_text("BitField");
443
p_rt->pop(); // hint
444
p_rt->add_text("[");
445
p_rt->pop(); // color
446
}
447
448
if (is_enum_type) {
449
p_rt->push_meta("$" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // enum
450
} else {
451
p_rt->push_meta("#" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // class
452
}
453
}
454
p_rt->add_text(display_t);
455
if (can_ref) {
456
p_rt->pop(); // meta
457
if (add_typed_container) {
458
p_rt->add_text("]");
459
} else if (is_bitfield) {
460
p_rt->push_color(Color(type_color, 0.5));
461
p_rt->add_text("]");
462
p_rt->pop(); // color
463
}
464
}
465
p_rt->pop(); // color
466
}
467
468
void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is_bitfield) {
469
_add_type_to_rt(p_type, p_enum, p_is_bitfield, class_desc, this, edited_class);
470
}
471
472
void EditorHelp::_add_type_icon(const String &p_type, int p_size, const String &p_fallback) {
473
Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(p_type, p_fallback);
474
Vector2i size = Vector2i(icon->get_width(), icon->get_height());
475
if (p_size > 0) {
476
// Ensures icon scales proportionally on both axes, based on icon height.
477
float ratio = p_size / float(size.height);
478
size.width *= ratio;
479
size.height *= ratio;
480
}
481
482
class_desc->add_image(icon, size.width, size.height);
483
}
484
485
// Macros for assigning the deprecated/experimental marks to class members in overview.
486
487
#define DEPRECATED_DOC_TAG \
488
class_desc->push_font(theme_cache.doc_bold_font); \
489
class_desc->push_color(get_theme_color(SNAME("error_color"), EditorStringName(Editor))); \
490
Ref<Texture2D> error_icon = get_editor_theme_icon(SNAME("StatusError")); \
491
class_desc->add_image(error_icon, error_icon->get_width(), error_icon->get_height()); \
492
class_desc->add_text(String::chr(160) + TTR("Deprecated")); \
493
class_desc->pop(); \
494
class_desc->pop();
495
496
#define EXPERIMENTAL_DOC_TAG \
497
class_desc->push_font(theme_cache.doc_bold_font); \
498
class_desc->push_color(get_theme_color(SNAME("warning_color"), EditorStringName(Editor))); \
499
Ref<Texture2D> warning_icon = get_editor_theme_icon(SNAME("NodeWarning")); \
500
class_desc->add_image(warning_icon, warning_icon->get_width(), warning_icon->get_height()); \
501
class_desc->add_text(String::chr(160) + TTR("Experimental")); \
502
class_desc->pop(); \
503
class_desc->pop();
504
505
// Macros for displaying the deprecated/experimental info in class member descriptions.
506
507
#define DEPRECATED_DOC_MSG(m_message, m_default_message) \
508
Ref<Texture2D> error_icon = get_editor_theme_icon(SNAME("StatusError")); \
509
class_desc->add_image(error_icon, error_icon->get_width(), error_icon->get_height()); \
510
class_desc->add_text(nbsp); \
511
class_desc->push_color(get_theme_color(SNAME("error_color"), EditorStringName(Editor))); \
512
class_desc->push_font(theme_cache.doc_bold_font); \
513
class_desc->add_text(TTR("Deprecated:")); \
514
class_desc->pop(); \
515
class_desc->pop(); \
516
class_desc->add_text(" "); \
517
if ((m_message).is_empty()) { \
518
class_desc->add_text(m_default_message); \
519
} else { \
520
_add_text(m_message); \
521
}
522
523
#define EXPERIMENTAL_DOC_MSG(m_message, m_default_message) \
524
Ref<Texture2D> warning_icon = get_editor_theme_icon(SNAME("NodeWarning")); \
525
class_desc->add_image(warning_icon, warning_icon->get_width(), warning_icon->get_height()); \
526
class_desc->add_text(nbsp); \
527
class_desc->push_color(get_theme_color(SNAME("warning_color"), EditorStringName(Editor))); \
528
class_desc->push_font(theme_cache.doc_bold_font); \
529
class_desc->add_text(TTR("Experimental:")); \
530
class_desc->pop(); \
531
class_desc->pop(); \
532
class_desc->add_text(" "); \
533
if ((m_message).is_empty()) { \
534
class_desc->add_text(m_default_message); \
535
} else { \
536
_add_text(m_message); \
537
}
538
539
void EditorHelp::_add_method(const DocData::MethodDoc &p_method, bool p_overview, bool p_override) {
540
if (p_override) {
541
method_line[p_method.name] = class_desc->get_paragraph_count() - 2; // Gets overridden if description.
542
}
543
544
const bool is_vararg = p_method.qualifiers.contains("vararg");
545
546
if (p_overview) {
547
class_desc->push_cell();
548
class_desc->push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT, Control::TEXT_DIRECTION_AUTO, "");
549
} else {
550
_add_bulletpoint();
551
}
552
553
_add_type(p_method.return_type, p_method.return_enum, p_method.return_is_bitfield);
554
555
if (p_overview) {
556
class_desc->pop(); // paragraph
557
class_desc->pop(); // cell
558
class_desc->push_cell();
559
} else {
560
class_desc->add_text(" ");
561
}
562
563
const bool is_documented = p_method.is_deprecated || p_method.is_experimental || !p_method.description.strip_edges().is_empty();
564
565
if (p_overview && is_documented) {
566
class_desc->push_meta("@method " + p_method.name, RichTextLabel::META_UNDERLINE_ON_HOVER);
567
}
568
569
class_desc->push_color(theme_cache.headline_color);
570
class_desc->add_text(p_method.name);
571
class_desc->pop(); // color
572
573
if (p_overview && is_documented) {
574
class_desc->pop(); // meta
575
}
576
577
class_desc->push_color(theme_cache.symbol_color);
578
class_desc->add_text("(");
579
class_desc->pop(); // color
580
581
for (int j = 0; j < p_method.arguments.size(); j++) {
582
const DocData::ArgumentDoc &argument = p_method.arguments[j];
583
584
if (j > 0) {
585
class_desc->push_color(theme_cache.symbol_color);
586
class_desc->add_text(", ");
587
class_desc->pop(); // color
588
}
589
590
class_desc->push_color(theme_cache.text_color);
591
class_desc->add_text(argument.name);
592
class_desc->pop(); // color
593
594
class_desc->push_color(theme_cache.symbol_color);
595
class_desc->add_text(colon_nbsp);
596
class_desc->pop(); // color
597
598
_add_type(argument.type, argument.enumeration, argument.is_bitfield);
599
600
if (!argument.default_value.is_empty()) {
601
class_desc->push_color(theme_cache.symbol_color);
602
class_desc->add_text(nbsp_equal_nbsp);
603
class_desc->pop(); // color
604
605
class_desc->push_color(theme_cache.value_color);
606
class_desc->add_text(_fix_constant(argument.default_value));
607
class_desc->pop(); // color
608
}
609
}
610
611
if (is_vararg) {
612
if (!p_method.arguments.is_empty()) {
613
class_desc->push_color(theme_cache.symbol_color);
614
class_desc->add_text(", ");
615
class_desc->pop(); // color
616
}
617
618
class_desc->push_color(theme_cache.symbol_color);
619
class_desc->add_text("...");
620
class_desc->pop(); // color
621
622
const DocData::ArgumentDoc &rest_argument = p_method.rest_argument;
623
624
class_desc->push_color(theme_cache.text_color);
625
class_desc->add_text(rest_argument.name.is_empty() ? "args" : rest_argument.name);
626
class_desc->pop(); // color
627
628
class_desc->push_color(theme_cache.symbol_color);
629
class_desc->add_text(colon_nbsp);
630
class_desc->pop(); // color
631
632
if (rest_argument.type.is_empty()) {
633
_add_type("Array");
634
} else {
635
_add_type(rest_argument.type, rest_argument.enumeration, rest_argument.is_bitfield);
636
}
637
}
638
639
class_desc->push_color(theme_cache.symbol_color);
640
class_desc->add_text(")");
641
class_desc->pop(); // color
642
643
if (!p_method.qualifiers.is_empty()) {
644
class_desc->push_color(theme_cache.qualifier_color);
645
_add_qualifiers_to_rt(p_method.qualifiers, class_desc);
646
class_desc->pop(); // color
647
}
648
649
if (p_overview) {
650
if (p_method.is_deprecated) {
651
class_desc->add_text(" ");
652
DEPRECATED_DOC_TAG;
653
}
654
655
if (p_method.is_experimental) {
656
class_desc->add_text(" ");
657
EXPERIMENTAL_DOC_TAG;
658
}
659
660
class_desc->pop(); // cell
661
}
662
}
663
664
void EditorHelp::_add_bulletpoint() {
665
static const char32_t prefix[3] = { 0x25CF /* filled circle */, ' ', 0 };
666
class_desc->add_text(String(prefix));
667
}
668
669
void EditorHelp::_push_normal_font() {
670
class_desc->push_font(theme_cache.doc_font);
671
class_desc->push_font_size(theme_cache.doc_font_size);
672
}
673
674
void EditorHelp::_pop_normal_font() {
675
class_desc->pop(); // font_size
676
class_desc->pop(); // font
677
}
678
679
void EditorHelp::_push_title_font() {
680
class_desc->push_font(theme_cache.doc_title_font);
681
class_desc->push_font_size(theme_cache.doc_title_font_size);
682
class_desc->push_color(theme_cache.title_color);
683
}
684
685
void EditorHelp::_pop_title_font() {
686
class_desc->pop(); // color
687
class_desc->pop(); // font_size
688
class_desc->pop(); // font
689
}
690
691
void EditorHelp::_push_code_font() {
692
class_desc->push_font(theme_cache.doc_code_font);
693
class_desc->push_font_size(theme_cache.doc_code_font_size);
694
}
695
696
void EditorHelp::_pop_code_font() {
697
class_desc->pop(); // font_size
698
class_desc->pop(); // font
699
}
700
701
Error EditorHelp::_goto_desc(const String &p_class) {
702
if (!doc->class_list.has(p_class)) {
703
return ERR_DOES_NOT_EXIST;
704
}
705
706
select_locked = true;
707
708
class_desc->show();
709
710
description_line = 0;
711
712
if (p_class == edited_class) {
713
return OK; // Already there.
714
}
715
716
edited_class = p_class;
717
_update_doc();
718
return OK;
719
}
720
721
void EditorHelp::_update_method_list(MethodType p_method_type, const Vector<DocData::MethodDoc> &p_methods) {
722
class_desc->add_newline();
723
class_desc->add_newline();
724
725
static const char *titles_by_type[METHOD_TYPE_MAX] = {
726
TTRC("Methods"),
727
TTRC("Constructors"),
728
TTRC("Operators"),
729
};
730
const String title = TTRGET(titles_by_type[p_method_type]);
731
732
section_line.push_back(Pair<String, int>(title, class_desc->get_paragraph_count() - 2));
733
_push_title_font();
734
class_desc->add_text(title);
735
_pop_title_font();
736
737
class_desc->add_newline();
738
class_desc->add_newline();
739
740
class_desc->push_indent(1);
741
_push_code_font();
742
class_desc->push_table(2);
743
class_desc->set_table_column_expand(1, true);
744
745
bool any_previous = false;
746
for (int pass = 0; pass < 2; pass++) {
747
Vector<DocData::MethodDoc> m;
748
749
for (const DocData::MethodDoc &method : p_methods) {
750
const String &q = method.qualifiers;
751
if ((pass == 0 && q.contains("virtual")) || (pass == 1 && !q.contains("virtual"))) {
752
m.push_back(method);
753
}
754
}
755
756
if (any_previous && !m.is_empty()) {
757
class_desc->push_cell();
758
class_desc->pop(); // cell
759
class_desc->push_cell();
760
class_desc->pop(); // cell
761
}
762
763
String group_prefix;
764
for (int i = 0; i < m.size(); i++) {
765
const String new_prefix = m[i].name.left(3);
766
bool is_new_group = false;
767
768
if (i < m.size() - 1 && new_prefix == m[i + 1].name.left(3) && new_prefix != group_prefix) {
769
is_new_group = i > 0;
770
group_prefix = new_prefix;
771
} else if (!group_prefix.is_empty() && new_prefix != group_prefix) {
772
is_new_group = true;
773
group_prefix = "";
774
}
775
776
if (is_new_group && pass == 1) {
777
class_desc->push_cell();
778
class_desc->pop(); // cell
779
class_desc->push_cell();
780
class_desc->pop(); // cell
781
}
782
783
// For constructors always point to the first one.
784
_add_method(m[i], true, (p_method_type != METHOD_TYPE_CONSTRUCTOR || i == 0));
785
}
786
787
any_previous = !m.is_empty();
788
}
789
790
class_desc->pop(); // table
791
_pop_code_font();
792
class_desc->pop(); // indent
793
}
794
795
void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc, MethodType p_method_type, const Vector<DocData::MethodDoc> &p_methods) {
796
#define HANDLE_DOC(m_string) ((p_classdoc.is_script_doc ? (m_string) : DTR(m_string)).strip_edges())
797
798
class_desc->add_newline();
799
class_desc->add_newline();
800
class_desc->add_newline();
801
802
static const char *titles_by_type[METHOD_TYPE_MAX] = {
803
TTRC("Method Descriptions"),
804
TTRC("Constructor Descriptions"),
805
TTRC("Operator Descriptions"),
806
};
807
const String title = TTRGET(titles_by_type[p_method_type]);
808
809
section_line.push_back(Pair<String, int>(title, class_desc->get_paragraph_count() - 2));
810
_push_title_font();
811
class_desc->add_text(title);
812
_pop_title_font();
813
814
String link_color_text = theme_cache.title_color.to_html(false);
815
816
for (int pass = 0; pass < 2; pass++) {
817
Vector<DocData::MethodDoc> methods_filtered;
818
819
for (int i = 0; i < p_methods.size(); i++) {
820
const String &q = p_methods[i].qualifiers;
821
if ((pass == 0 && q.contains("virtual")) || (pass == 1 && !q.contains("virtual"))) {
822
methods_filtered.push_back(p_methods[i]);
823
}
824
}
825
826
for (int i = 0; i < methods_filtered.size(); i++) {
827
const DocData::MethodDoc &method = methods_filtered[i];
828
829
class_desc->add_newline();
830
class_desc->add_newline();
831
class_desc->add_newline();
832
833
_push_code_font();
834
// For constructors always point to the first one.
835
_add_method(method, false, (p_method_type != METHOD_TYPE_CONSTRUCTOR || i == 0));
836
_pop_code_font();
837
838
class_desc->add_newline();
839
class_desc->add_newline();
840
841
class_desc->push_indent(1);
842
_push_normal_font();
843
class_desc->push_color(theme_cache.text_color);
844
845
bool has_prev_text = false;
846
847
if (method.is_deprecated) {
848
has_prev_text = true;
849
850
static const char *messages_by_type[METHOD_TYPE_MAX] = {
851
TTRC("This method may be changed or removed in future versions."),
852
TTRC("This constructor may be changed or removed in future versions."),
853
TTRC("This operator may be changed or removed in future versions."),
854
};
855
DEPRECATED_DOC_MSG(HANDLE_DOC(method.deprecated_message), TTRGET(messages_by_type[p_method_type]));
856
}
857
858
if (method.is_experimental) {
859
if (has_prev_text) {
860
class_desc->add_newline();
861
class_desc->add_newline();
862
}
863
has_prev_text = true;
864
865
static const char *messages_by_type[METHOD_TYPE_MAX] = {
866
TTRC("This method may be changed or removed in future versions."),
867
TTRC("This constructor may be changed or removed in future versions."),
868
TTRC("This operator may be changed or removed in future versions."),
869
};
870
EXPERIMENTAL_DOC_MSG(HANDLE_DOC(method.experimental_message), TTRGET(messages_by_type[p_method_type]));
871
}
872
873
if (!method.errors_returned.is_empty()) {
874
if (has_prev_text) {
875
class_desc->add_newline();
876
class_desc->add_newline();
877
}
878
has_prev_text = true;
879
880
class_desc->add_text(TTR("Error codes returned:"));
881
class_desc->add_newline();
882
class_desc->push_list(0, RichTextLabel::LIST_DOTS, false);
883
for (int j = 0; j < method.errors_returned.size(); j++) {
884
if (j > 0) {
885
class_desc->add_newline();
886
}
887
888
int val = method.errors_returned[j];
889
String text = itos(val);
890
for (int k = 0; k < CoreConstants::get_global_constant_count(); k++) {
891
if (CoreConstants::get_global_constant_value(k) == val && CoreConstants::get_global_constant_enum(k) == SNAME("Error")) {
892
text = CoreConstants::get_global_constant_name(k);
893
break;
894
}
895
}
896
897
class_desc->push_font(theme_cache.doc_bold_font);
898
class_desc->add_text(text);
899
class_desc->pop(); // font
900
}
901
class_desc->pop(); // list
902
}
903
904
const String descr = HANDLE_DOC(method.description);
905
const bool is_documented = method.is_deprecated || method.is_experimental || !descr.is_empty();
906
if (!descr.is_empty()) {
907
if (has_prev_text) {
908
class_desc->add_newline();
909
class_desc->add_newline();
910
}
911
has_prev_text = true;
912
913
_add_text(descr);
914
} else if (!is_documented) {
915
if (has_prev_text) {
916
class_desc->add_newline();
917
class_desc->add_newline();
918
}
919
has_prev_text = true;
920
921
String message;
922
if (p_classdoc.is_script_doc) {
923
static const char *messages_by_type[METHOD_TYPE_MAX] = {
924
TTRC("There is currently no description for this method."),
925
TTRC("There is currently no description for this constructor."),
926
TTRC("There is currently no description for this operator."),
927
};
928
message = TTRGET(messages_by_type[p_method_type]);
929
} else {
930
static const char *messages_by_type[METHOD_TYPE_MAX] = {
931
TTRC("There is currently no description for this method. Please help us by [color=$color][url=$url]contributing one[/url][/color]!"),
932
TTRC("There is currently no description for this constructor. Please help us by [color=$color][url=$url]contributing one[/url][/color]!"),
933
TTRC("There is currently no description for this operator. Please help us by [color=$color][url=$url]contributing one[/url][/color]!"),
934
};
935
message = TTRGET(messages_by_type[p_method_type]).replace("$url", CONTRIBUTE_URL).replace("$color", link_color_text);
936
}
937
938
class_desc->add_image(get_editor_theme_icon(SNAME("Error")));
939
class_desc->add_text(" ");
940
class_desc->push_color(theme_cache.comment_color);
941
class_desc->append_text(message);
942
class_desc->pop(); // color
943
}
944
945
class_desc->pop(); // color
946
_pop_normal_font();
947
class_desc->pop(); // indent
948
}
949
}
950
951
#undef HANDLE_DOC
952
}
953
954
void EditorHelp::_update_doc() {
955
if (!doc->class_list.has(edited_class)) {
956
return;
957
}
958
959
scroll_locked = true;
960
961
class_desc->clear();
962
method_line.clear();
963
section_line.clear();
964
section_line.push_back(Pair<String, int>(TTR("Top"), 0));
965
966
String link_color_text = theme_cache.title_color.to_html(false);
967
968
DocData::ClassDoc cd = doc->class_list[edited_class]; // Make a copy, so we can sort without worrying.
969
970
#define HANDLE_DOC(m_string) ((cd.is_script_doc ? (m_string) : DTR(m_string)).strip_edges())
971
972
// Class name
973
974
_push_title_font();
975
976
class_desc->add_text(TTR("Class:") + " ");
977
_add_type_icon(edited_class, theme_cache.doc_title_font_size, "Object");
978
class_desc->add_text(nbsp);
979
980
class_desc->push_color(theme_cache.headline_color);
981
class_desc->add_text(edited_class);
982
class_desc->pop(); // color
983
984
_pop_title_font();
985
986
if (cd.is_deprecated) {
987
class_desc->add_newline();
988
DEPRECATED_DOC_MSG(HANDLE_DOC(cd.deprecated_message), TTR("This class may be changed or removed in future versions."));
989
}
990
991
if (cd.is_experimental) {
992
class_desc->add_newline();
993
EXPERIMENTAL_DOC_MSG(HANDLE_DOC(cd.experimental_message), TTR("This class may be changed or removed in future versions."));
994
}
995
996
// Inheritance tree
997
998
// Ascendents
999
if (!cd.inherits.is_empty()) {
1000
class_desc->add_newline();
1001
1002
_push_normal_font();
1003
class_desc->push_color(theme_cache.title_color);
1004
1005
class_desc->add_text(TTR("Inherits:") + " ");
1006
1007
String inherits = cd.inherits;
1008
while (!inherits.is_empty()) {
1009
_add_type_icon(inherits, theme_cache.doc_font_size, "ArrowRight");
1010
class_desc->add_text(nbsp); // Otherwise icon borrows hyperlink from `_add_type()`.
1011
_add_type(inherits);
1012
1013
const DocData::ClassDoc *base_class_doc = doc->class_list.getptr(inherits);
1014
inherits = base_class_doc ? base_class_doc->inherits : String();
1015
1016
if (!inherits.is_empty()) {
1017
class_desc->add_text(" < ");
1018
}
1019
}
1020
1021
class_desc->pop(); // color
1022
_pop_normal_font();
1023
}
1024
1025
// Descendants
1026
if ((cd.is_script_doc || ClassDB::class_exists(cd.name)) && doc->inheriting.has(cd.name)) {
1027
class_desc->add_newline();
1028
1029
_push_normal_font();
1030
class_desc->push_color(theme_cache.title_color);
1031
class_desc->add_text(TTR("Inherited by:") + " ");
1032
1033
for (RBSet<String, NaturalNoCaseComparator>::Element *itr = doc->inheriting[cd.name].front(); itr; itr = itr->next()) {
1034
if (itr->prev()) {
1035
class_desc->add_text(" , ");
1036
}
1037
1038
_add_type_icon(itr->get(), theme_cache.doc_font_size, "ArrowRight");
1039
class_desc->add_text(nbsp); // Otherwise icon borrows hyperlink from `_add_type()`.
1040
_add_type(itr->get());
1041
}
1042
1043
class_desc->pop(); // color
1044
_pop_normal_font();
1045
}
1046
1047
bool has_description = false;
1048
1049
// Brief description
1050
const String brief_class_descr = HANDLE_DOC(cd.brief_description);
1051
if (!brief_class_descr.is_empty()) {
1052
has_description = true;
1053
1054
class_desc->add_newline();
1055
class_desc->add_newline();
1056
1057
class_desc->push_indent(1);
1058
class_desc->push_font(theme_cache.doc_bold_font);
1059
class_desc->push_color(theme_cache.text_color);
1060
1061
_add_text(brief_class_descr);
1062
1063
class_desc->pop(); // color
1064
class_desc->pop(); // font
1065
class_desc->pop(); // indent
1066
}
1067
1068
// Class description
1069
const String class_descr = HANDLE_DOC(cd.description);
1070
if (!class_descr.is_empty()) {
1071
has_description = true;
1072
1073
class_desc->add_newline();
1074
class_desc->add_newline();
1075
1076
section_line.push_back(Pair<String, int>(TTR("Description"), class_desc->get_paragraph_count() - 2));
1077
description_line = class_desc->get_paragraph_count() - 2;
1078
_push_title_font();
1079
class_desc->add_text(TTR("Description"));
1080
_pop_title_font();
1081
1082
class_desc->add_newline();
1083
class_desc->add_newline();
1084
1085
class_desc->push_indent(1);
1086
_push_normal_font();
1087
class_desc->push_color(theme_cache.text_color);
1088
1089
_add_text(class_descr);
1090
1091
class_desc->pop(); // color
1092
_pop_normal_font();
1093
class_desc->pop(); // indent
1094
}
1095
1096
if (!has_description) {
1097
class_desc->add_newline();
1098
class_desc->add_newline();
1099
1100
class_desc->push_indent(1);
1101
_push_normal_font();
1102
1103
class_desc->add_image(get_editor_theme_icon(SNAME("Error")));
1104
class_desc->add_text(" ");
1105
1106
class_desc->push_color(theme_cache.comment_color);
1107
if (cd.is_script_doc) {
1108
class_desc->add_text(TTR("There is currently no description for this class."));
1109
} else {
1110
class_desc->append_text(TTR("There is currently no description for this class. Please help us by [color=$color][url=$url]contributing one[/url][/color]!").replace("$url", CONTRIBUTE_URL).replace("$color", link_color_text));
1111
}
1112
class_desc->pop(); // color
1113
1114
_pop_normal_font();
1115
class_desc->pop(); // indent
1116
}
1117
1118
#ifdef MODULE_MONO_ENABLED
1119
if (classes_with_csharp_differences.has(cd.name)) {
1120
class_desc->add_newline();
1121
class_desc->add_newline();
1122
1123
const String &csharp_differences_url = vformat("%s/tutorials/scripting/c_sharp/c_sharp_differences.html", GODOT_VERSION_DOCS_URL);
1124
1125
class_desc->push_indent(1);
1126
_push_normal_font();
1127
class_desc->push_color(theme_cache.text_color);
1128
1129
class_desc->append_text("[b]" + TTR("Note:") + "[/b] " + vformat(TTR("There are notable differences when using this API with C#. See [url=%s]C# API differences to GDScript[/url] for more information."), csharp_differences_url));
1130
1131
class_desc->pop(); // color
1132
_pop_normal_font();
1133
class_desc->pop(); // indent
1134
}
1135
#endif
1136
1137
// Online tutorials
1138
if (!cd.tutorials.is_empty()) {
1139
class_desc->add_newline();
1140
class_desc->add_newline();
1141
1142
_push_title_font();
1143
class_desc->add_text(TTR("Online Tutorials"));
1144
_pop_title_font();
1145
1146
class_desc->add_newline();
1147
1148
class_desc->push_indent(1);
1149
_push_code_font();
1150
class_desc->push_color(theme_cache.symbol_color);
1151
1152
for (const DocData::TutorialDoc &tutorial : cd.tutorials) {
1153
const String link = HANDLE_DOC(tutorial.link);
1154
1155
String link_text = HANDLE_DOC(tutorial.title);
1156
if (link_text.is_empty()) {
1157
const int sep_pos = link.find("//");
1158
if (sep_pos >= 0) {
1159
link_text = link.substr(sep_pos + 2);
1160
} else {
1161
link_text = link;
1162
}
1163
}
1164
1165
class_desc->add_newline();
1166
_add_bulletpoint();
1167
class_desc->append_text("[url=" + link + "]" + link_text + "[/url]");
1168
}
1169
1170
class_desc->pop(); // color
1171
_pop_code_font();
1172
class_desc->pop(); // indent
1173
}
1174
1175
// Properties overview
1176
HashSet<String> skip_methods;
1177
1178
bool has_properties = false;
1179
bool has_property_descriptions = false;
1180
for (const DocData::PropertyDoc &prop : cd.properties) {
1181
const bool is_documented = prop.is_deprecated || prop.is_experimental || !prop.description.strip_edges().is_empty();
1182
if (!is_documented && prop.name.begins_with("_")) {
1183
continue;
1184
}
1185
has_properties = true;
1186
if (!prop.overridden) {
1187
has_property_descriptions = true;
1188
break;
1189
}
1190
}
1191
1192
if (has_properties) {
1193
class_desc->add_newline();
1194
class_desc->add_newline();
1195
1196
section_line.push_back(Pair<String, int>(TTR("Properties"), class_desc->get_paragraph_count() - 2));
1197
_push_title_font();
1198
class_desc->add_text(TTR("Properties"));
1199
_pop_title_font();
1200
1201
class_desc->add_newline();
1202
class_desc->add_newline();
1203
1204
class_desc->push_indent(1);
1205
_push_code_font();
1206
class_desc->push_table(4);
1207
class_desc->set_table_column_expand(1, true);
1208
1209
cd.properties.sort_custom<PropertyCompare>();
1210
1211
bool is_generating_overridden_properties = true; // Set to false as soon as we encounter a non-overridden property.
1212
bool overridden_property_exists = false;
1213
1214
for (const DocData::PropertyDoc &prop : cd.properties) {
1215
// Ignore undocumented private.
1216
const bool is_documented = prop.is_deprecated || prop.is_experimental || !prop.description.strip_edges().is_empty();
1217
if (!is_documented && prop.name.begins_with("_")) {
1218
continue;
1219
}
1220
1221
if (is_generating_overridden_properties && !prop.overridden) {
1222
is_generating_overridden_properties = false;
1223
// No need for the extra spacing when there's no overridden property.
1224
if (overridden_property_exists) {
1225
class_desc->push_cell();
1226
class_desc->pop(); // cell
1227
class_desc->push_cell();
1228
class_desc->pop(); // cell
1229
class_desc->push_cell();
1230
class_desc->pop(); // cell
1231
class_desc->push_cell();
1232
class_desc->pop(); // cell
1233
}
1234
}
1235
1236
property_line[prop.name] = class_desc->get_paragraph_count() - 2; // Gets overridden if description.
1237
1238
// Property type.
1239
class_desc->push_cell();
1240
class_desc->push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT, Control::TEXT_DIRECTION_AUTO, "");
1241
_add_type(prop.type, prop.enumeration, prop.is_bitfield);
1242
class_desc->pop(); // paragraph
1243
class_desc->pop(); // cell
1244
1245
bool describe = false;
1246
1247
if (!prop.setter.is_empty()) {
1248
skip_methods.insert(prop.setter);
1249
describe = true;
1250
}
1251
if (!prop.getter.is_empty()) {
1252
skip_methods.insert(prop.getter);
1253
describe = true;
1254
}
1255
1256
if (is_documented) {
1257
describe = true;
1258
}
1259
1260
if (prop.overridden) {
1261
describe = false;
1262
}
1263
1264
// Property name.
1265
class_desc->push_cell();
1266
class_desc->push_color(theme_cache.headline_color);
1267
1268
if (describe) {
1269
class_desc->push_meta("@member " + prop.name, RichTextLabel::META_UNDERLINE_ON_HOVER);
1270
}
1271
1272
class_desc->add_text(prop.name);
1273
1274
if (describe) {
1275
class_desc->pop(); // meta
1276
}
1277
1278
class_desc->pop(); // color
1279
class_desc->pop(); // cell
1280
1281
// Property value.
1282
class_desc->push_cell();
1283
1284
if (!prop.default_value.is_empty()) {
1285
if (prop.overridden) {
1286
class_desc->push_color(theme_cache.override_color);
1287
class_desc->add_text("[");
1288
const String link = vformat("[url=@member %s.%s]%s[/url]", prop.overrides, prop.name, prop.overrides);
1289
class_desc->append_text(vformat(TTR("overrides %s:"), link));
1290
class_desc->add_text(" " + _fix_constant(prop.default_value) + "]");
1291
class_desc->pop(); // color
1292
overridden_property_exists = true;
1293
} else {
1294
class_desc->push_color(theme_cache.symbol_color);
1295
class_desc->add_text("[" + TTR("default:") + " ");
1296
class_desc->pop(); // color
1297
1298
class_desc->push_color(theme_cache.value_color);
1299
class_desc->add_text(_fix_constant(prop.default_value));
1300
class_desc->pop(); // color
1301
1302
class_desc->push_color(theme_cache.symbol_color);
1303
class_desc->add_text("]");
1304
class_desc->pop(); // color
1305
}
1306
}
1307
1308
class_desc->pop(); // cell
1309
1310
// Property setter/getter and deprecated/experimental marks.
1311
class_desc->push_cell();
1312
1313
bool has_prev_text = false;
1314
1315
if (cd.is_script_doc && (!prop.setter.is_empty() || !prop.getter.is_empty())) {
1316
has_prev_text = true;
1317
1318
class_desc->push_color(theme_cache.symbol_color);
1319
class_desc->add_text("[" + TTR("property:") + " ");
1320
class_desc->pop(); // color
1321
1322
if (!prop.setter.is_empty()) {
1323
class_desc->push_color(theme_cache.value_color);
1324
class_desc->add_text("setter");
1325
class_desc->pop(); // color
1326
}
1327
if (!prop.getter.is_empty()) {
1328
if (!prop.setter.is_empty()) {
1329
class_desc->push_color(theme_cache.symbol_color);
1330
class_desc->add_text(", ");
1331
class_desc->pop(); // color
1332
}
1333
class_desc->push_color(theme_cache.value_color);
1334
class_desc->add_text("getter");
1335
class_desc->pop(); // color
1336
}
1337
1338
class_desc->push_color(theme_cache.symbol_color);
1339
class_desc->add_text("]");
1340
class_desc->pop(); // color
1341
}
1342
1343
if (prop.is_deprecated) {
1344
if (has_prev_text) {
1345
class_desc->add_text(" ");
1346
}
1347
has_prev_text = true;
1348
DEPRECATED_DOC_TAG;
1349
}
1350
1351
if (prop.is_experimental) {
1352
if (has_prev_text) {
1353
class_desc->add_text(" ");
1354
}
1355
has_prev_text = true;
1356
EXPERIMENTAL_DOC_TAG;
1357
}
1358
1359
class_desc->pop(); // cell
1360
}
1361
1362
class_desc->pop(); // table
1363
_pop_code_font();
1364
class_desc->pop(); // indent
1365
}
1366
1367
// Methods overview
1368
bool sort_methods = EDITOR_GET("text_editor/help/sort_functions_alphabetically");
1369
1370
Vector<DocData::MethodDoc> methods;
1371
1372
for (const DocData::MethodDoc &method : cd.methods) {
1373
if (skip_methods.has(method.name)) {
1374
if (method.arguments.is_empty() /* getter */ || (method.arguments.size() == 1 && method.return_type == "void" /* setter */)) {
1375
continue;
1376
}
1377
}
1378
// Ignore undocumented non virtual private.
1379
const bool is_documented = method.is_deprecated || method.is_experimental || !method.description.strip_edges().is_empty();
1380
if (!is_documented && method.name.begins_with("_") && !method.qualifiers.contains("virtual")) {
1381
continue;
1382
}
1383
methods.push_back(method);
1384
}
1385
1386
if (!cd.constructors.is_empty()) {
1387
if (sort_methods) {
1388
cd.constructors.sort();
1389
}
1390
_update_method_list(METHOD_TYPE_CONSTRUCTOR, cd.constructors);
1391
}
1392
1393
if (!methods.is_empty()) {
1394
if (sort_methods) {
1395
methods.sort();
1396
}
1397
_update_method_list(METHOD_TYPE_METHOD, methods);
1398
}
1399
1400
if (!cd.operators.is_empty()) {
1401
if (sort_methods) {
1402
cd.operators.sort();
1403
}
1404
_update_method_list(METHOD_TYPE_OPERATOR, cd.operators);
1405
}
1406
1407
// Theme properties
1408
if (!cd.theme_properties.is_empty()) {
1409
class_desc->add_newline();
1410
class_desc->add_newline();
1411
1412
section_line.push_back(Pair<String, int>(TTR("Theme Properties"), class_desc->get_paragraph_count() - 2));
1413
_push_title_font();
1414
class_desc->add_text(TTR("Theme Properties"));
1415
_pop_title_font();
1416
1417
String theme_data_type;
1418
HashMap<String, String> data_type_names;
1419
data_type_names["color"] = TTR("Colors");
1420
data_type_names["constant"] = TTR("Constants");
1421
data_type_names["font"] = TTR("Fonts");
1422
data_type_names["font_size"] = TTR("Font Sizes");
1423
data_type_names["icon"] = TTR("Icons");
1424
data_type_names["style"] = TTR("Styles");
1425
1426
for (const DocData::ThemeItemDoc &theme_item : cd.theme_properties) {
1427
if (theme_data_type != theme_item.data_type) {
1428
theme_data_type = theme_item.data_type;
1429
1430
class_desc->add_newline();
1431
class_desc->add_newline();
1432
1433
class_desc->push_indent(1);
1434
_push_title_font();
1435
1436
if (data_type_names.has(theme_data_type)) {
1437
class_desc->add_text(data_type_names[theme_data_type]);
1438
} else {
1439
class_desc->add_text(theme_data_type);
1440
}
1441
1442
_pop_title_font();
1443
class_desc->pop(); // indent
1444
}
1445
1446
class_desc->add_newline();
1447
class_desc->add_newline();
1448
1449
theme_property_line[theme_item.name] = class_desc->get_paragraph_count() - 2; // Gets overridden if description.
1450
1451
class_desc->push_indent(1);
1452
1453
// Theme item header.
1454
_push_code_font();
1455
_add_bulletpoint();
1456
1457
// Theme item object type.
1458
_add_type(theme_item.type);
1459
1460
// Theme item name.
1461
class_desc->push_color(theme_cache.headline_color);
1462
class_desc->add_text(" ");
1463
class_desc->add_text(theme_item.name);
1464
class_desc->pop(); // color
1465
1466
// Theme item default value.
1467
if (!theme_item.default_value.is_empty()) {
1468
class_desc->push_color(theme_cache.symbol_color);
1469
class_desc->add_text(" [" + TTR("default:") + " ");
1470
class_desc->pop(); // color
1471
1472
class_desc->push_color(theme_cache.value_color);
1473
class_desc->add_text(_fix_constant(theme_item.default_value));
1474
class_desc->pop(); // color
1475
1476
class_desc->push_color(theme_cache.symbol_color);
1477
class_desc->add_text("]");
1478
class_desc->pop(); // color
1479
}
1480
1481
_pop_code_font();
1482
1483
// Theme item description.
1484
class_desc->push_indent(1);
1485
_push_normal_font();
1486
class_desc->push_color(theme_cache.comment_color);
1487
1488
bool has_prev_text = false;
1489
1490
if (theme_item.is_deprecated) {
1491
has_prev_text = true;
1492
DEPRECATED_DOC_MSG(HANDLE_DOC(theme_item.deprecated_message), TTR("This theme property may be changed or removed in future versions."));
1493
}
1494
1495
if (theme_item.is_experimental) {
1496
if (has_prev_text) {
1497
class_desc->add_newline();
1498
class_desc->add_newline();
1499
}
1500
has_prev_text = true;
1501
EXPERIMENTAL_DOC_MSG(HANDLE_DOC(theme_item.experimental_message), TTR("This theme property may be changed or removed in future versions."));
1502
}
1503
1504
const String descr = HANDLE_DOC(theme_item.description);
1505
if (!descr.is_empty()) {
1506
if (has_prev_text) {
1507
class_desc->add_newline();
1508
class_desc->add_newline();
1509
}
1510
has_prev_text = true;
1511
_add_text(descr);
1512
} else if (!has_prev_text) {
1513
class_desc->add_image(get_editor_theme_icon(SNAME("Error")));
1514
class_desc->add_text(" ");
1515
class_desc->push_color(theme_cache.comment_color);
1516
if (cd.is_script_doc) {
1517
class_desc->add_text(TTR("There is currently no description for this theme property."));
1518
} else {
1519
class_desc->append_text(TTR("There is currently no description for this theme property. Please help us by [color=$color][url=$url]contributing one[/url][/color]!").replace("$url", CONTRIBUTE_URL).replace("$color", link_color_text));
1520
}
1521
class_desc->pop(); // color
1522
}
1523
1524
class_desc->pop(); // color
1525
_pop_normal_font();
1526
class_desc->pop(); // indent
1527
1528
class_desc->pop(); // indent
1529
}
1530
}
1531
1532
// Signals
1533
if (!cd.signals.is_empty()) {
1534
if (sort_methods) {
1535
cd.signals.sort();
1536
}
1537
1538
bool header_added = false;
1539
1540
for (const DocData::MethodDoc &signal : cd.signals) {
1541
// Ignore undocumented private.
1542
const bool is_documented = signal.is_deprecated || signal.is_experimental || !signal.description.strip_edges().is_empty();
1543
if (!is_documented && signal.name.begins_with("_")) {
1544
continue;
1545
}
1546
1547
if (!header_added) {
1548
header_added = true;
1549
1550
class_desc->add_newline();
1551
class_desc->add_newline();
1552
1553
section_line.push_back(Pair<String, int>(TTR("Signals"), class_desc->get_paragraph_count() - 2));
1554
_push_title_font();
1555
class_desc->add_text(TTR("Signals"));
1556
_pop_title_font();
1557
}
1558
1559
class_desc->add_newline();
1560
class_desc->add_newline();
1561
1562
signal_line[signal.name] = class_desc->get_paragraph_count() - 2; // Gets overridden if description.
1563
1564
class_desc->push_indent(1);
1565
1566
// Signal header.
1567
_push_code_font();
1568
_add_bulletpoint();
1569
class_desc->push_color(theme_cache.headline_color);
1570
class_desc->add_text(signal.name);
1571
class_desc->pop(); // color
1572
1573
class_desc->push_color(theme_cache.symbol_color);
1574
class_desc->add_text("(");
1575
class_desc->pop(); // color
1576
1577
for (int j = 0; j < signal.arguments.size(); j++) {
1578
const DocData::ArgumentDoc &argument = signal.arguments[j];
1579
1580
if (j > 0) {
1581
class_desc->push_color(theme_cache.symbol_color);
1582
class_desc->add_text(", ");
1583
class_desc->pop(); // color
1584
}
1585
1586
class_desc->push_color(theme_cache.text_color);
1587
class_desc->add_text(argument.name);
1588
class_desc->pop(); // color
1589
1590
class_desc->push_color(theme_cache.symbol_color);
1591
class_desc->add_text(colon_nbsp);
1592
class_desc->pop(); // color
1593
1594
_add_type(argument.type, argument.enumeration, argument.is_bitfield);
1595
1596
// Signals currently do not support default argument values, neither the core nor GDScript.
1597
// This code is just for completeness.
1598
if (!argument.default_value.is_empty()) {
1599
class_desc->push_color(theme_cache.symbol_color);
1600
class_desc->add_text(nbsp_equal_nbsp);
1601
class_desc->pop(); // color
1602
1603
class_desc->push_color(theme_cache.value_color);
1604
class_desc->add_text(_fix_constant(argument.default_value));
1605
class_desc->pop(); // color
1606
}
1607
}
1608
1609
class_desc->push_color(theme_cache.symbol_color);
1610
class_desc->add_text(")");
1611
class_desc->pop(); // color
1612
1613
_pop_code_font();
1614
1615
class_desc->add_newline();
1616
1617
// Signal description.
1618
class_desc->push_indent(1);
1619
_push_normal_font();
1620
class_desc->push_color(theme_cache.comment_color);
1621
1622
const String descr = HANDLE_DOC(signal.description);
1623
const bool is_multiline = descr.find_char('\n') > 0;
1624
bool has_prev_text = false;
1625
1626
if (signal.is_deprecated) {
1627
has_prev_text = true;
1628
DEPRECATED_DOC_MSG(HANDLE_DOC(signal.deprecated_message), TTR("This signal may be changed or removed in future versions."));
1629
}
1630
1631
if (signal.is_experimental) {
1632
if (has_prev_text) {
1633
class_desc->add_newline();
1634
if (is_multiline) {
1635
class_desc->add_newline();
1636
}
1637
}
1638
has_prev_text = true;
1639
EXPERIMENTAL_DOC_MSG(HANDLE_DOC(signal.experimental_message), TTR("This signal may be changed or removed in future versions."));
1640
}
1641
1642
if (!descr.is_empty()) {
1643
if (has_prev_text) {
1644
class_desc->add_newline();
1645
if (is_multiline) {
1646
class_desc->add_newline();
1647
}
1648
}
1649
has_prev_text = true;
1650
_add_text(descr);
1651
} else if (!has_prev_text) {
1652
class_desc->add_image(get_editor_theme_icon(SNAME("Error")));
1653
class_desc->add_text(" ");
1654
class_desc->push_color(theme_cache.comment_color);
1655
if (cd.is_script_doc) {
1656
class_desc->add_text(TTR("There is currently no description for this signal."));
1657
} else {
1658
class_desc->append_text(TTR("There is currently no description for this signal. Please help us by [color=$color][url=$url]contributing one[/url][/color]!").replace("$url", CONTRIBUTE_URL).replace("$color", link_color_text));
1659
}
1660
class_desc->pop(); // color
1661
}
1662
1663
class_desc->pop(); // color
1664
_pop_normal_font();
1665
class_desc->pop(); // indent
1666
1667
class_desc->pop(); // indent
1668
}
1669
}
1670
1671
// Constants and enums
1672
if (!cd.constants.is_empty()) {
1673
HashMap<String, Vector<DocData::ConstantDoc>> enums;
1674
Vector<DocData::ConstantDoc> constants;
1675
1676
for (const DocData::ConstantDoc &constant : cd.constants) {
1677
if (!constant.enumeration.is_empty()) {
1678
if (!enums.has(constant.enumeration)) {
1679
enums[constant.enumeration] = Vector<DocData::ConstantDoc>();
1680
}
1681
1682
enums[constant.enumeration].push_back(constant);
1683
} else {
1684
// Ignore undocumented private.
1685
const bool is_documented = constant.is_deprecated || constant.is_experimental || !constant.description.strip_edges().is_empty();
1686
if (!is_documented && constant.name.begins_with("_")) {
1687
continue;
1688
}
1689
constants.push_back(constant);
1690
}
1691
}
1692
1693
// Enums
1694
bool has_enums = enums.size() && !cd.is_script_doc;
1695
if (enums.size() && !has_enums) {
1696
for (KeyValue<String, DocData::EnumDoc> &E : cd.enums) {
1697
const bool is_documented = E.value.is_deprecated || E.value.is_experimental || !E.value.description.strip_edges().is_empty();
1698
if (!is_documented && E.key.begins_with("_")) {
1699
continue;
1700
}
1701
has_enums = true;
1702
break;
1703
}
1704
}
1705
if (has_enums) {
1706
class_desc->add_newline();
1707
class_desc->add_newline();
1708
1709
section_line.push_back(Pair<String, int>(TTR("Enumerations"), class_desc->get_paragraph_count() - 2));
1710
_push_title_font();
1711
class_desc->add_text(TTR("Enumerations"));
1712
_pop_title_font();
1713
1714
for (KeyValue<String, Vector<DocData::ConstantDoc>> &E : enums) {
1715
String key = E.key;
1716
if ((key.get_slice_count(".") > 1) && (key.get_slicec('.', 0) == edited_class)) {
1717
key = key.get_slicec('.', 1);
1718
}
1719
if (cd.enums.has(key)) {
1720
const bool is_documented = cd.enums[key].is_deprecated || cd.enums[key].is_experimental || !cd.enums[key].description.strip_edges().is_empty();
1721
if (!is_documented && cd.is_script_doc && E.key.begins_with("_")) {
1722
continue;
1723
}
1724
}
1725
1726
class_desc->add_newline();
1727
class_desc->add_newline();
1728
1729
// Enum header.
1730
_push_code_font();
1731
1732
enum_line[E.key] = class_desc->get_paragraph_count() - 2;
1733
class_desc->push_color(theme_cache.title_color);
1734
if (E.value.size() && E.value[0].is_bitfield) {
1735
class_desc->add_text("flags ");
1736
} else {
1737
class_desc->add_text("enum ");
1738
}
1739
class_desc->pop(); // color
1740
1741
class_desc->push_color(theme_cache.headline_color);
1742
class_desc->add_text(key);
1743
class_desc->pop(); // color
1744
1745
class_desc->push_color(theme_cache.symbol_color);
1746
class_desc->add_text(":");
1747
class_desc->pop(); // color
1748
1749
_pop_code_font();
1750
1751
// Enum description.
1752
if (key != "@unnamed_enums" && cd.enums.has(key)) {
1753
const String descr = HANDLE_DOC(cd.enums[key].description);
1754
const bool is_multiline = descr.find_char('\n') > 0;
1755
if (cd.enums[key].is_deprecated || cd.enums[key].is_experimental || !descr.is_empty()) {
1756
class_desc->add_newline();
1757
1758
class_desc->push_indent(1);
1759
_push_normal_font();
1760
class_desc->push_color(theme_cache.text_color);
1761
1762
bool has_prev_text = false;
1763
1764
if (cd.enums[key].is_deprecated) {
1765
has_prev_text = true;
1766
DEPRECATED_DOC_MSG(HANDLE_DOC(cd.enums[key].deprecated_message), TTR("This enumeration may be changed or removed in future versions."));
1767
}
1768
1769
if (cd.enums[key].is_experimental) {
1770
if (has_prev_text) {
1771
class_desc->add_newline();
1772
if (is_multiline) {
1773
class_desc->add_newline();
1774
}
1775
}
1776
has_prev_text = true;
1777
EXPERIMENTAL_DOC_MSG(HANDLE_DOC(cd.enums[key].experimental_message), TTR("This enumeration may be changed or removed in future versions."));
1778
}
1779
1780
if (!descr.is_empty()) {
1781
if (has_prev_text) {
1782
class_desc->add_newline();
1783
if (is_multiline) {
1784
class_desc->add_newline();
1785
}
1786
}
1787
has_prev_text = true;
1788
_add_text(descr);
1789
}
1790
1791
class_desc->pop(); // color
1792
_pop_normal_font();
1793
class_desc->pop(); // indent
1794
}
1795
}
1796
1797
HashMap<String, int> enum_values;
1798
const int enum_start_line = enum_line[E.key];
1799
1800
bool prev_is_multiline = true; // Use a large margin for the first item.
1801
for (const DocData::ConstantDoc &enum_value : E.value) {
1802
const String descr = HANDLE_DOC(enum_value.description);
1803
const bool is_multiline = descr.find_char('\n') > 0;
1804
1805
class_desc->add_newline();
1806
if (prev_is_multiline || is_multiline) {
1807
class_desc->add_newline();
1808
}
1809
prev_is_multiline = is_multiline;
1810
1811
if (cd.name == "@GlobalScope") {
1812
enum_values[enum_value.name] = enum_start_line;
1813
}
1814
1815
// Add the enum constant line to the constant_line map so we can locate it as a constant.
1816
constant_line[enum_value.name] = class_desc->get_paragraph_count() - 2;
1817
1818
class_desc->push_indent(1);
1819
1820
// Enum value header.
1821
_push_code_font();
1822
_add_bulletpoint();
1823
1824
class_desc->push_color(theme_cache.headline_color);
1825
class_desc->add_text(enum_value.name);
1826
class_desc->pop(); // color
1827
1828
class_desc->push_color(theme_cache.symbol_color);
1829
class_desc->add_text(nbsp_equal_nbsp);
1830
class_desc->pop(); // color
1831
1832
class_desc->push_color(theme_cache.value_color);
1833
class_desc->add_text(_fix_constant(enum_value.value));
1834
class_desc->pop(); // color
1835
1836
_pop_code_font();
1837
1838
// Enum value description.
1839
if (enum_value.is_deprecated || enum_value.is_experimental || !descr.is_empty()) {
1840
class_desc->add_newline();
1841
1842
class_desc->push_indent(1);
1843
_push_normal_font();
1844
class_desc->push_color(theme_cache.comment_color);
1845
1846
bool has_prev_text = false;
1847
1848
if (enum_value.is_deprecated) {
1849
has_prev_text = true;
1850
DEPRECATED_DOC_MSG(HANDLE_DOC(enum_value.deprecated_message), TTR("This constant may be changed or removed in future versions."));
1851
}
1852
1853
if (enum_value.is_experimental) {
1854
if (has_prev_text) {
1855
class_desc->add_newline();
1856
if (is_multiline) {
1857
class_desc->add_newline();
1858
}
1859
}
1860
has_prev_text = true;
1861
EXPERIMENTAL_DOC_MSG(HANDLE_DOC(enum_value.experimental_message), TTR("This constant may be changed or removed in future versions."));
1862
}
1863
1864
if (!descr.is_empty()) {
1865
if (has_prev_text) {
1866
class_desc->add_newline();
1867
if (is_multiline) {
1868
class_desc->add_newline();
1869
}
1870
}
1871
has_prev_text = true;
1872
_add_text(descr);
1873
}
1874
1875
class_desc->pop(); // color
1876
_pop_normal_font();
1877
class_desc->pop(); // indent
1878
}
1879
1880
class_desc->pop(); // indent
1881
}
1882
1883
if (cd.name == "@GlobalScope") {
1884
enum_values_line[E.key] = enum_values;
1885
}
1886
}
1887
}
1888
1889
// Constants
1890
if (!constants.is_empty()) {
1891
class_desc->add_newline();
1892
class_desc->add_newline();
1893
1894
section_line.push_back(Pair<String, int>(TTR("Constants"), class_desc->get_paragraph_count() - 2));
1895
_push_title_font();
1896
class_desc->add_text(TTR("Constants"));
1897
_pop_title_font();
1898
1899
bool prev_is_multiline = true; // Use a large margin for the first item.
1900
for (const DocData::ConstantDoc &constant : constants) {
1901
const String descr = HANDLE_DOC(constant.description);
1902
const bool is_multiline = descr.find_char('\n') > 0;
1903
1904
class_desc->add_newline();
1905
if (prev_is_multiline || is_multiline) {
1906
class_desc->add_newline();
1907
}
1908
prev_is_multiline = is_multiline;
1909
1910
constant_line[constant.name] = class_desc->get_paragraph_count() - 2;
1911
1912
class_desc->push_indent(1);
1913
1914
// Constant header.
1915
_push_code_font();
1916
1917
if (constant.value.begins_with("Color(") && constant.value.ends_with(")")) {
1918
String stripped = constant.value.remove_char(' ').replace("Color(", "").remove_char(')');
1919
PackedFloat64Array color = stripped.split_floats(",");
1920
if (color.size() >= 3) {
1921
class_desc->push_color(Color(color[0], color[1], color[2]));
1922
_add_bulletpoint();
1923
class_desc->pop(); // color
1924
}
1925
} else {
1926
_add_bulletpoint();
1927
}
1928
1929
class_desc->push_color(theme_cache.headline_color);
1930
class_desc->add_text(constant.name);
1931
class_desc->pop(); // color
1932
1933
class_desc->push_color(theme_cache.symbol_color);
1934
class_desc->add_text(nbsp_equal_nbsp);
1935
class_desc->pop(); // color
1936
1937
class_desc->push_color(theme_cache.value_color);
1938
class_desc->add_text(_fix_constant(constant.value));
1939
class_desc->pop(); // color
1940
1941
_pop_code_font();
1942
1943
// Constant description.
1944
if (constant.is_deprecated || constant.is_experimental || !descr.is_empty()) {
1945
class_desc->add_newline();
1946
1947
class_desc->push_indent(1);
1948
_push_normal_font();
1949
class_desc->push_color(theme_cache.comment_color);
1950
1951
bool has_prev_text = false;
1952
1953
if (constant.is_deprecated) {
1954
has_prev_text = true;
1955
DEPRECATED_DOC_MSG(HANDLE_DOC(constant.deprecated_message), TTR("This constant may be changed or removed in future versions."));
1956
}
1957
1958
if (constant.is_experimental) {
1959
if (has_prev_text) {
1960
class_desc->add_newline();
1961
if (is_multiline) {
1962
class_desc->add_newline();
1963
}
1964
}
1965
has_prev_text = true;
1966
EXPERIMENTAL_DOC_MSG(HANDLE_DOC(constant.experimental_message), TTR("This constant may be changed or removed in future versions."));
1967
}
1968
1969
if (!descr.is_empty()) {
1970
if (has_prev_text) {
1971
class_desc->add_newline();
1972
if (is_multiline) {
1973
class_desc->add_newline();
1974
}
1975
}
1976
has_prev_text = true;
1977
_add_text(descr);
1978
}
1979
1980
class_desc->pop(); // color
1981
_pop_normal_font();
1982
class_desc->pop(); // indent
1983
}
1984
1985
class_desc->pop(); // indent
1986
}
1987
}
1988
}
1989
1990
// Annotations
1991
if (!cd.annotations.is_empty()) {
1992
if (sort_methods) {
1993
cd.annotations.sort();
1994
}
1995
1996
class_desc->add_newline();
1997
class_desc->add_newline();
1998
1999
section_line.push_back(Pair<String, int>(TTR("Annotations"), class_desc->get_paragraph_count() - 2));
2000
_push_title_font();
2001
class_desc->add_text(TTR("Annotations"));
2002
_pop_title_font();
2003
2004
for (const DocData::MethodDoc &annotation : cd.annotations) {
2005
class_desc->add_newline();
2006
class_desc->add_newline();
2007
2008
annotation_line[annotation.name] = class_desc->get_paragraph_count() - 2; // Gets overridden if description.
2009
2010
class_desc->push_indent(1);
2011
2012
// Annotation header.
2013
_push_code_font();
2014
_add_bulletpoint();
2015
2016
class_desc->push_color(theme_cache.headline_color);
2017
class_desc->add_text(annotation.name);
2018
class_desc->pop(); // color
2019
2020
if (!annotation.arguments.is_empty()) {
2021
class_desc->push_color(theme_cache.symbol_color);
2022
class_desc->add_text("(");
2023
class_desc->pop(); // color
2024
2025
for (int j = 0; j < annotation.arguments.size(); j++) {
2026
const DocData::ArgumentDoc &argument = annotation.arguments[j];
2027
2028
if (j > 0) {
2029
class_desc->push_color(theme_cache.symbol_color);
2030
class_desc->add_text(", ");
2031
class_desc->pop(); // color
2032
}
2033
2034
class_desc->push_color(theme_cache.text_color);
2035
class_desc->add_text(argument.name);
2036
class_desc->pop(); // color
2037
2038
class_desc->push_color(theme_cache.symbol_color);
2039
class_desc->add_text(colon_nbsp);
2040
class_desc->pop(); // color
2041
2042
_add_type(argument.type, argument.enumeration, argument.is_bitfield);
2043
2044
if (!argument.default_value.is_empty()) {
2045
class_desc->push_color(theme_cache.symbol_color);
2046
class_desc->add_text(nbsp_equal_nbsp);
2047
class_desc->pop(); // color
2048
2049
class_desc->push_color(theme_cache.value_color);
2050
class_desc->add_text(_fix_constant(argument.default_value));
2051
class_desc->pop(); // color
2052
}
2053
}
2054
2055
if (annotation.qualifiers.contains("vararg")) {
2056
if (!annotation.arguments.is_empty()) {
2057
class_desc->push_color(theme_cache.symbol_color);
2058
class_desc->add_text(", ");
2059
class_desc->pop(); // color
2060
}
2061
2062
class_desc->push_color(theme_cache.symbol_color);
2063
class_desc->add_text("...");
2064
class_desc->pop(); // color
2065
2066
const DocData::ArgumentDoc &rest_argument = annotation.rest_argument;
2067
2068
class_desc->push_color(theme_cache.text_color);
2069
class_desc->add_text(rest_argument.name.is_empty() ? "args" : rest_argument.name);
2070
class_desc->pop(); // color
2071
2072
class_desc->push_color(theme_cache.symbol_color);
2073
class_desc->add_text(colon_nbsp);
2074
class_desc->pop(); // color
2075
2076
if (rest_argument.type.is_empty()) {
2077
_add_type("Array");
2078
} else {
2079
_add_type(rest_argument.type, rest_argument.enumeration, rest_argument.is_bitfield);
2080
}
2081
}
2082
2083
class_desc->push_color(theme_cache.symbol_color);
2084
class_desc->add_text(")");
2085
class_desc->pop(); // color
2086
}
2087
2088
if (!annotation.qualifiers.is_empty()) {
2089
class_desc->push_color(theme_cache.qualifier_color);
2090
_add_qualifiers_to_rt(annotation.qualifiers, class_desc);
2091
class_desc->pop(); // color
2092
}
2093
2094
_pop_code_font();
2095
2096
class_desc->add_newline();
2097
2098
// Annotation description.
2099
class_desc->push_indent(1);
2100
_push_normal_font();
2101
class_desc->push_color(theme_cache.comment_color);
2102
2103
const String descr = HANDLE_DOC(annotation.description);
2104
if (!descr.is_empty()) {
2105
_add_text(descr);
2106
} else {
2107
class_desc->add_image(get_editor_theme_icon(SNAME("Error")));
2108
class_desc->add_text(" ");
2109
class_desc->push_color(theme_cache.comment_color);
2110
if (cd.is_script_doc) {
2111
class_desc->add_text(TTR("There is currently no description for this annotation."));
2112
} else {
2113
class_desc->append_text(TTR("There is currently no description for this annotation. Please help us by [color=$color][url=$url]contributing one[/url][/color]!").replace("$url", CONTRIBUTE_URL).replace("$color", link_color_text));
2114
}
2115
class_desc->pop(); // color
2116
}
2117
2118
class_desc->pop(); // color
2119
_pop_normal_font();
2120
class_desc->pop(); // indent
2121
2122
class_desc->pop(); // indent
2123
}
2124
}
2125
2126
// Property descriptions
2127
if (has_property_descriptions) {
2128
class_desc->add_newline();
2129
class_desc->add_newline();
2130
class_desc->add_newline();
2131
2132
section_line.push_back(Pair<String, int>(TTR("Property Descriptions"), class_desc->get_paragraph_count() - 2));
2133
_push_title_font();
2134
class_desc->add_text(TTR("Property Descriptions"));
2135
_pop_title_font();
2136
2137
for (const DocData::PropertyDoc &prop : cd.properties) {
2138
if (prop.overridden) {
2139
continue;
2140
}
2141
// Ignore undocumented private.
2142
const bool is_documented = prop.is_deprecated || prop.is_experimental || !prop.description.strip_edges().is_empty();
2143
if (!is_documented && prop.name.begins_with("_")) {
2144
continue;
2145
}
2146
2147
class_desc->add_newline();
2148
class_desc->add_newline();
2149
class_desc->add_newline();
2150
2151
property_line[prop.name] = class_desc->get_paragraph_count() - 2;
2152
2153
class_desc->push_table(2);
2154
class_desc->set_table_column_expand(1, true);
2155
2156
class_desc->push_cell();
2157
_push_code_font();
2158
_add_bulletpoint();
2159
_add_type(prop.type, prop.enumeration, prop.is_bitfield);
2160
_pop_code_font();
2161
class_desc->pop(); // cell
2162
2163
class_desc->push_cell();
2164
_push_code_font();
2165
2166
class_desc->push_color(theme_cache.headline_color);
2167
class_desc->add_text(prop.name);
2168
class_desc->pop(); // color
2169
2170
if (!prop.default_value.is_empty()) {
2171
class_desc->push_color(theme_cache.symbol_color);
2172
class_desc->add_text(" [" + TTR("default:") + " ");
2173
class_desc->pop(); // color
2174
2175
class_desc->push_color(theme_cache.value_color);
2176
class_desc->add_text(_fix_constant(prop.default_value));
2177
class_desc->pop(); // color
2178
2179
class_desc->push_color(theme_cache.symbol_color);
2180
class_desc->add_text("]");
2181
class_desc->pop(); // color
2182
}
2183
2184
if (cd.is_script_doc && (!prop.setter.is_empty() || !prop.getter.is_empty())) {
2185
class_desc->push_color(theme_cache.symbol_color);
2186
class_desc->add_text(" [" + TTR("property:") + " ");
2187
class_desc->pop(); // color
2188
2189
if (!prop.setter.is_empty()) {
2190
class_desc->push_color(theme_cache.value_color);
2191
class_desc->add_text("setter");
2192
class_desc->pop(); // color
2193
}
2194
if (!prop.getter.is_empty()) {
2195
if (!prop.setter.is_empty()) {
2196
class_desc->push_color(theme_cache.symbol_color);
2197
class_desc->add_text(", ");
2198
class_desc->pop(); // color
2199
}
2200
class_desc->push_color(theme_cache.value_color);
2201
class_desc->add_text("getter");
2202
class_desc->pop(); // color
2203
}
2204
2205
class_desc->push_color(theme_cache.symbol_color);
2206
class_desc->add_text("]");
2207
class_desc->pop(); // color
2208
}
2209
2210
_pop_code_font();
2211
class_desc->pop(); // cell
2212
2213
// Script doc doesn't have setter, getter.
2214
if (!cd.is_script_doc) {
2215
HashMap<String, DocData::MethodDoc> method_map;
2216
for (int j = 0; j < methods.size(); j++) {
2217
method_map[methods[j].name] = methods[j];
2218
}
2219
2220
if (!prop.setter.is_empty()) {
2221
class_desc->push_cell();
2222
class_desc->pop(); // cell
2223
2224
class_desc->push_cell();
2225
_push_code_font();
2226
class_desc->push_color(theme_cache.text_color);
2227
2228
if (method_map[prop.setter].arguments.size() > 1) {
2229
// Setters with additional arguments are exposed in the method list, so we link them here for quick access.
2230
class_desc->push_meta("@method " + prop.setter);
2231
class_desc->add_text(prop.setter + TTR("(value)"));
2232
class_desc->pop(); // meta
2233
} else {
2234
class_desc->add_text(prop.setter + TTR("(value)"));
2235
}
2236
2237
class_desc->pop(); // color
2238
class_desc->push_color(theme_cache.comment_color);
2239
class_desc->add_text(" setter");
2240
class_desc->pop(); // color
2241
_pop_code_font();
2242
class_desc->pop(); // cell
2243
2244
method_line[prop.setter] = property_line[prop.name];
2245
}
2246
2247
if (!prop.getter.is_empty()) {
2248
class_desc->push_cell();
2249
class_desc->pop(); // cell
2250
2251
class_desc->push_cell();
2252
_push_code_font();
2253
class_desc->push_color(theme_cache.text_color);
2254
2255
if (!method_map[prop.getter].arguments.is_empty()) {
2256
// Getters with additional arguments are exposed in the method list, so we link them here for quick access.
2257
class_desc->push_meta("@method " + prop.getter);
2258
class_desc->add_text(prop.getter + "()");
2259
class_desc->pop(); // meta
2260
} else {
2261
class_desc->add_text(prop.getter + "()");
2262
}
2263
2264
class_desc->pop(); // color
2265
class_desc->push_color(theme_cache.comment_color);
2266
class_desc->add_text(" getter");
2267
class_desc->pop(); // color
2268
_pop_code_font();
2269
class_desc->pop(); // cell
2270
2271
method_line[prop.getter] = property_line[prop.name];
2272
}
2273
}
2274
2275
class_desc->pop(); // table
2276
2277
class_desc->add_newline();
2278
class_desc->add_newline();
2279
2280
class_desc->push_indent(1);
2281
_push_normal_font();
2282
class_desc->push_color(theme_cache.text_color);
2283
2284
bool has_prev_text = false;
2285
2286
if (prop.is_deprecated) {
2287
has_prev_text = true;
2288
DEPRECATED_DOC_MSG(HANDLE_DOC(prop.deprecated_message), TTR("This property may be changed or removed in future versions."));
2289
}
2290
2291
if (prop.is_experimental) {
2292
if (has_prev_text) {
2293
class_desc->add_newline();
2294
class_desc->add_newline();
2295
}
2296
has_prev_text = true;
2297
EXPERIMENTAL_DOC_MSG(HANDLE_DOC(prop.experimental_message), TTR("This property may be changed or removed in future versions."));
2298
}
2299
2300
const String descr = HANDLE_DOC(prop.description);
2301
if (!descr.is_empty()) {
2302
if (has_prev_text) {
2303
class_desc->add_newline();
2304
class_desc->add_newline();
2305
}
2306
has_prev_text = true;
2307
_add_text(descr);
2308
} else if (!has_prev_text) {
2309
class_desc->add_image(get_editor_theme_icon(SNAME("Error")));
2310
class_desc->add_text(" ");
2311
class_desc->push_color(theme_cache.comment_color);
2312
if (cd.is_script_doc) {
2313
class_desc->add_text(TTR("There is currently no description for this property."));
2314
} else {
2315
class_desc->append_text(TTR("There is currently no description for this property. Please help us by [color=$color][url=$url]contributing one[/url][/color]!").replace("$url", CONTRIBUTE_URL).replace("$color", link_color_text));
2316
}
2317
class_desc->pop(); // color
2318
}
2319
2320
// Add copy note to built-in properties returning `Packed*Array`.
2321
if (!cd.is_script_doc && packed_array_types.has(prop.type)) {
2322
class_desc->add_newline();
2323
class_desc->add_newline();
2324
_add_text(vformat(TTR("[b]Note:[/b] The returned array is [i]copied[/i] and any changes to it will not update the original property value. See [%s] for more details."), prop.type));
2325
}
2326
2327
class_desc->pop(); // color
2328
_pop_normal_font();
2329
class_desc->pop(); // indent
2330
}
2331
}
2332
2333
// Constructor descriptions
2334
if (!cd.constructors.is_empty()) {
2335
_update_method_descriptions(cd, METHOD_TYPE_CONSTRUCTOR, cd.constructors);
2336
}
2337
2338
// Method descriptions
2339
if (!methods.is_empty()) {
2340
_update_method_descriptions(cd, METHOD_TYPE_METHOD, methods);
2341
}
2342
2343
// Operator descriptions
2344
if (!cd.operators.is_empty()) {
2345
_update_method_descriptions(cd, METHOD_TYPE_OPERATOR, cd.operators);
2346
}
2347
2348
// Allow the document to be scrolled slightly below the end.
2349
class_desc->add_newline();
2350
class_desc->add_newline();
2351
2352
// Free the scroll.
2353
scroll_locked = false;
2354
2355
#undef HANDLE_DOC
2356
}
2357
2358
void EditorHelp::_request_help(const String &p_string) {
2359
Error err = _goto_desc(p_string);
2360
if (err == OK) {
2361
EditorNode::get_singleton()->get_editor_main_screen()->select(EditorMainScreen::EDITOR_SCRIPT);
2362
}
2363
}
2364
2365
void EditorHelp::_help_callback(const String &p_topic) {
2366
Vector<String> parts;
2367
{
2368
int from = 0;
2369
int buffer_start = 0;
2370
while (true) {
2371
const int pos = p_topic.find_char(':', from);
2372
if (pos < 0) {
2373
parts.push_back(p_topic.substr(buffer_start));
2374
break;
2375
}
2376
2377
if (pos + 1 < p_topic.length() && p_topic[pos + 1] == ':') {
2378
// `::` used in built-in scripts.
2379
from = pos + 2;
2380
} else {
2381
parts.push_back(p_topic.substr(buffer_start, pos - buffer_start));
2382
from = pos + 1;
2383
buffer_start = from;
2384
}
2385
}
2386
}
2387
2388
const String what = parts[0]; // `parts` is always non-empty.
2389
const String clss = (parts.size() > 1) ? parts[1] : String();
2390
const String name = (parts.size() > 2) ? parts[2] : String();
2391
2392
_request_help(clss); // First go to class.
2393
2394
int line = 0;
2395
2396
if (what == "class_desc") {
2397
line = description_line;
2398
} else if (what == "class_signal") {
2399
if (signal_line.has(name)) {
2400
line = signal_line[name];
2401
}
2402
} else if (what == "class_method" || what == "class_method_desc") {
2403
if (method_line.has(name)) {
2404
line = method_line[name];
2405
}
2406
} else if (what == "class_property") {
2407
if (property_line.has(name)) {
2408
line = property_line[name];
2409
}
2410
} else if (what == "class_enum") {
2411
if (enum_line.has(name)) {
2412
line = enum_line[name];
2413
}
2414
} else if (what == "class_theme_item") {
2415
if (theme_property_line.has(name)) {
2416
line = theme_property_line[name];
2417
}
2418
} else if (what == "class_constant") {
2419
if (constant_line.has(name)) {
2420
line = constant_line[name];
2421
}
2422
} else if (what == "class_annotation") {
2423
if (annotation_line.has(name)) {
2424
line = annotation_line[name];
2425
}
2426
} else if (what == "class_global") { // Deprecated.
2427
if (constant_line.has(name)) {
2428
line = constant_line[name];
2429
} else if (method_line.has(name)) {
2430
line = method_line[name];
2431
} else {
2432
HashMap<String, HashMap<String, int>>::Iterator iter = enum_values_line.begin();
2433
while (true) {
2434
if (iter->value.has(name)) {
2435
line = iter->value[name];
2436
break;
2437
} else if (iter == enum_values_line.last()) {
2438
break;
2439
} else {
2440
++iter;
2441
}
2442
}
2443
}
2444
}
2445
2446
if (class_desc->is_finished()) {
2447
class_desc->scroll_to_paragraph(line);
2448
} else {
2449
scroll_to = line;
2450
}
2451
}
2452
2453
static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, const Control *p_owner_node, const String &p_class) {
2454
bool is_native = false;
2455
{
2456
const DocData::ClassDoc *E = EditorHelp::get_doc(p_class);
2457
if (E && !E->is_script_doc) {
2458
is_native = true;
2459
}
2460
}
2461
2462
const bool using_space_indent = int(EDITOR_GET("text_editor/behavior/indent/type")) == 1;
2463
const int indent_size = MAX(1, int(EDITOR_GET("text_editor/behavior/indent/size")));
2464
2465
const Ref<Font> doc_font = p_owner_node->get_theme_font(SNAME("doc"), EditorStringName(EditorFonts));
2466
const Ref<Font> doc_bold_font = p_owner_node->get_theme_font(SNAME("doc_bold"), EditorStringName(EditorFonts));
2467
const Ref<Font> doc_italic_font = p_owner_node->get_theme_font(SNAME("doc_italic"), EditorStringName(EditorFonts));
2468
const Ref<Font> doc_code_font = p_owner_node->get_theme_font(SNAME("doc_source"), EditorStringName(EditorFonts));
2469
const Ref<Font> doc_kbd_font = p_owner_node->get_theme_font(SNAME("doc_keyboard"), EditorStringName(EditorFonts));
2470
2471
const int doc_font_size = p_owner_node->get_theme_font_size(SNAME("doc_size"), EditorStringName(EditorFonts));
2472
const int doc_code_font_size = p_owner_node->get_theme_font_size(SNAME("doc_source_size"), EditorStringName(EditorFonts));
2473
const int doc_kbd_font_size = p_owner_node->get_theme_font_size(SNAME("doc_keyboard_size"), EditorStringName(EditorFonts));
2474
2475
const Color type_color = p_owner_node->get_theme_color(SNAME("type_color"), SNAME("EditorHelp"));
2476
const Color code_color = p_owner_node->get_theme_color(SNAME("code_color"), SNAME("EditorHelp"));
2477
const Color kbd_color = p_owner_node->get_theme_color(SNAME("kbd_color"), SNAME("EditorHelp"));
2478
const Color code_dark_color = Color(code_color, 0.8);
2479
2480
const Color link_color = p_owner_node->get_theme_color(SNAME("link_color"), SNAME("EditorHelp"));
2481
const Color link_method_color = p_owner_node->get_theme_color(SNAME("accent_color"), EditorStringName(Editor));
2482
const Color link_property_color = link_color.lerp(p_owner_node->get_theme_color(SNAME("accent_color"), EditorStringName(Editor)), 0.25);
2483
const Color link_annotation_color = link_color.lerp(p_owner_node->get_theme_color(SNAME("accent_color"), EditorStringName(Editor)), 0.5);
2484
2485
const Color code_bg_color = p_owner_node->get_theme_color(SNAME("code_bg_color"), SNAME("EditorHelp"));
2486
const Color kbd_bg_color = p_owner_node->get_theme_color(SNAME("kbd_bg_color"), SNAME("EditorHelp"));
2487
const Color param_bg_color = p_owner_node->get_theme_color(SNAME("param_bg_color"), SNAME("EditorHelp"));
2488
2489
String bbcode = p_bbcode.dedent().remove_chars("\r").strip_edges();
2490
2491
// Select the correct code examples.
2492
switch ((int)EDITOR_GET("text_editor/help/class_reference_examples")) {
2493
case 0: // GDScript
2494
bbcode = bbcode.replace("[gdscript", "[codeblock lang=gdscript"); // Tag can have extra arguments.
2495
bbcode = bbcode.replace("[/gdscript]", "[/codeblock]");
2496
2497
for (int pos = bbcode.find("[csharp"); pos != -1; pos = bbcode.find("[csharp")) {
2498
int end_pos = bbcode.find("[/csharp]");
2499
if (end_pos == -1) {
2500
WARN_PRINT("Unclosed [csharp] block or parse fail in code (search for tag errors)");
2501
break;
2502
}
2503
2504
bbcode = bbcode.left(pos) + bbcode.substr(end_pos + 9); // 9 is length of "[/csharp]".
2505
while (bbcode[pos] == '\n') {
2506
bbcode = bbcode.left(pos) + bbcode.substr(pos + 1);
2507
}
2508
}
2509
break;
2510
case 1: // C#
2511
bbcode = bbcode.replace("[csharp", "[codeblock lang=csharp"); // Tag can have extra arguments.
2512
bbcode = bbcode.replace("[/csharp]", "[/codeblock]");
2513
2514
for (int pos = bbcode.find("[gdscript"); pos != -1; pos = bbcode.find("[gdscript")) {
2515
int end_pos = bbcode.find("[/gdscript]");
2516
if (end_pos == -1) {
2517
WARN_PRINT("Unclosed [gdscript] block or parse fail in code (search for tag errors)");
2518
break;
2519
}
2520
2521
bbcode = bbcode.left(pos) + bbcode.substr(end_pos + 11); // 11 is length of "[/gdscript]".
2522
while (bbcode[pos] == '\n') {
2523
bbcode = bbcode.left(pos) + bbcode.substr(pos + 1);
2524
}
2525
}
2526
break;
2527
case 2: // GDScript and C#
2528
bbcode = bbcode.replace("[csharp", "[b]C#:[/b]\n[codeblock lang=csharp"); // Tag can have extra arguments.
2529
bbcode = bbcode.replace("[gdscript", "[b]GDScript:[/b]\n[codeblock lang=gdscript"); // Tag can have extra arguments.
2530
2531
bbcode = bbcode.replace("[/csharp]", "[/codeblock]");
2532
bbcode = bbcode.replace("[/gdscript]", "[/codeblock]");
2533
break;
2534
}
2535
2536
// Remove codeblocks (they would be printed otherwise).
2537
bbcode = bbcode.replace("[codeblocks]\n", "");
2538
bbcode = bbcode.replace("\n[/codeblocks]", "");
2539
bbcode = bbcode.replace("[codeblocks]", "");
2540
bbcode = bbcode.replace("[/codeblocks]", "");
2541
2542
// Remove `\n` here because `\n` is replaced by `\n\n` later.
2543
// Will be compensated when parsing `[/codeblock]`.
2544
bbcode = bbcode.replace("[/codeblock]\n", "[/codeblock]");
2545
2546
List<String> tag_stack;
2547
2548
int pos = 0;
2549
while (pos < bbcode.length()) {
2550
int brk_pos = bbcode.find_char('[', pos);
2551
2552
if (brk_pos < 0) {
2553
brk_pos = bbcode.length();
2554
}
2555
2556
if (brk_pos > pos) {
2557
p_rt->add_text(bbcode.substr(pos, brk_pos - pos).replace("\n", "\n\n"));
2558
}
2559
2560
if (brk_pos == bbcode.length()) {
2561
break; // Nothing else to add.
2562
}
2563
2564
int brk_end = bbcode.find_char(']', brk_pos + 1);
2565
2566
if (brk_end == -1) {
2567
p_rt->add_text(bbcode.substr(brk_pos).replace("\n", "\n\n"));
2568
break;
2569
}
2570
2571
const String tag = bbcode.substr(brk_pos + 1, brk_end - brk_pos - 1);
2572
2573
if (tag.begins_with("/")) {
2574
bool tag_ok = tag_stack.size() && tag_stack.front()->get() == tag.substr(1);
2575
2576
if (!tag_ok) {
2577
p_rt->add_text("[");
2578
pos = brk_pos + 1;
2579
continue;
2580
}
2581
2582
tag_stack.pop_front();
2583
pos = brk_end + 1;
2584
if (tag == "/img") {
2585
// Nothing to do.
2586
} else if (tag == "/url") {
2587
p_rt->pop(); // meta
2588
p_rt->pop(); // color
2589
p_rt->add_text(nbsp);
2590
p_rt->add_image(p_owner_node->get_editor_theme_icon(SNAME("ExternalLink")), 0, doc_font_size, link_color);
2591
} else {
2592
p_rt->pop();
2593
}
2594
} else if (tag.begins_with("method ") || tag.begins_with("constructor ") || tag.begins_with("operator ") || tag.begins_with("member ") || tag.begins_with("signal ") || tag.begins_with("enum ") || tag.begins_with("constant ") || tag.begins_with("annotation ") || tag.begins_with("theme_item ")) {
2595
const int tag_end = tag.find_char(' ');
2596
const String link_tag = tag.left(tag_end);
2597
const String link_target = tag.substr(tag_end + 1).lstrip(" ");
2598
2599
Color target_color = link_color;
2600
RichTextLabel::MetaUnderline underline_mode = RichTextLabel::META_UNDERLINE_ON_HOVER;
2601
if (link_tag == "method" || link_tag == "constructor" || link_tag == "operator") {
2602
target_color = link_method_color;
2603
} else if (link_tag == "member" || link_tag == "signal" || link_tag == "theme_item") {
2604
target_color = link_property_color;
2605
} else if (link_tag == "annotation") {
2606
target_color = link_annotation_color;
2607
} else {
2608
// Better visibility for constants, enums, etc.
2609
underline_mode = RichTextLabel::META_UNDERLINE_ALWAYS;
2610
}
2611
2612
// Use monospace font to make clickable references
2613
// easier to distinguish from inline code and other text.
2614
p_rt->push_font(doc_code_font);
2615
p_rt->push_font_size(doc_code_font_size);
2616
p_rt->push_color(target_color);
2617
p_rt->push_meta("@" + link_tag + " " + link_target, underline_mode);
2618
2619
if (link_tag == "member" &&
2620
((!link_target.contains_char('.') && (p_class == "ProjectSettings" || p_class == "EditorSettings")) ||
2621
link_target.begins_with("ProjectSettings.") || link_target.begins_with("EditorSettings."))) {
2622
// Special formatting for both ProjectSettings and EditorSettings.
2623
String prefix;
2624
if (link_target.begins_with("EditorSettings.")) {
2625
prefix = "(" + TTR("Editor") + ") ";
2626
}
2627
2628
const String setting_name = link_target.trim_prefix("ProjectSettings.").trim_prefix("EditorSettings.");
2629
PackedStringArray setting_sections;
2630
for (const String &section : setting_name.split("/", false)) {
2631
setting_sections.append(EditorPropertyNameProcessor::get_singleton()->process_name(section, EditorPropertyNameProcessor::get_settings_style()));
2632
}
2633
2634
p_rt->push_bold();
2635
p_rt->add_text(prefix + String(" > ").join(setting_sections));
2636
p_rt->pop(); // bold
2637
} else {
2638
p_rt->add_text(link_target + (link_tag == "method" ? "()" : ""));
2639
}
2640
2641
p_rt->pop(); // meta
2642
p_rt->pop(); // color
2643
p_rt->pop(); // font_size
2644
p_rt->pop(); // font
2645
2646
pos = brk_end + 1;
2647
} else if (tag.begins_with("param ")) {
2648
const int tag_end = tag.find_char(' ');
2649
const String param_name = tag.substr(tag_end + 1).lstrip(" ");
2650
2651
// Use monospace font with translucent background color to make code easier to distinguish from other text.
2652
p_rt->push_font(doc_code_font);
2653
p_rt->push_font_size(doc_code_font_size);
2654
p_rt->push_bgcolor(param_bg_color);
2655
p_rt->push_color(code_color);
2656
2657
p_rt->add_text(param_name);
2658
2659
p_rt->pop(); // color
2660
p_rt->pop(); // bgcolor
2661
p_rt->pop(); // font_size
2662
p_rt->pop(); // font
2663
2664
pos = brk_end + 1;
2665
} else if (tag == p_class) {
2666
// Use a bold font when class reference tags are in their own page.
2667
p_rt->push_font(doc_bold_font);
2668
p_rt->add_text(tag);
2669
p_rt->pop(); // font
2670
2671
pos = brk_end + 1;
2672
} else if (EditorHelp::has_doc(tag)) {
2673
// Use a monospace font for class reference tags such as [Node2D] or [SceneTree].
2674
2675
p_rt->push_font(doc_code_font);
2676
p_rt->push_font_size(doc_code_font_size);
2677
p_rt->push_color(type_color);
2678
p_rt->push_meta("#" + tag, RichTextLabel::META_UNDERLINE_ON_HOVER);
2679
2680
p_rt->add_text(tag);
2681
2682
p_rt->pop(); // meta
2683
p_rt->pop(); // color
2684
p_rt->pop(); // font_size
2685
p_rt->pop(); // font
2686
2687
pos = brk_end + 1;
2688
} else if (tag == "b") {
2689
// Use bold font.
2690
p_rt->push_font(doc_bold_font);
2691
2692
pos = brk_end + 1;
2693
tag_stack.push_front(tag);
2694
} else if (tag == "i") {
2695
// Use italics font.
2696
p_rt->push_font(doc_italic_font);
2697
2698
pos = brk_end + 1;
2699
tag_stack.push_front(tag);
2700
} else if (tag == "code" || tag.begins_with("code ")) {
2701
int end_pos = bbcode.find("[/code]", brk_end + 1);
2702
if (end_pos < 0) {
2703
end_pos = bbcode.length();
2704
}
2705
2706
// Use monospace font with darkened background color to make code easier to distinguish from other text.
2707
p_rt->push_font(doc_code_font);
2708
p_rt->push_font_size(doc_code_font_size);
2709
p_rt->push_bgcolor(code_bg_color);
2710
p_rt->push_color(code_color.lerp(p_owner_node->get_theme_color(SNAME("error_color"), EditorStringName(Editor)), 0.6));
2711
2712
p_rt->add_text(bbcode.substr(brk_end + 1, end_pos - (brk_end + 1)));
2713
2714
p_rt->pop(); // color
2715
p_rt->pop(); // bgcolor
2716
p_rt->pop(); // font_size
2717
p_rt->pop(); // font
2718
2719
pos = end_pos + 7; // `len("[/code]")`.
2720
} else if (tag == "codeblock" || tag.begins_with("codeblock ")) {
2721
int end_pos = bbcode.find("[/codeblock]", brk_end + 1);
2722
if (end_pos < 0) {
2723
end_pos = bbcode.length();
2724
}
2725
2726
const String codeblock_text = bbcode.substr(brk_end + 1, end_pos - (brk_end + 1)).strip_edges();
2727
2728
String codeblock_copy_text = codeblock_text;
2729
if (using_space_indent) {
2730
// Replace the code block's tab indentation with spaces.
2731
StringBuilder builder;
2732
PackedStringArray text_lines = codeblock_copy_text.split("\n");
2733
for (const String &line : text_lines) {
2734
const String stripped_line = line.dedent();
2735
const int tab_count = line.length() - stripped_line.length();
2736
2737
if (builder.num_strings_appended() > 0) {
2738
builder.append("\n");
2739
}
2740
if (tab_count > 0) {
2741
builder.append(String(" ").repeat(tab_count * indent_size) + stripped_line);
2742
} else {
2743
builder.append(line);
2744
}
2745
}
2746
codeblock_copy_text = builder.as_string();
2747
}
2748
2749
String lang;
2750
const PackedStringArray args = tag.trim_prefix("codeblock").split(" ", false);
2751
for (int i = args.size() - 1; i >= 0; i--) {
2752
if (args[i].begins_with("lang=")) {
2753
lang = args[i].trim_prefix("lang=");
2754
break;
2755
}
2756
}
2757
2758
// Use monospace font with darkened background color to make code easier to distinguish from other text.
2759
// Use a single-column table with cell row background color instead of `[bgcolor]`.
2760
// This makes the background color highlight cover the entire block, rather than individual lines.
2761
p_rt->push_font(doc_code_font);
2762
p_rt->push_font_size(doc_code_font_size);
2763
p_rt->push_table(2);
2764
2765
p_rt->push_cell();
2766
p_rt->set_cell_row_background_color(code_bg_color, Color(code_bg_color, 0.99));
2767
p_rt->set_cell_padding(Rect2(10 * EDSCALE, 10 * EDSCALE, 10 * EDSCALE, 10 * EDSCALE));
2768
p_rt->push_color(code_dark_color);
2769
2770
bool codeblock_printed = false;
2771
2772
#ifdef MODULE_GDSCRIPT_ENABLED
2773
if (!codeblock_printed && (lang.is_empty() || lang == "gdscript")) {
2774
EditorHelpHighlighter::get_singleton()->highlight(p_rt, EditorHelpHighlighter::LANGUAGE_GDSCRIPT, codeblock_text, is_native);
2775
codeblock_printed = true;
2776
}
2777
#endif
2778
2779
#ifdef MODULE_MONO_ENABLED
2780
if (!codeblock_printed && lang == "csharp") {
2781
EditorHelpHighlighter::get_singleton()->highlight(p_rt, EditorHelpHighlighter::LANGUAGE_CSHARP, codeblock_text, is_native);
2782
codeblock_printed = true;
2783
}
2784
#endif
2785
2786
if (!codeblock_printed) {
2787
p_rt->add_text(codeblock_text);
2788
codeblock_printed = true;
2789
}
2790
2791
p_rt->pop(); // color
2792
p_rt->pop(); // cell
2793
2794
// Copy codeblock button.
2795
p_rt->push_cell();
2796
p_rt->set_cell_row_background_color(code_bg_color, Color(code_bg_color, 0.99));
2797
p_rt->set_cell_padding(Rect2(0, 10 * EDSCALE, 0, 10 * EDSCALE));
2798
p_rt->set_cell_size_override(Vector2(1, 1), Vector2(10, 10) * EDSCALE);
2799
p_rt->push_meta("^" + codeblock_copy_text, RichTextLabel::META_UNDERLINE_ON_HOVER);
2800
p_rt->add_image(p_owner_node->get_editor_theme_icon(SNAME("ActionCopy")), 24 * EDSCALE, 24 * EDSCALE, Color(link_property_color, 0.3), INLINE_ALIGNMENT_BOTTOM_TO, Rect2(), Variant(), false, TTR("Click to copy."));
2801
p_rt->pop(); // meta
2802
p_rt->pop(); // cell
2803
2804
p_rt->pop(); // table
2805
p_rt->pop(); // font_size
2806
p_rt->pop(); // font
2807
2808
pos = end_pos + 12; // `len("[/codeblock]")`.
2809
2810
// Compensate for `\n` removed before the loop.
2811
if (pos < bbcode.length()) {
2812
p_rt->add_newline();
2813
}
2814
} else if (tag == "kbd") {
2815
int end_pos = bbcode.find("[/kbd]", brk_end + 1);
2816
if (end_pos < 0) {
2817
end_pos = bbcode.length();
2818
}
2819
2820
// Use keyboard font with custom color and background color.
2821
p_rt->push_font(doc_kbd_font);
2822
p_rt->push_font_size(doc_kbd_font_size);
2823
p_rt->push_bgcolor(kbd_bg_color);
2824
p_rt->push_color(kbd_color);
2825
2826
p_rt->add_text(bbcode.substr(brk_end + 1, end_pos - (brk_end + 1)));
2827
2828
p_rt->pop(); // color
2829
p_rt->pop(); // bgcolor
2830
p_rt->pop(); // font_size
2831
p_rt->pop(); // font
2832
2833
pos = end_pos + 6; // `len("[/kbd]")`.
2834
} else if (tag == "center") {
2835
// Align to center.
2836
p_rt->push_paragraph(HORIZONTAL_ALIGNMENT_CENTER, Control::TEXT_DIRECTION_AUTO, "");
2837
pos = brk_end + 1;
2838
tag_stack.push_front(tag);
2839
} else if (tag == "br") {
2840
// Force a line break.
2841
p_rt->add_newline();
2842
pos = brk_end + 1;
2843
} else if (tag == "u") {
2844
// Use underline.
2845
p_rt->push_underline();
2846
pos = brk_end + 1;
2847
tag_stack.push_front(tag);
2848
} else if (tag == "s") {
2849
// Use strikethrough.
2850
p_rt->push_strikethrough();
2851
pos = brk_end + 1;
2852
tag_stack.push_front(tag);
2853
} else if (tag == "lb") {
2854
p_rt->add_text("[");
2855
pos = brk_end + 1;
2856
} else if (tag == "rb") {
2857
p_rt->add_text("]");
2858
pos = brk_end + 1;
2859
} else if (tag == "url" || tag.begins_with("url=")) {
2860
String url;
2861
if (tag.begins_with("url=")) {
2862
url = tag.substr(4);
2863
} else {
2864
int end = bbcode.find_char('[', brk_end);
2865
if (end == -1) {
2866
end = bbcode.length();
2867
}
2868
url = bbcode.substr(brk_end + 1, end - brk_end - 1);
2869
}
2870
2871
p_rt->push_color(link_color);
2872
p_rt->push_meta(url, RichTextLabel::META_UNDERLINE_ON_HOVER, url + "\n\n" + TTR("Click to open in browser."));
2873
2874
pos = brk_end + 1;
2875
tag_stack.push_front("url");
2876
} else if (tag.begins_with("img")) {
2877
int width = 0;
2878
int height = 0;
2879
bool size_in_percent = false;
2880
if (tag.length() > 4) {
2881
Vector<String> subtags = tag.substr(4).split(" ");
2882
HashMap<String, String> bbcode_options;
2883
for (int i = 0; i < subtags.size(); i++) {
2884
const String &expr = subtags[i];
2885
int value_pos = expr.find_char('=');
2886
if (value_pos > -1) {
2887
bbcode_options[expr.left(value_pos)] = expr.substr(value_pos + 1).unquote();
2888
}
2889
}
2890
HashMap<String, String>::Iterator width_option = bbcode_options.find("width");
2891
if (width_option) {
2892
width = width_option->value.to_int();
2893
if (width_option->value.ends_with("%")) {
2894
size_in_percent = true;
2895
}
2896
}
2897
2898
HashMap<String, String>::Iterator height_option = bbcode_options.find("height");
2899
if (height_option) {
2900
height = height_option->value.to_int();
2901
if (height_option->value.ends_with("%")) {
2902
size_in_percent = true;
2903
}
2904
}
2905
}
2906
int end = bbcode.find_char('[', brk_end);
2907
if (end == -1) {
2908
end = bbcode.length();
2909
}
2910
2911
String image_path = bbcode.substr(brk_end + 1, end - brk_end - 1);
2912
p_rt->add_image(ResourceLoader::load(image_path, "Texture2D"), width, height, Color(1, 1, 1), INLINE_ALIGNMENT_CENTER, Rect2(), Variant(), false, String(), size_in_percent);
2913
2914
pos = end;
2915
tag_stack.push_front("img");
2916
} else if (tag.begins_with("color=")) {
2917
String col = tag.substr(6);
2918
Color color = Color::from_string(col, Color());
2919
p_rt->push_color(color);
2920
2921
pos = brk_end + 1;
2922
tag_stack.push_front("color");
2923
} else if (tag.begins_with("font=")) {
2924
String font_path = tag.substr(5);
2925
Ref<Font> font = ResourceLoader::load(font_path, "Font");
2926
if (font.is_valid()) {
2927
p_rt->push_font(font);
2928
} else {
2929
p_rt->push_font(doc_font);
2930
}
2931
2932
pos = brk_end + 1;
2933
tag_stack.push_front("font");
2934
} else {
2935
p_rt->add_text("["); // Ignore.
2936
pos = brk_pos + 1;
2937
}
2938
}
2939
2940
// Close unclosed tags.
2941
for (const String &tag : tag_stack) {
2942
if (tag != "img") {
2943
p_rt->pop();
2944
}
2945
}
2946
}
2947
2948
void EditorHelp::_add_text(const String &p_bbcode) {
2949
_add_text_to_rt(p_bbcode, class_desc, this, edited_class);
2950
}
2951
2952
void EditorHelp::_wait_for_thread(Thread &p_thread) {
2953
if (p_thread.is_started()) {
2954
p_thread.wait_to_finish();
2955
}
2956
}
2957
2958
void EditorHelp::_compute_doc_version_hash() {
2959
uint32_t version_hash = Engine::get_singleton()->get_version_info().hash();
2960
doc_version_hash = vformat("%d/%d/%d/%s", version_hash, ClassDB::get_api_hash(ClassDB::API_CORE), ClassDB::get_api_hash(ClassDB::API_EDITOR), _doc_data_hash);
2961
}
2962
2963
String EditorHelp::get_cache_full_path() {
2964
return EditorPaths::get_singleton()->get_cache_dir().path_join(vformat("editor_doc_cache-%d.%d.res", GODOT_VERSION_MAJOR, GODOT_VERSION_MINOR));
2965
}
2966
2967
String EditorHelp::get_script_doc_cache_full_path() {
2968
return EditorPaths::get_singleton()->get_project_settings_dir().path_join("editor_script_doc_cache.res");
2969
}
2970
2971
DocTools *EditorHelp::get_doc_data() {
2972
_wait_for_thread();
2973
return doc;
2974
}
2975
2976
bool EditorHelp::has_doc(const String &p_class_name) {
2977
return get_doc(p_class_name) != nullptr;
2978
}
2979
2980
DocData::ClassDoc *EditorHelp::get_doc(const String &p_class_name) {
2981
return get_doc_data()->class_list.getptr(p_class_name);
2982
}
2983
2984
void EditorHelp::add_doc(const DocData::ClassDoc &p_class_doc) {
2985
if (!_script_docs_loaded.is_set()) {
2986
_docs_to_add.push_back(p_class_doc);
2987
return;
2988
}
2989
2990
get_doc_data()->add_doc(p_class_doc);
2991
}
2992
2993
void EditorHelp::remove_doc(const String &p_class_name) {
2994
if (!_script_docs_loaded.is_set()) {
2995
_docs_to_remove.push_back(p_class_name);
2996
return;
2997
}
2998
2999
DocTools *dt = get_doc_data();
3000
if (dt->has_doc(p_class_name)) {
3001
dt->remove_doc(p_class_name);
3002
}
3003
}
3004
3005
void EditorHelp::remove_script_doc_by_path(const String &p_path) {
3006
if (!_script_docs_loaded.is_set()) {
3007
_docs_to_remove_by_path.push_back(p_path);
3008
return;
3009
}
3010
get_doc_data()->remove_script_doc_by_path(p_path);
3011
}
3012
3013
void EditorHelp::load_xml_buffer(const uint8_t *p_buffer, int p_size) {
3014
if (!ext_doc) {
3015
ext_doc = memnew(DocTools);
3016
}
3017
3018
ext_doc->load_xml(p_buffer, p_size);
3019
3020
if (doc) {
3021
doc->load_xml(p_buffer, p_size);
3022
}
3023
}
3024
3025
void EditorHelp::remove_class(const String &p_class) {
3026
if (ext_doc && ext_doc->has_doc(p_class)) {
3027
ext_doc->remove_doc(p_class);
3028
}
3029
3030
if (doc && doc->has_doc(p_class)) {
3031
remove_doc(p_class);
3032
}
3033
}
3034
3035
void EditorHelp::_load_doc_thread(void *p_udata) {
3036
bool use_script_cache = (bool)p_udata;
3037
Ref<Resource> cache_res = ResourceLoader::load(get_cache_full_path());
3038
if (cache_res.is_valid() && cache_res->get_meta("version_hash", "") == doc_version_hash) {
3039
Array classes = cache_res->get_meta("classes", Array());
3040
for (int i = 0; i < classes.size(); i++) {
3041
doc->add_doc(DocData::ClassDoc::from_dict(classes[i]));
3042
}
3043
if (use_script_cache) {
3044
callable_mp_static(&EditorHelp::load_script_doc_cache).call_deferred();
3045
}
3046
// Extensions' docs are not cached. Generate them now (on the main thread).
3047
callable_mp_static(&EditorHelp::_gen_extensions_docs).call_deferred();
3048
} else {
3049
// We have to go back to the main thread to start from scratch, bypassing any possibly existing cache.
3050
callable_mp_static(&EditorHelp::generate_doc).call_deferred(false, use_script_cache);
3051
}
3052
3053
OS::get_singleton()->benchmark_end_measure("EditorHelp", vformat("Generate Documentation (Run %d)", doc_generation_count));
3054
}
3055
3056
void EditorHelp::_gen_doc_thread(void *p_udata) {
3057
DocTools compdoc;
3058
compdoc.load_compressed(_doc_data_compressed, _doc_data_compressed_size, _doc_data_uncompressed_size);
3059
doc->merge_from(compdoc); // Ensure all is up to date.
3060
3061
Ref<Resource> cache_res;
3062
cache_res.instantiate();
3063
cache_res->set_meta("version_hash", doc_version_hash);
3064
Array classes;
3065
for (const KeyValue<String, DocData::ClassDoc> &E : doc->class_list) {
3066
if (ClassDB::class_exists(E.value.name)) {
3067
ClassDB::APIType api = ClassDB::get_api_type(E.value.name);
3068
if (api == ClassDB::API_EXTENSION || api == ClassDB::API_EDITOR_EXTENSION) {
3069
continue;
3070
}
3071
}
3072
classes.push_back(DocData::ClassDoc::to_dict(E.value));
3073
}
3074
cache_res->set_meta("classes", classes);
3075
Error err = ResourceSaver::save(cache_res, get_cache_full_path(), ResourceSaver::FLAG_COMPRESS);
3076
if (err) {
3077
ERR_PRINT("Cannot save editor help cache (" + get_cache_full_path() + ").");
3078
}
3079
3080
// Load script docs after native ones are cached so native cache doesn't contain script docs.
3081
bool use_script_cache = (bool)p_udata;
3082
if (use_script_cache) {
3083
callable_mp_static(&EditorHelp::load_script_doc_cache).call_deferred();
3084
}
3085
3086
OS::get_singleton()->benchmark_end_measure("EditorHelp", vformat("Generate Documentation (Run %d)", doc_generation_count));
3087
}
3088
3089
void EditorHelp::_gen_extensions_docs() {
3090
doc->generate((DocTools::GENERATE_FLAG_SKIP_BASIC_TYPES | DocTools::GENERATE_FLAG_EXTENSION_CLASSES_ONLY));
3091
3092
// Append extra doc data, as it gets overridden by the generation step.
3093
if (ext_doc) {
3094
doc->merge_from(*ext_doc);
3095
}
3096
}
3097
static void _load_script_doc_cache(bool p_changes) {
3098
EditorHelp::load_script_doc_cache();
3099
}
3100
3101
void EditorHelp::load_script_doc_cache() {
3102
if (!ProjectSettings::get_singleton()->is_project_loaded()) {
3103
print_verbose("Skipping loading script doc cache since no project is open.");
3104
return;
3105
}
3106
3107
if (EditorNode::is_cmdline_mode()) {
3108
return;
3109
}
3110
3111
_wait_for_thread();
3112
3113
if (!ResourceLoader::exists(get_script_doc_cache_full_path())) {
3114
print_verbose("Script documentation cache not found. Regenerating it may take a while for projects with many scripts.");
3115
regenerate_script_doc_cache();
3116
return;
3117
}
3118
3119
if (EditorFileSystem::get_singleton()->is_scanning()) {
3120
// This is assuming EditorFileSystem is performing first scan. We must wait until it is done.
3121
EditorFileSystem::get_singleton()->connect(SNAME("sources_changed"), callable_mp_static(_load_script_doc_cache), CONNECT_ONE_SHOT);
3122
return;
3123
}
3124
3125
worker_thread.start(_load_script_doc_cache_thread, nullptr);
3126
}
3127
3128
void EditorHelp::_process_postponed_docs() {
3129
for (const String &class_name : _docs_to_remove) {
3130
doc->remove_doc(class_name);
3131
}
3132
for (const String &path : _docs_to_remove_by_path) {
3133
doc->remove_script_doc_by_path(path);
3134
}
3135
for (const DocData::ClassDoc &cd : _docs_to_add) {
3136
doc->add_doc(cd);
3137
}
3138
_docs_to_add.clear();
3139
_docs_to_remove.clear();
3140
_docs_to_remove_by_path.clear();
3141
}
3142
3143
void EditorHelp::_load_script_doc_cache_thread(void *p_udata) {
3144
ERR_FAIL_COND_MSG(!ProjectSettings::get_singleton()->is_project_loaded(), "Error: cannot load script doc cache without a project.");
3145
ERR_FAIL_COND_MSG(!ResourceLoader::exists(get_script_doc_cache_full_path()), "Error: cannot load script doc cache from inexistent file.");
3146
3147
Ref<Resource> script_doc_cache_res = ResourceLoader::load(get_script_doc_cache_full_path(), "", ResourceFormatLoader::CACHE_MODE_IGNORE);
3148
if (script_doc_cache_res.is_null()) {
3149
print_verbose("Script doc cache is corrupted. Regenerating it instead.");
3150
_delete_script_doc_cache();
3151
callable_mp_static(EditorHelp::regenerate_script_doc_cache).call_deferred();
3152
return;
3153
}
3154
3155
Array classes = script_doc_cache_res->get_meta("classes", Array());
3156
for (const Dictionary dict : classes) {
3157
doc->add_doc(DocData::ClassDoc::from_dict(dict));
3158
}
3159
3160
// Protect from race condition in other threads reading / this thread writing to _docs_to_add/remove/etc.
3161
_script_docs_loaded.set();
3162
3163
// Deal with docs likely added from EditorFileSystem's scans while the cache was loading in EditorHelp::worker_thread.
3164
_process_postponed_docs();
3165
3166
// Always delete the doc cache after successful load since most uses of editor will change a script, invalidating cache.
3167
_delete_script_doc_cache();
3168
}
3169
3170
// Helper method to deal with "sources_changed" signal having a parameter.
3171
static void _regenerate_script_doc_cache(bool p_changes) {
3172
EditorHelp::regenerate_script_doc_cache();
3173
}
3174
3175
void EditorHelp::regenerate_script_doc_cache() {
3176
if (EditorFileSystem::get_singleton()->is_scanning()) {
3177
// Wait until EditorFileSystem scanning is complete to use updated filesystem structure.
3178
EditorFileSystem::get_singleton()->connect(SNAME("sources_changed"), callable_mp_static(_regenerate_script_doc_cache), CONNECT_ONE_SHOT);
3179
return;
3180
}
3181
3182
_wait_for_thread(worker_thread);
3183
_wait_for_thread(loader_thread);
3184
loader_thread.start(_regen_script_doc_thread, EditorFileSystem::get_singleton()->get_filesystem());
3185
}
3186
3187
// Runs on worker_thread since it writes to DocData.
3188
void EditorHelp::_finish_regen_script_doc_thread(void *p_udata) {
3189
loader_thread.wait_to_finish();
3190
_process_postponed_docs();
3191
_script_docs_loaded.set();
3192
3193
OS::get_singleton()->benchmark_end_measure("EditorHelp", "Generate Script Documentation");
3194
}
3195
3196
// Runs on loader_thread since _reload_scripts_documentation calls ResourceLoader::load().
3197
// Avoids deadlocks of worker_thread needing main thread for load task dispatching, but main thread waiting on worker_thread.
3198
void EditorHelp::_regen_script_doc_thread(void *p_udata) {
3199
OS::get_singleton()->benchmark_begin_measure("EditorHelp", "Generate Script Documentation");
3200
3201
EditorFileSystemDirectory *dir = static_cast<EditorFileSystemDirectory *>(p_udata);
3202
_script_docs_loaded.set_to(false);
3203
3204
// Ignore changes from filesystem scan since script docs will be now.
3205
_docs_to_add.clear();
3206
_docs_to_remove.clear();
3207
_docs_to_remove_by_path.clear();
3208
3209
_reload_scripts_documentation(dir);
3210
3211
// All ResourceLoader::load() calls are done, so we can no longer deadlock with main thread.
3212
// Switch to back to worker_thread from loader_thread to resynchronize access to DocData.
3213
worker_thread.start(_finish_regen_script_doc_thread, nullptr);
3214
}
3215
3216
void EditorHelp::_reload_scripts_documentation(EditorFileSystemDirectory *p_dir) {
3217
// Recursively force compile all scripts, which should generate their documentation.
3218
for (int i = 0; i < p_dir->get_subdir_count(); i++) {
3219
_reload_scripts_documentation(p_dir->get_subdir(i));
3220
}
3221
3222
for (int i = 0; i < p_dir->get_file_count(); i++) {
3223
if (ClassDB::is_parent_class(p_dir->get_file_type(i), SNAME("Script"))) {
3224
Ref<Script> scr = ResourceLoader::load(p_dir->get_file_path(i));
3225
if (scr.is_valid()) {
3226
for (const DocData::ClassDoc &cd : scr->get_documentation()) {
3227
_docs_to_add.push_back(cd);
3228
}
3229
}
3230
}
3231
}
3232
}
3233
3234
void EditorHelp::_delete_script_doc_cache() {
3235
if (FileAccess::exists(get_script_doc_cache_full_path())) {
3236
DirAccess::remove_file_or_error(ProjectSettings::get_singleton()->globalize_path(get_script_doc_cache_full_path()));
3237
}
3238
}
3239
3240
void EditorHelp::save_script_doc_cache() {
3241
if (!_script_docs_loaded.is_set()) {
3242
print_verbose("Script docs haven't been properly loaded or regenerated, so don't save them to disk.");
3243
return;
3244
}
3245
3246
Ref<Resource> cache_res;
3247
cache_res.instantiate();
3248
Array classes;
3249
for (const KeyValue<String, DocData::ClassDoc> &E : doc->class_list) {
3250
if (E.value.is_script_doc) {
3251
classes.push_back(DocData::ClassDoc::to_dict(E.value));
3252
}
3253
}
3254
3255
cache_res->set_meta("classes", classes);
3256
Error err = ResourceSaver::save(cache_res, get_script_doc_cache_full_path(), ResourceSaver::FLAG_COMPRESS);
3257
ERR_FAIL_COND_MSG(err != OK, vformat("Cannot save script documentation cache in %s.", get_script_doc_cache_full_path()));
3258
}
3259
3260
void EditorHelp::generate_doc(bool p_use_cache, bool p_use_script_cache) {
3261
doc_generation_count++;
3262
OS::get_singleton()->benchmark_begin_measure("EditorHelp", vformat("Generate Documentation (Run %d)", doc_generation_count));
3263
3264
// In case not the first attempt.
3265
_wait_for_thread();
3266
3267
if (!doc) {
3268
doc = memnew(DocTools);
3269
}
3270
3271
if (doc_version_hash.is_empty()) {
3272
_compute_doc_version_hash();
3273
}
3274
3275
if (p_use_cache && FileAccess::exists(get_cache_full_path())) {
3276
worker_thread.start(_load_doc_thread, (void *)p_use_script_cache);
3277
} else {
3278
print_verbose("Regenerating editor help cache");
3279
doc->generate();
3280
worker_thread.start(_gen_doc_thread, (void *)p_use_script_cache);
3281
}
3282
}
3283
3284
void EditorHelp::_toggle_files_pressed() {
3285
ScriptEditor::get_singleton()->toggle_files_panel();
3286
update_toggle_files_button();
3287
}
3288
3289
void EditorHelp::_notification(int p_what) {
3290
switch (p_what) {
3291
case NOTIFICATION_POSTINITIALIZE: {
3292
// Requires theme to be up to date.
3293
_class_desc_resized(false);
3294
} break;
3295
3296
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
3297
bool need_update = false;
3298
if (EditorSettings::get_singleton()->check_changed_settings_in_group("text_editor/help")) {
3299
need_update = true;
3300
}
3301
#if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED)
3302
if (!need_update && EditorSettings::get_singleton()->check_changed_settings_in_group("text_editor/theme/highlighting")) {
3303
need_update = true;
3304
}
3305
#endif
3306
if (!need_update) {
3307
break;
3308
}
3309
[[fallthrough]];
3310
}
3311
case NOTIFICATION_READY: {
3312
_wait_for_thread();
3313
_update_doc();
3314
} break;
3315
3316
case NOTIFICATION_THEME_CHANGED: {
3317
if (is_inside_tree()) {
3318
_class_desc_resized(true);
3319
}
3320
update_toggle_files_button();
3321
} break;
3322
3323
case NOTIFICATION_VISIBILITY_CHANGED: {
3324
if (update_pending && is_visible_in_tree()) {
3325
_update_doc();
3326
}
3327
update_toggle_files_button();
3328
} break;
3329
3330
case NOTIFICATION_TRANSLATION_CHANGED: {
3331
if (!is_ready()) {
3332
break;
3333
}
3334
3335
if (is_visible_in_tree()) {
3336
_update_doc();
3337
} else {
3338
update_pending = true;
3339
}
3340
[[fallthrough]];
3341
}
3342
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: {
3343
update_toggle_files_button();
3344
} break;
3345
}
3346
}
3347
3348
void EditorHelp::go_to_help(const String &p_help) {
3349
_wait_for_thread();
3350
_help_callback(p_help);
3351
}
3352
3353
void EditorHelp::go_to_class(const String &p_class) {
3354
_wait_for_thread();
3355
_goto_desc(p_class);
3356
}
3357
3358
void EditorHelp::update_doc() {
3359
_wait_for_thread();
3360
ERR_FAIL_COND(!doc->class_list.has(edited_class));
3361
ERR_FAIL_COND(!doc->class_list[edited_class].is_script_doc);
3362
_update_doc();
3363
}
3364
3365
void EditorHelp::cleanup_doc() {
3366
_wait_for_thread();
3367
memdelete(doc);
3368
doc = nullptr;
3369
}
3370
3371
Vector<Pair<String, int>> EditorHelp::get_sections() {
3372
_wait_for_thread();
3373
Vector<Pair<String, int>> sections;
3374
3375
for (int i = 0; i < section_line.size(); i++) {
3376
sections.push_back(Pair<String, int>(section_line[i].first, i));
3377
}
3378
return sections;
3379
}
3380
3381
void EditorHelp::scroll_to_section(int p_section_index) {
3382
_wait_for_thread();
3383
int line = section_line[p_section_index].second;
3384
if (class_desc->is_finished()) {
3385
class_desc->scroll_to_paragraph(line);
3386
} else {
3387
scroll_to = line;
3388
}
3389
}
3390
3391
void EditorHelp::popup_search() {
3392
_wait_for_thread();
3393
find_bar->popup_search();
3394
}
3395
3396
String EditorHelp::get_class() {
3397
return edited_class;
3398
}
3399
3400
void EditorHelp::search_again(bool p_search_previous) {
3401
_search(p_search_previous);
3402
}
3403
3404
int EditorHelp::get_scroll() const {
3405
return class_desc->get_v_scroll_bar()->get_value();
3406
}
3407
3408
void EditorHelp::set_scroll(int p_scroll) {
3409
class_desc->get_v_scroll_bar()->set_value(p_scroll);
3410
}
3411
3412
void EditorHelp::update_toggle_files_button() {
3413
if (is_layout_rtl()) {
3414
toggle_files_button->set_button_icon(get_editor_theme_icon(ScriptEditor::get_singleton()->is_files_panel_toggled() ? SNAME("Forward") : SNAME("Back")));
3415
} else {
3416
toggle_files_button->set_button_icon(get_editor_theme_icon(ScriptEditor::get_singleton()->is_files_panel_toggled() ? SNAME("Back") : SNAME("Forward")));
3417
}
3418
toggle_files_button->set_tooltip_text(vformat("%s (%s)", TTR("Toggle Files Panel"), ED_GET_SHORTCUT("script_editor/toggle_files_panel")->get_as_text()));
3419
}
3420
3421
void EditorHelp::_bind_methods() {
3422
ClassDB::bind_method("_class_list_select", &EditorHelp::_class_list_select);
3423
ClassDB::bind_method("_request_help", &EditorHelp::_request_help);
3424
ClassDB::bind_method("_search", &EditorHelp::_search);
3425
ClassDB::bind_method("_help_callback", &EditorHelp::_help_callback);
3426
3427
ADD_SIGNAL(MethodInfo("go_to_help"));
3428
ADD_SIGNAL(MethodInfo("request_save_history"));
3429
}
3430
3431
void EditorHelp::init_gdext_pointers() {
3432
GDExtensionEditorHelp::editor_help_load_xml_buffer = &EditorHelp::load_xml_buffer;
3433
GDExtensionEditorHelp::editor_help_remove_class = &EditorHelp::remove_class;
3434
}
3435
3436
EditorHelp::EditorHelp() {
3437
set_custom_minimum_size(Size2(150 * EDSCALE, 0));
3438
3439
class_desc = memnew(RichTextLabel);
3440
class_desc->set_tab_size(8);
3441
add_child(class_desc);
3442
class_desc->set_threaded(true);
3443
class_desc->set_v_size_flags(SIZE_EXPAND_FILL);
3444
3445
class_desc->connect(SceneStringName(finished), callable_mp(this, &EditorHelp::_class_desc_finished));
3446
class_desc->connect("meta_clicked", callable_mp(this, &EditorHelp::_class_desc_select));
3447
class_desc->connect(SceneStringName(gui_input), callable_mp(this, &EditorHelp::_class_desc_input));
3448
class_desc->connect(SceneStringName(resized), callable_mp(this, &EditorHelp::_class_desc_resized).bind(false));
3449
3450
// Added second so it opens at the bottom so it won't offset the entire widget.
3451
find_bar = memnew(FindBar);
3452
add_child(find_bar);
3453
find_bar->hide();
3454
find_bar->set_rich_text_label(class_desc);
3455
3456
status_bar = memnew(HBoxContainer);
3457
add_child(status_bar);
3458
status_bar->set_h_size_flags(SIZE_EXPAND_FILL);
3459
status_bar->set_custom_minimum_size(Size2(0, 24 * EDSCALE));
3460
3461
toggle_files_button = memnew(Button);
3462
toggle_files_button->set_theme_type_variation(SceneStringName(FlatButton));
3463
toggle_files_button->set_accessibility_name(TTRC("Scripts"));
3464
toggle_files_button->set_tooltip_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
3465
toggle_files_button->connect(SceneStringName(pressed), callable_mp(this, &EditorHelp::_toggle_files_pressed));
3466
status_bar->add_child(toggle_files_button);
3467
3468
class_desc->set_selection_enabled(true);
3469
class_desc->set_context_menu_enabled(true);
3470
class_desc->set_selection_modifier(callable_mp_static(_replace_nbsp_with_space));
3471
3472
class_desc->hide();
3473
}
3474
3475
/// EditorHelpBit ///
3476
3477
#define HANDLE_DOC(m_string) ((is_native ? DTR(m_string) : (m_string)).strip_edges())
3478
3479
EditorHelpBit::HelpData EditorHelpBit::_get_class_help_data(const StringName &p_class_name) {
3480
if (doc_class_cache.has(p_class_name)) {
3481
return doc_class_cache[p_class_name];
3482
}
3483
3484
HelpData result;
3485
3486
const DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
3487
if (class_doc) {
3488
// Non-native class shouldn't be cached, nor translated.
3489
const bool is_native = !class_doc->is_script_doc;
3490
3491
const String brief_description = HANDLE_DOC(class_doc->brief_description);
3492
const String long_description = HANDLE_DOC(class_doc->description);
3493
3494
if (!brief_description.is_empty()) {
3495
result.description += "[b]" + brief_description + "[/b]";
3496
}
3497
if (!long_description.is_empty()) {
3498
if (!result.description.is_empty()) {
3499
result.description += '\n';
3500
}
3501
result.description += long_description;
3502
}
3503
if (class_doc->is_deprecated) {
3504
if (class_doc->deprecated_message.is_empty()) {
3505
result.deprecated_message = TTR("This class may be changed or removed in future versions.");
3506
} else {
3507
result.deprecated_message = HANDLE_DOC(class_doc->deprecated_message);
3508
}
3509
}
3510
if (class_doc->is_experimental) {
3511
if (class_doc->experimental_message.is_empty()) {
3512
result.experimental_message = TTR("This class may be changed or removed in future versions.");
3513
} else {
3514
result.experimental_message = HANDLE_DOC(class_doc->experimental_message);
3515
}
3516
}
3517
3518
if (is_native) {
3519
doc_class_cache[p_class_name] = result;
3520
}
3521
}
3522
3523
return result;
3524
}
3525
3526
EditorHelpBit::HelpData EditorHelpBit::_get_enum_help_data(const StringName &p_class_name, const StringName &p_enum_name) {
3527
if (doc_enum_cache.has(p_class_name) && doc_enum_cache[p_class_name].has(p_enum_name)) {
3528
return doc_enum_cache[p_class_name][p_enum_name];
3529
}
3530
3531
HelpData result;
3532
3533
const DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
3534
if (class_doc) {
3535
// Non-native enums shouldn't be cached, nor translated.
3536
const bool is_native = !class_doc->is_script_doc;
3537
3538
for (const KeyValue<String, DocData::EnumDoc> &kv : class_doc->enums) {
3539
const StringName enum_name = kv.key;
3540
const DocData::EnumDoc &enum_doc = kv.value;
3541
3542
HelpData current;
3543
current.description = HANDLE_DOC(enum_doc.description);
3544
if (enum_doc.is_deprecated) {
3545
if (enum_doc.deprecated_message.is_empty()) {
3546
current.deprecated_message = TTR("This enumeration may be changed or removed in future versions.");
3547
} else {
3548
current.deprecated_message = HANDLE_DOC(enum_doc.deprecated_message);
3549
}
3550
}
3551
if (enum_doc.is_experimental) {
3552
if (enum_doc.experimental_message.is_empty()) {
3553
current.experimental_message = TTR("This enumeration may be changed or removed in future versions.");
3554
} else {
3555
current.experimental_message = HANDLE_DOC(enum_doc.experimental_message);
3556
}
3557
}
3558
3559
if (enum_name == p_enum_name) {
3560
result = current;
3561
3562
if (!is_native) {
3563
break;
3564
}
3565
}
3566
3567
if (is_native) {
3568
doc_enum_cache[p_class_name][enum_name] = current;
3569
}
3570
}
3571
}
3572
3573
return result;
3574
}
3575
3576
EditorHelpBit::HelpData EditorHelpBit::_get_constant_help_data(const StringName &p_class_name, const StringName &p_constant_name) {
3577
if (doc_constant_cache.has(p_class_name) && doc_constant_cache[p_class_name].has(p_constant_name)) {
3578
return doc_constant_cache[p_class_name][p_constant_name];
3579
}
3580
3581
HelpData result;
3582
3583
const DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
3584
if (class_doc) {
3585
// Non-native constants shouldn't be cached, nor translated.
3586
const bool is_native = !class_doc->is_script_doc;
3587
3588
for (const DocData::ConstantDoc &constant : class_doc->constants) {
3589
HelpData current;
3590
current.description = HANDLE_DOC(constant.description);
3591
if (constant.is_deprecated) {
3592
if (constant.deprecated_message.is_empty()) {
3593
current.deprecated_message = TTR("This constant may be changed or removed in future versions.");
3594
} else {
3595
current.deprecated_message = HANDLE_DOC(constant.deprecated_message);
3596
}
3597
}
3598
if (constant.is_experimental) {
3599
if (constant.experimental_message.is_empty()) {
3600
current.experimental_message = TTR("This constant may be changed or removed in future versions.");
3601
} else {
3602
current.experimental_message = HANDLE_DOC(constant.experimental_message);
3603
}
3604
}
3605
current.doc_type = { constant.type, constant.enumeration, constant.is_bitfield };
3606
if (constant.is_value_valid) {
3607
current.value = constant.value;
3608
}
3609
3610
if (constant.name == p_constant_name) {
3611
result = current;
3612
3613
if (!is_native) {
3614
break;
3615
}
3616
}
3617
3618
if (is_native) {
3619
doc_constant_cache[p_class_name][constant.name] = current;
3620
}
3621
}
3622
}
3623
3624
return result;
3625
}
3626
3627
EditorHelpBit::HelpData EditorHelpBit::_get_property_help_data(const StringName &p_class_name, const StringName &p_property_name) {
3628
if (doc_property_cache.has(p_class_name) && doc_property_cache[p_class_name].has(p_property_name)) {
3629
return doc_property_cache[p_class_name][p_property_name];
3630
}
3631
3632
HelpData result;
3633
3634
const DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
3635
if (class_doc) {
3636
// Non-native properties shouldn't be cached, nor translated.
3637
const bool is_native = !class_doc->is_script_doc;
3638
3639
for (const DocData::PropertyDoc &property : class_doc->properties) {
3640
HelpData current;
3641
current.description = HANDLE_DOC(property.description);
3642
if (property.is_deprecated) {
3643
if (property.deprecated_message.is_empty()) {
3644
current.deprecated_message = TTR("This property may be changed or removed in future versions.");
3645
} else {
3646
current.deprecated_message = HANDLE_DOC(property.deprecated_message);
3647
}
3648
}
3649
if (property.is_experimental) {
3650
if (property.experimental_message.is_empty()) {
3651
current.experimental_message = TTR("This property may be changed or removed in future versions.");
3652
} else {
3653
current.experimental_message = HANDLE_DOC(property.experimental_message);
3654
}
3655
}
3656
current.doc_type = { property.type, property.enumeration, property.is_bitfield };
3657
current.value = property.default_value;
3658
3659
String enum_class_name;
3660
String enum_name;
3661
if (CoreConstants::is_global_enum(property.enumeration)) {
3662
enum_class_name = "@GlobalScope";
3663
enum_name = property.enumeration;
3664
} else {
3665
const int dot_pos = property.enumeration.rfind_char('.');
3666
if (dot_pos >= 0) {
3667
enum_class_name = property.enumeration.left(dot_pos);
3668
enum_name = property.enumeration.substr(dot_pos + 1);
3669
}
3670
}
3671
3672
if (!enum_class_name.is_empty() && !enum_name.is_empty()) {
3673
// Classes can use enums from other classes, so check from which it came.
3674
const DocData::ClassDoc *enum_class = EditorHelp::get_doc(enum_class_name);
3675
if (enum_class) {
3676
const String enum_prefix = EditorPropertyNameProcessor::get_singleton()->process_name(enum_name, EditorPropertyNameProcessor::STYLE_CAPITALIZED) + " ";
3677
for (DocData::ConstantDoc constant : enum_class->constants) {
3678
// Don't display `_MAX` enum value descriptions, as these are never exposed in the inspector.
3679
if (constant.enumeration == enum_name && !constant.name.ends_with("_MAX")) {
3680
// Prettify the enum value display, so that "<ENUM_NAME>_<ITEM>" becomes "Item".
3681
const String item_name = EditorPropertyNameProcessor::get_singleton()->process_name(constant.name, EditorPropertyNameProcessor::STYLE_CAPITALIZED).trim_prefix(enum_prefix);
3682
String item_descr = HANDLE_DOC(constant.description);
3683
if (item_descr.is_empty()) {
3684
item_descr = "[color=<EditorHelpBitCommentColor>][i]" + TTR("No description available.") + "[/i][/color]";
3685
}
3686
current.description += vformat("\n[b]%s:[/b] %s", item_name, item_descr);
3687
}
3688
}
3689
current.description = current.description.lstrip("\n");
3690
}
3691
}
3692
3693
if (property.name == p_property_name) {
3694
result = current;
3695
3696
if (!is_native) {
3697
break;
3698
}
3699
}
3700
3701
if (is_native) {
3702
doc_property_cache[p_class_name][property.name] = current;
3703
}
3704
}
3705
}
3706
3707
return result;
3708
}
3709
3710
EditorHelpBit::HelpData EditorHelpBit::_get_theme_item_help_data(const StringName &p_class_name, const StringName &p_theme_item_name) {
3711
if (doc_theme_item_cache.has(p_class_name) && doc_theme_item_cache[p_class_name].has(p_theme_item_name)) {
3712
return doc_theme_item_cache[p_class_name][p_theme_item_name];
3713
}
3714
3715
HelpData result;
3716
3717
bool found = false;
3718
DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
3719
while (class_doc) {
3720
// Non-native theme items shouldn't be cached, nor translated.
3721
const bool is_native = !class_doc->is_script_doc;
3722
3723
for (const DocData::ThemeItemDoc &theme_item : class_doc->theme_properties) {
3724
HelpData current;
3725
current.description = HANDLE_DOC(theme_item.description);
3726
if (theme_item.is_deprecated) {
3727
if (theme_item.deprecated_message.is_empty()) {
3728
current.deprecated_message = TTR("This theme property may be changed or removed in future versions.");
3729
} else {
3730
current.deprecated_message = HANDLE_DOC(theme_item.deprecated_message);
3731
}
3732
}
3733
if (theme_item.is_experimental) {
3734
if (theme_item.experimental_message.is_empty()) {
3735
current.experimental_message = TTR("This theme property may be changed or removed in future versions.");
3736
} else {
3737
current.experimental_message = HANDLE_DOC(theme_item.experimental_message);
3738
}
3739
}
3740
current.doc_type = { theme_item.type, String(), false };
3741
current.value = theme_item.default_value;
3742
3743
if (theme_item.name == p_theme_item_name) {
3744
result = current;
3745
found = true;
3746
3747
if (!is_native) {
3748
break;
3749
}
3750
}
3751
3752
if (is_native) {
3753
doc_theme_item_cache[p_class_name][theme_item.name] = current;
3754
}
3755
}
3756
3757
if (found || class_doc->inherits.is_empty()) {
3758
break;
3759
}
3760
3761
// Check for inherited theme items.
3762
class_doc = EditorHelp::get_doc(class_doc->inherits);
3763
}
3764
3765
return result;
3766
}
3767
3768
EditorHelpBit::HelpData EditorHelpBit::_get_method_help_data(const StringName &p_class_name, const StringName &p_method_name) {
3769
if (doc_method_cache.has(p_class_name) && doc_method_cache[p_class_name].has(p_method_name)) {
3770
return doc_method_cache[p_class_name][p_method_name];
3771
}
3772
3773
HelpData result;
3774
3775
const DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
3776
if (class_doc) {
3777
// Non-native methods shouldn't be cached, nor translated.
3778
const bool is_native = !class_doc->is_script_doc;
3779
3780
for (const DocData::MethodDoc &method : class_doc->methods) {
3781
HelpData current;
3782
current.description = HANDLE_DOC(method.description);
3783
if (method.is_deprecated) {
3784
if (method.deprecated_message.is_empty()) {
3785
current.deprecated_message = TTR("This method may be changed or removed in future versions.");
3786
} else {
3787
current.deprecated_message = HANDLE_DOC(method.deprecated_message);
3788
}
3789
}
3790
if (method.is_experimental) {
3791
if (method.experimental_message.is_empty()) {
3792
current.experimental_message = TTR("This method may be changed or removed in future versions.");
3793
} else {
3794
current.experimental_message = HANDLE_DOC(method.experimental_message);
3795
}
3796
}
3797
current.doc_type = { method.return_type, method.return_enum, method.return_is_bitfield };
3798
for (const DocData::ArgumentDoc &argument : method.arguments) {
3799
const DocType argument_doc_type = { argument.type, argument.enumeration, argument.is_bitfield };
3800
current.arguments.push_back({ argument.name, argument_doc_type, argument.default_value });
3801
}
3802
current.qualifiers = method.qualifiers;
3803
const DocData::ArgumentDoc &rest_argument = method.rest_argument;
3804
const DocType rest_argument_doc_type = { rest_argument.type, rest_argument.enumeration, rest_argument.is_bitfield };
3805
current.rest_argument = { rest_argument.name, rest_argument_doc_type, rest_argument.default_value };
3806
3807
if (method.name == p_method_name) {
3808
result = current;
3809
3810
if (!is_native) {
3811
break;
3812
}
3813
}
3814
3815
if (is_native) {
3816
doc_method_cache[p_class_name][method.name] = current;
3817
}
3818
}
3819
}
3820
3821
return result;
3822
}
3823
3824
EditorHelpBit::HelpData EditorHelpBit::_get_signal_help_data(const StringName &p_class_name, const StringName &p_signal_name) {
3825
if (doc_signal_cache.has(p_class_name) && doc_signal_cache[p_class_name].has(p_signal_name)) {
3826
return doc_signal_cache[p_class_name][p_signal_name];
3827
}
3828
3829
HelpData result;
3830
3831
const DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
3832
if (class_doc) {
3833
// Non-native signals shouldn't be cached, nor translated.
3834
const bool is_native = !class_doc->is_script_doc;
3835
3836
for (const DocData::MethodDoc &signal : class_doc->signals) {
3837
HelpData current;
3838
current.description = HANDLE_DOC(signal.description);
3839
if (signal.is_deprecated) {
3840
if (signal.deprecated_message.is_empty()) {
3841
current.deprecated_message = TTR("This signal may be changed or removed in future versions.");
3842
} else {
3843
current.deprecated_message = HANDLE_DOC(signal.deprecated_message);
3844
}
3845
}
3846
if (signal.is_experimental) {
3847
if (signal.experimental_message.is_empty()) {
3848
current.experimental_message = TTR("This signal may be changed or removed in future versions.");
3849
} else {
3850
current.experimental_message = HANDLE_DOC(signal.experimental_message);
3851
}
3852
}
3853
for (const DocData::ArgumentDoc &argument : signal.arguments) {
3854
const DocType argument_type = { argument.type, argument.enumeration, argument.is_bitfield };
3855
current.arguments.push_back({ argument.name, argument_type, argument.default_value });
3856
}
3857
current.qualifiers = signal.qualifiers;
3858
3859
if (signal.name == p_signal_name) {
3860
result = current;
3861
3862
if (!is_native) {
3863
break;
3864
}
3865
}
3866
3867
if (is_native) {
3868
doc_signal_cache[p_class_name][signal.name] = current;
3869
}
3870
}
3871
}
3872
3873
return result;
3874
}
3875
3876
EditorHelpBit::HelpData EditorHelpBit::_get_annotation_help_data(const StringName &p_class_name, const StringName &p_annotation_name) {
3877
if (doc_annotation_cache.has(p_class_name) && doc_annotation_cache[p_class_name].has(p_annotation_name)) {
3878
return doc_annotation_cache[p_class_name][p_annotation_name];
3879
}
3880
3881
HelpData result;
3882
3883
const DocData::ClassDoc *class_doc = EditorHelp::get_doc(p_class_name);
3884
if (class_doc) {
3885
// Non-native annotations shouldn't be cached, nor translated.
3886
const bool is_native = !class_doc->is_script_doc;
3887
3888
for (const DocData::MethodDoc &annotation : class_doc->annotations) {
3889
HelpData current;
3890
current.description = HANDLE_DOC(annotation.description);
3891
if (annotation.is_deprecated) {
3892
if (annotation.deprecated_message.is_empty()) {
3893
current.deprecated_message = TTR("This annotation may be changed or removed in future versions.");
3894
} else {
3895
current.deprecated_message = HANDLE_DOC(annotation.deprecated_message);
3896
}
3897
}
3898
if (annotation.is_experimental) {
3899
if (annotation.experimental_message.is_empty()) {
3900
current.experimental_message = TTR("This annotation may be changed or removed in future versions.");
3901
} else {
3902
current.experimental_message = HANDLE_DOC(annotation.experimental_message);
3903
}
3904
}
3905
for (const DocData::ArgumentDoc &argument : annotation.arguments) {
3906
const DocType argument_type = { argument.type, argument.enumeration, argument.is_bitfield };
3907
current.arguments.push_back({ argument.name, argument_type, argument.default_value });
3908
}
3909
current.qualifiers = annotation.qualifiers;
3910
3911
if (annotation.name == p_annotation_name) {
3912
result = current;
3913
3914
if (!is_native) {
3915
break;
3916
}
3917
}
3918
3919
if (is_native) {
3920
doc_annotation_cache[p_class_name][annotation.name] = current;
3921
}
3922
}
3923
}
3924
3925
return result;
3926
}
3927
3928
#undef HANDLE_DOC
3929
3930
void EditorHelpBit::_add_type_to_title(const DocType &p_doc_type) {
3931
_add_type_to_rt(p_doc_type.type, p_doc_type.enumeration, p_doc_type.is_bitfield, title, this, symbol_class_name);
3932
}
3933
3934
void EditorHelpBit::_update_labels() {
3935
const Ref<Font> doc_bold_font = get_theme_font(SNAME("doc_bold"), EditorStringName(EditorFonts));
3936
3937
if (!symbol_type.is_empty() || !symbol_name.is_empty()) {
3938
title->clear();
3939
3940
title->push_font(doc_bold_font);
3941
3942
if (!symbol_type.is_empty()) {
3943
title->push_color(get_theme_color(SNAME("title_color"), SNAME("EditorHelp")));
3944
title->add_text(symbol_type);
3945
title->pop(); // color
3946
}
3947
3948
if (!symbol_type.is_empty() && !symbol_name.is_empty()) {
3949
title->add_text(" ");
3950
}
3951
3952
if (!symbol_name.is_empty()) {
3953
if (!symbol_doc_link.is_empty()) {
3954
title->push_meta(symbol_doc_link, RichTextLabel::META_UNDERLINE_ON_HOVER);
3955
}
3956
if (use_class_prefix && !symbol_class_name.is_empty() && symbol_hint != SYMBOL_HINT_INHERITANCE) {
3957
title->add_text(symbol_class_name + ".");
3958
}
3959
title->add_text(symbol_name);
3960
if (!symbol_doc_link.is_empty()) {
3961
title->pop(); // meta
3962
}
3963
}
3964
3965
title->pop(); // font
3966
3967
const Color text_color = get_theme_color(SNAME("text_color"), SNAME("EditorHelp"));
3968
const Color symbol_color = get_theme_color(SNAME("symbol_color"), SNAME("EditorHelp"));
3969
const Color value_color = get_theme_color(SNAME("value_color"), SNAME("EditorHelp"));
3970
const Color qualifier_color = get_theme_color(SNAME("qualifier_color"), SNAME("EditorHelp"));
3971
const Ref<Font> doc_source = get_theme_font(SNAME("doc_source"), EditorStringName(EditorFonts));
3972
const int doc_source_size = get_theme_font_size(SNAME("doc_source_size"), EditorStringName(EditorFonts));
3973
3974
switch (symbol_hint) {
3975
case SYMBOL_HINT_NONE: {
3976
// Nothing to do.
3977
} break;
3978
case SYMBOL_HINT_INHERITANCE: {
3979
const DocData::ClassDoc *class_doc = EditorHelp::get_doc(symbol_class_name);
3980
String inherits = class_doc ? class_doc->inherits : String();
3981
3982
if (!inherits.is_empty()) {
3983
title->push_font(doc_source);
3984
title->push_font_size(doc_source_size * 0.9);
3985
3986
while (!inherits.is_empty()) {
3987
title->push_color(symbol_color);
3988
title->add_text(" <" + nbsp);
3989
title->pop(); // color
3990
3991
_add_type_to_title({ inherits, String(), false });
3992
3993
const DocData::ClassDoc *base_class_doc = EditorHelp::get_doc(inherits);
3994
inherits = base_class_doc ? base_class_doc->inherits : String();
3995
}
3996
3997
title->pop(); // font_size
3998
title->pop(); // font
3999
}
4000
} break;
4001
case SYMBOL_HINT_ASSIGNABLE: {
4002
const bool has_type = !help_data.doc_type.type.is_empty();
4003
const bool has_value = !help_data.value.is_empty();
4004
4005
if (has_type || has_value) {
4006
title->push_font(doc_source);
4007
title->push_font_size(doc_source_size * 0.9);
4008
4009
if (has_type) {
4010
title->push_color(symbol_color);
4011
title->add_text(colon_nbsp);
4012
title->pop(); // color
4013
4014
_add_type_to_title(help_data.doc_type);
4015
}
4016
4017
if (has_value) {
4018
title->push_color(symbol_color);
4019
title->add_text(nbsp_equal_nbsp);
4020
title->pop(); // color
4021
4022
title->push_color(value_color);
4023
title->add_text(_fix_constant(help_data.value));
4024
title->pop(); // color
4025
}
4026
4027
title->pop(); // font_size
4028
title->pop(); // font
4029
}
4030
} break;
4031
case SYMBOL_HINT_SIGNATURE: {
4032
title->push_font(doc_source);
4033
title->push_font_size(doc_source_size * 0.9);
4034
4035
title->push_color(symbol_color);
4036
title->add_text("(");
4037
title->pop(); // color
4038
4039
for (int i = 0; i < help_data.arguments.size(); i++) {
4040
const ArgumentData &argument = help_data.arguments[i];
4041
4042
if (i > 0) {
4043
title->push_color(symbol_color);
4044
title->add_text(", ");
4045
title->pop(); // color
4046
}
4047
4048
title->push_color(text_color);
4049
title->add_text(argument.name);
4050
title->pop(); // color
4051
4052
title->push_color(symbol_color);
4053
title->add_text(colon_nbsp);
4054
title->pop(); // color
4055
4056
_add_type_to_title(argument.doc_type);
4057
4058
if (!argument.default_value.is_empty()) {
4059
title->push_color(symbol_color);
4060
title->add_text(nbsp_equal_nbsp);
4061
title->pop(); // color
4062
4063
title->push_color(value_color);
4064
title->add_text(_fix_constant(argument.default_value));
4065
title->pop(); // color
4066
}
4067
}
4068
4069
if (help_data.qualifiers.contains("vararg")) {
4070
if (!help_data.arguments.is_empty()) {
4071
title->push_color(symbol_color);
4072
title->add_text(", ");
4073
title->pop(); // color
4074
}
4075
4076
title->push_color(symbol_color);
4077
title->add_text("...");
4078
title->pop(); // color
4079
4080
const ArgumentData &rest_argument = help_data.rest_argument;
4081
4082
title->push_color(text_color);
4083
title->add_text(rest_argument.name.is_empty() ? "args" : rest_argument.name);
4084
title->pop(); // color
4085
4086
title->push_color(symbol_color);
4087
title->add_text(colon_nbsp);
4088
title->pop(); // color
4089
4090
if (rest_argument.doc_type.type.is_empty()) {
4091
_add_type_to_title({ "Array", "", false });
4092
} else {
4093
_add_type_to_title(rest_argument.doc_type);
4094
}
4095
}
4096
4097
title->push_color(symbol_color);
4098
title->add_text(")");
4099
title->pop(); // color
4100
4101
if (!help_data.doc_type.type.is_empty()) {
4102
title->push_color(symbol_color);
4103
title->add_text(" ->" + nbsp);
4104
title->pop(); // color
4105
4106
_add_type_to_title(help_data.doc_type);
4107
}
4108
4109
if (!help_data.qualifiers.is_empty()) {
4110
title->push_color(qualifier_color);
4111
_add_qualifiers_to_rt(help_data.qualifiers, title);
4112
title->pop(); // color
4113
}
4114
4115
title->pop(); // font_size
4116
title->pop(); // font
4117
} break;
4118
}
4119
4120
title->show();
4121
} else {
4122
title->hide();
4123
}
4124
4125
content->clear();
4126
4127
bool has_prev_text = false;
4128
4129
if (!help_data.deprecated_message.is_empty()) {
4130
has_prev_text = true;
4131
4132
Ref<Texture2D> error_icon = get_editor_theme_icon(SNAME("StatusError"));
4133
content->add_image(error_icon, error_icon->get_width(), error_icon->get_height());
4134
content->add_text(nbsp);
4135
content->push_color(get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
4136
content->push_font(doc_bold_font);
4137
content->add_text(TTR("Deprecated:"));
4138
content->pop(); // font
4139
content->pop(); // color
4140
content->add_text(" ");
4141
_add_text_to_rt(help_data.deprecated_message, content, this, symbol_class_name);
4142
}
4143
4144
if (!help_data.experimental_message.is_empty()) {
4145
if (has_prev_text) {
4146
content->add_newline();
4147
content->add_newline();
4148
}
4149
has_prev_text = true;
4150
4151
Ref<Texture2D> warning_icon = get_editor_theme_icon(SNAME("NodeWarning"));
4152
content->add_image(warning_icon, warning_icon->get_width(), warning_icon->get_height());
4153
content->add_text(nbsp);
4154
content->push_color(get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
4155
content->push_font(doc_bold_font);
4156
content->add_text(TTR("Experimental:"));
4157
content->pop(); // font
4158
content->pop(); // color
4159
content->add_text(" ");
4160
_add_text_to_rt(help_data.experimental_message, content, this, symbol_class_name);
4161
}
4162
4163
if (!help_data.description.is_empty()) {
4164
if (has_prev_text) {
4165
content->add_newline();
4166
content->add_newline();
4167
}
4168
has_prev_text = true;
4169
4170
const Color comment_color = get_theme_color(SNAME("comment_color"), SNAME("EditorHelp"));
4171
_add_text_to_rt(help_data.description.replace("<EditorHelpBitCommentColor>", comment_color.to_html()), content, this, symbol_class_name);
4172
}
4173
4174
if (!help_data.resource_path.is_empty()) {
4175
if (has_prev_text) {
4176
content->add_newline();
4177
content->add_newline();
4178
}
4179
has_prev_text = true;
4180
4181
const String ext = help_data.resource_path.get_extension();
4182
const bool is_dir = ext.is_empty();
4183
const bool is_valid = is_dir || EditorFileSystem::get_singleton()->get_valid_extensions().has(ext);
4184
if (!is_dir && is_valid) {
4185
content->push_meta("open-res:" + help_data.resource_path, RichTextLabel::META_UNDERLINE_ON_HOVER);
4186
content->add_image(get_editor_theme_icon(SNAME("Load")));
4187
content->add_text(nbsp + TTR("Open"));
4188
content->pop(); // meta
4189
content->add_newline();
4190
}
4191
4192
if (is_valid) {
4193
content->push_meta("show:" + help_data.resource_path, RichTextLabel::META_UNDERLINE_ON_HOVER);
4194
content->add_image(get_editor_theme_icon(SNAME("Filesystem")));
4195
content->add_text(nbsp + TTR("Show in FileSystem"));
4196
content->pop(); // meta
4197
} else {
4198
content->push_meta("open-file:" + help_data.resource_path, RichTextLabel::META_UNDERLINE_ON_HOVER);
4199
content->add_image(get_editor_theme_icon(SNAME("Filesystem")));
4200
content->add_text(nbsp + TTR("Open in File Manager"));
4201
content->pop(); // meta
4202
}
4203
}
4204
4205
if (is_inside_tree()) {
4206
update_content_height();
4207
}
4208
}
4209
4210
void EditorHelpBit::_go_to_help(const String &p_what) {
4211
EditorNode::get_singleton()->get_editor_main_screen()->select(EditorMainScreen::EDITOR_SCRIPT);
4212
ScriptEditor::get_singleton()->goto_help(p_what);
4213
emit_signal(SNAME("request_hide"));
4214
}
4215
4216
void EditorHelpBit::_meta_clicked(const String &p_select) {
4217
if (p_select.begins_with("$")) { // Enum.
4218
const String link = p_select.substr(1);
4219
4220
String enum_class_name;
4221
String enum_name;
4222
if (CoreConstants::is_global_enum(link)) {
4223
enum_class_name = "@GlobalScope";
4224
enum_name = link;
4225
} else {
4226
const int dot_pos = link.rfind_char('.');
4227
if (dot_pos >= 0) {
4228
enum_class_name = link.left(dot_pos);
4229
enum_name = link.substr(dot_pos + 1);
4230
} else {
4231
enum_class_name = symbol_class_name;
4232
enum_name = link;
4233
}
4234
}
4235
4236
_go_to_help("class_enum:" + enum_class_name + ":" + enum_name);
4237
} else if (p_select.begins_with("#")) { // Class.
4238
_go_to_help("class_name:" + p_select.substr(1));
4239
} else if (p_select.begins_with("@")) { // Member.
4240
const int tag_end = p_select.find_char(' ');
4241
const String tag = p_select.substr(1, tag_end - 1);
4242
const String link = p_select.substr(tag_end + 1).lstrip(" ");
4243
4244
String topic;
4245
if (tag == "method") {
4246
topic = "class_method";
4247
} else if (tag == "constructor") {
4248
topic = "class_method";
4249
} else if (tag == "operator") {
4250
topic = "class_method";
4251
} else if (tag == "member") {
4252
topic = "class_property";
4253
} else if (tag == "enum") {
4254
topic = "class_enum";
4255
} else if (tag == "signal") {
4256
topic = "class_signal";
4257
} else if (tag == "constant") {
4258
topic = "class_constant";
4259
} else if (tag == "annotation") {
4260
topic = "class_annotation";
4261
} else if (tag == "theme_item") {
4262
topic = "class_theme_item";
4263
} else {
4264
return;
4265
}
4266
4267
if (topic == "class_enum") {
4268
const String enum_link = link.trim_prefix("@GlobalScope.");
4269
if (CoreConstants::is_global_enum(enum_link)) {
4270
_go_to_help(topic + ":@GlobalScope:" + enum_link);
4271
return;
4272
}
4273
} else if (topic == "class_constant") {
4274
if (CoreConstants::is_global_constant(link)) {
4275
_go_to_help(topic + ":@GlobalScope:" + link);
4276
return;
4277
}
4278
}
4279
4280
if (link.contains_char('.')) {
4281
const int class_end = link.rfind_char('.');
4282
_go_to_help(topic + ":" + link.left(class_end) + ":" + link.substr(class_end + 1));
4283
} else {
4284
_go_to_help(topic + ":" + symbol_class_name + ":" + link);
4285
}
4286
} else if (p_select.begins_with("open-file:")) {
4287
String path = ProjectSettings::get_singleton()->globalize_path(p_select.trim_prefix("open-file:"));
4288
OS::get_singleton()->shell_show_in_file_manager(path, true);
4289
} else if (p_select.begins_with("open-res:")) {
4290
EditorNode::get_singleton()->load_scene_or_resource(p_select.trim_prefix("open-res:"));
4291
} else if (p_select.begins_with("show:")) {
4292
FileSystemDock::get_singleton()->navigate_to_path(p_select.trim_prefix("show:"));
4293
} else if (p_select.begins_with("http:") || p_select.begins_with("https:")) {
4294
OS::get_singleton()->shell_open(p_select);
4295
} else if (p_select.begins_with("^")) { // Copy button.
4296
DisplayServer::get_singleton()->clipboard_set(p_select.substr(1));
4297
}
4298
}
4299
4300
void EditorHelpBit::_bind_methods() {
4301
ADD_SIGNAL(MethodInfo("request_hide"));
4302
}
4303
4304
void EditorHelpBit::_notification(int p_what) {
4305
switch (p_what) {
4306
case NOTIFICATION_THEME_CHANGED:
4307
_update_labels();
4308
break;
4309
}
4310
}
4311
4312
void EditorHelpBit::parse_symbol(const String &p_symbol, const String &p_prologue) {
4313
const PackedStringArray slices = p_symbol.split("|", true, 3);
4314
ERR_FAIL_COND_MSG(slices.size() < 3, R"(Invalid doc id: The expected format is "item_type|class_name|item_name[|item_data]".)");
4315
4316
const String &item_type = slices[0];
4317
const String &class_name = slices[1];
4318
const String &item_name = slices[2];
4319
4320
Dictionary item_data;
4321
if (slices.size() > 3) {
4322
item_data = JSON::parse_string(slices[3]);
4323
}
4324
4325
symbol_doc_link = String();
4326
symbol_class_name = class_name;
4327
symbol_type = String();
4328
symbol_name = item_name;
4329
symbol_hint = SYMBOL_HINT_NONE;
4330
help_data = HelpData();
4331
4332
if (item_type == "class") {
4333
symbol_doc_link = vformat("#%s", class_name);
4334
symbol_type = TTR("Class");
4335
symbol_name = class_name;
4336
symbol_hint = SYMBOL_HINT_INHERITANCE;
4337
help_data = _get_class_help_data(class_name);
4338
} else if (item_type == "enum") {
4339
symbol_doc_link = vformat("$%s.%s", class_name, item_name);
4340
symbol_type = TTR("Enumeration");
4341
help_data = _get_enum_help_data(class_name, item_name);
4342
} else if (item_type == "constant") {
4343
symbol_doc_link = vformat("@constant %s.%s", class_name, item_name);
4344
symbol_type = TTR("Constant");
4345
symbol_hint = SYMBOL_HINT_ASSIGNABLE;
4346
help_data = _get_constant_help_data(class_name, item_name);
4347
} else if (item_type == "property") {
4348
if (item_name.begins_with("metadata/")) {
4349
symbol_type = TTR("Metadata");
4350
symbol_name = item_name.trim_prefix("metadata/");
4351
} else if (class_name == "ProjectSettings" || class_name == "EditorSettings") {
4352
symbol_doc_link = vformat("@member %s.%s", class_name, item_name);
4353
symbol_type = TTR("Setting");
4354
symbol_hint = SYMBOL_HINT_ASSIGNABLE;
4355
} else {
4356
symbol_doc_link = vformat("@member %s.%s", class_name, item_name);
4357
symbol_type = TTR("Property");
4358
symbol_hint = SYMBOL_HINT_ASSIGNABLE;
4359
}
4360
help_data = _get_property_help_data(class_name, item_name);
4361
} else if (item_type == "internal_property") {
4362
symbol_type = TTR("Internal Property");
4363
help_data.description = "[color=<EditorHelpBitCommentColor>][i]" + TTR("This property can only be set in the Inspector.") + "[/i][/color]";
4364
} else if (item_type == "theme_item") {
4365
symbol_doc_link = vformat("@theme_item %s.%s", class_name, item_name);
4366
symbol_type = TTR("Theme Property");
4367
symbol_hint = SYMBOL_HINT_ASSIGNABLE;
4368
help_data = _get_theme_item_help_data(class_name, item_name);
4369
} else if (item_type == "method") {
4370
symbol_doc_link = vformat("@method %s.%s", class_name, item_name);
4371
symbol_type = TTR("Method");
4372
symbol_hint = SYMBOL_HINT_SIGNATURE;
4373
help_data = _get_method_help_data(class_name, item_name);
4374
} else if (item_type == "signal") {
4375
symbol_doc_link = vformat("@signal %s.%s", class_name, item_name);
4376
symbol_type = TTR("Signal");
4377
symbol_hint = SYMBOL_HINT_SIGNATURE;
4378
help_data = _get_signal_help_data(class_name, item_name);
4379
} else if (item_type == "annotation") {
4380
symbol_doc_link = vformat("@annotation %s.%s", class_name, item_name);
4381
symbol_type = TTR("Annotation");
4382
symbol_hint = SYMBOL_HINT_SIGNATURE;
4383
help_data = _get_annotation_help_data(class_name, item_name);
4384
} else if (item_type == "local_constant" || item_type == "local_variable") {
4385
symbol_type = (item_type == "local_constant") ? TTR("Local Constant") : TTR("Local Variable");
4386
symbol_hint = SYMBOL_HINT_ASSIGNABLE;
4387
help_data.description = item_data.get("description", "").operator String().strip_edges();
4388
if (item_data.get("is_deprecated", false)) {
4389
const String deprecated_message = item_data.get("deprecated_message", "").operator String().strip_edges();
4390
if (deprecated_message.is_empty()) {
4391
if (item_type == "local_constant") {
4392
help_data.deprecated_message = TTR("This constant may be changed or removed in future versions.");
4393
} else {
4394
help_data.deprecated_message = TTR("This variable may be changed or removed in future versions.");
4395
}
4396
} else {
4397
help_data.deprecated_message = deprecated_message;
4398
}
4399
}
4400
if (item_data.get("is_experimental", false)) {
4401
const String experimental_message = item_data.get("experimental_message", "").operator String().strip_edges();
4402
if (experimental_message.is_empty()) {
4403
if (item_type == "local_constant") {
4404
help_data.experimental_message = TTR("This constant may be changed or removed in future versions.");
4405
} else {
4406
help_data.experimental_message = TTR("This variable may be changed or removed in future versions.");
4407
}
4408
} else {
4409
help_data.experimental_message = experimental_message;
4410
}
4411
}
4412
help_data.doc_type.type = item_data.get("doc_type", "");
4413
help_data.doc_type.enumeration = item_data.get("enumeration", "");
4414
help_data.doc_type.is_bitfield = item_data.get("is_bitfield", false);
4415
help_data.value = item_data.get("value", "");
4416
} else if (item_type == "resource") {
4417
String path = item_name.simplify_path();
4418
const bool is_uid = path.begins_with("uid://");
4419
if (is_uid) {
4420
if (ResourceUID::get_singleton()->has_id(ResourceUID::get_singleton()->text_to_id(path))) {
4421
path = ResourceUID::uid_to_path(path);
4422
} else {
4423
path = "";
4424
}
4425
}
4426
help_data.resource_path = path;
4427
4428
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
4429
if (da->file_exists(path)) {
4430
help_data.doc_type.type = ResourceLoader::get_resource_type(path);
4431
if (help_data.doc_type.type.is_empty()) {
4432
const Vector<String> textfile_ext = ((String)(EDITOR_GET("docks/filesystem/textfile_extensions"))).split(",", false);
4433
symbol_type = textfile_ext.has(path.get_extension()) ? TTR("Text File") : TTR("File");
4434
} else {
4435
symbol_type = TTR("Resource");
4436
symbol_hint = SYMBOL_HINT_ASSIGNABLE;
4437
if (is_uid) {
4438
help_data.description = vformat("%s: [color=<EditorHelpBitCommentColor>]%s[/color]", TTR("Path"), path);
4439
}
4440
}
4441
symbol_name = path.get_file();
4442
} else if (!is_uid && da->dir_exists(path)) {
4443
symbol_type = TTR("Directory");
4444
symbol_name = path;
4445
} else {
4446
help_data.resource_path = "";
4447
symbol_name = "";
4448
if (is_uid) {
4449
symbol_type = TTR("Invalid UID");
4450
help_data.description = "[color=<EditorHelpBitCommentColor>][i]" + TTR("This UID does not point to any valid Resource.") + "[/i][/color]";
4451
} else {
4452
symbol_type = TTR("Invalid path");
4453
help_data.description = "[color=<EditorHelpBitCommentColor>][i]" + TTR("This path does not exist.") + "[/i][/color]";
4454
}
4455
}
4456
} else {
4457
ERR_FAIL_MSG("Invalid doc id: Unknown item type " + item_type.quote() + ".");
4458
}
4459
4460
// Do not add links for custom or undocumented symbols.
4461
if (symbol_class_name.is_empty() || (help_data.description.is_empty() && help_data.deprecated_message.is_empty() && help_data.experimental_message.is_empty())) {
4462
symbol_doc_link = String();
4463
}
4464
4465
if (!p_prologue.is_empty()) {
4466
if (help_data.description.is_empty()) {
4467
help_data.description = p_prologue;
4468
} else {
4469
help_data.description = p_prologue + "\n" + help_data.description;
4470
}
4471
}
4472
4473
if (help_data.description.is_empty() && item_type != "resource") {
4474
help_data.description = "[color=<EditorHelpBitCommentColor>][i]" + TTR("No description available.") + "[/i][/color]";
4475
}
4476
4477
if (is_inside_tree()) {
4478
_update_labels();
4479
}
4480
}
4481
4482
void EditorHelpBit::set_custom_text(const String &p_type, const String &p_name, const String &p_description) {
4483
symbol_doc_link = String();
4484
symbol_class_name = String();
4485
symbol_type = p_type;
4486
symbol_name = p_name;
4487
symbol_hint = SYMBOL_HINT_NONE;
4488
4489
help_data = HelpData();
4490
help_data.description = p_description;
4491
4492
if (is_inside_tree()) {
4493
_update_labels();
4494
}
4495
}
4496
4497
void EditorHelpBit::set_content_height_limits(float p_min, float p_max) {
4498
ERR_FAIL_COND(p_min > p_max);
4499
content_min_height = p_min;
4500
content_max_height = p_max;
4501
4502
if (is_inside_tree()) {
4503
update_content_height();
4504
}
4505
}
4506
4507
void EditorHelpBit::update_content_height() {
4508
float content_height = content->get_content_height();
4509
const Ref<StyleBox> style = content->get_theme_stylebox(CoreStringName(normal));
4510
if (style.is_valid()) {
4511
content_height += style->get_content_margin(SIDE_TOP) + style->get_content_margin(SIDE_BOTTOM);
4512
}
4513
content->set_custom_minimum_size(Size2(content->get_custom_minimum_size().x, CLAMP(content_height, content_min_height, content_max_height)));
4514
}
4515
4516
EditorHelpBit::EditorHelpBit(const String &p_symbol, const String &p_prologue, bool p_use_class_prefix, bool p_allow_selection) {
4517
add_theme_constant_override("separation", 0);
4518
4519
title = memnew(RichTextLabel);
4520
title->set_theme_type_variation("EditorHelpBitTitle");
4521
title->set_custom_minimum_size(Size2(640 * EDSCALE, 0)); // GH-93031. Set the minimum width even if `fit_content` is true.
4522
title->set_fit_content(true);
4523
title->set_selection_enabled(p_allow_selection);
4524
title->set_context_menu_enabled(p_allow_selection);
4525
title->set_selection_modifier(callable_mp_static(_replace_nbsp_with_space));
4526
title->connect("meta_clicked", callable_mp(this, &EditorHelpBit::_meta_clicked));
4527
title->hide();
4528
add_child(title);
4529
4530
content_min_height = 48 * EDSCALE;
4531
content_max_height = 360 * EDSCALE;
4532
4533
content = memnew(RichTextLabel);
4534
content->set_theme_type_variation("EditorHelpBitContent");
4535
content->set_custom_minimum_size(Size2(640 * EDSCALE, content_min_height));
4536
content->set_selection_enabled(p_allow_selection);
4537
content->set_context_menu_enabled(p_allow_selection);
4538
content->set_selection_modifier(callable_mp_static(_replace_nbsp_with_space));
4539
content->connect("meta_clicked", callable_mp(this, &EditorHelpBit::_meta_clicked));
4540
add_child(content);
4541
4542
use_class_prefix = p_use_class_prefix;
4543
4544
if (!p_symbol.is_empty()) {
4545
parse_symbol(p_symbol, p_prologue);
4546
} else if (!p_prologue.is_empty()) {
4547
set_custom_text(String(), String(), p_prologue);
4548
}
4549
}
4550
4551
/// EditorHelpBitTooltip ///
4552
4553
bool EditorHelpBitTooltip::_is_tooltip_visible = false;
4554
4555
Control *EditorHelpBitTooltip::_make_invisible_control() {
4556
Control *control = memnew(Control);
4557
control->set_visible(false);
4558
return control;
4559
}
4560
4561
void EditorHelpBitTooltip::_start_timer() {
4562
if (timer->is_inside_tree() && timer->is_stopped()) {
4563
timer->start();
4564
}
4565
}
4566
4567
void EditorHelpBitTooltip::_target_gui_input(const Ref<InputEvent> &p_event) {
4568
// Only scrolling is not checked in `NOTIFICATION_INTERNAL_PROCESS`.
4569
const Ref<InputEventMouseButton> mb = p_event;
4570
if (mb.is_valid()) {
4571
switch (mb->get_button_index()) {
4572
case MouseButton::WHEEL_UP:
4573
case MouseButton::WHEEL_DOWN:
4574
case MouseButton::WHEEL_LEFT:
4575
case MouseButton::WHEEL_RIGHT:
4576
queue_free();
4577
break;
4578
default:
4579
break;
4580
}
4581
}
4582
}
4583
4584
void EditorHelpBitTooltip::_notification(int p_what) {
4585
switch (p_what) {
4586
case NOTIFICATION_ENTER_TREE:
4587
_is_tooltip_visible = true;
4588
_enter_tree_time = OS::get_singleton()->get_ticks_msec();
4589
break;
4590
case NOTIFICATION_EXIT_TREE:
4591
_is_tooltip_visible = false;
4592
break;
4593
case NOTIFICATION_WM_MOUSE_ENTER:
4594
_is_mouse_inside_tooltip = true;
4595
timer->stop();
4596
break;
4597
case NOTIFICATION_WM_MOUSE_EXIT:
4598
_is_mouse_inside_tooltip = false;
4599
_start_timer();
4600
break;
4601
case NOTIFICATION_INTERNAL_PROCESS:
4602
// A workaround to hide the tooltip since the window does not receive keyboard events
4603
// with `FLAG_POPUP` and `FLAG_NO_FOCUS` flags, so we can't use `_input_from_window()`.
4604
if (is_inside_tree()) {
4605
if (Input::get_singleton()->is_action_just_pressed(SNAME("ui_cancel"), true)) {
4606
queue_free();
4607
get_parent_viewport()->set_input_as_handled();
4608
} else if (Input::get_singleton()->is_anything_pressed_except_mouse()) {
4609
queue_free();
4610
} else if (!Input::get_singleton()->get_mouse_button_mask().is_empty()) {
4611
if (!_is_mouse_inside_tooltip) {
4612
queue_free();
4613
}
4614
} else if (!Input::get_singleton()->get_last_mouse_velocity().is_zero_approx()) {
4615
if (!_is_mouse_inside_tooltip && OS::get_singleton()->get_ticks_msec() - _enter_tree_time > 350) {
4616
_start_timer();
4617
}
4618
}
4619
}
4620
break;
4621
}
4622
}
4623
4624
Control *EditorHelpBitTooltip::show_tooltip(Control *p_target, const String &p_symbol, const String &p_prologue, bool p_use_class_prefix) {
4625
ERR_FAIL_NULL_V(p_target, _make_invisible_control());
4626
4627
// Show the custom tooltip only if it is not already visible.
4628
// The viewport will retrigger `make_custom_tooltip()` every few seconds
4629
// because the return control is not visible even if the custom tooltip is displayed.
4630
if (_is_tooltip_visible || Input::get_singleton()->is_anything_pressed()) {
4631
return _make_invisible_control();
4632
}
4633
4634
EditorHelpBit *help_bit = memnew(EditorHelpBit(p_symbol, p_prologue, p_use_class_prefix, false));
4635
4636
EditorHelpBitTooltip *tooltip = memnew(EditorHelpBitTooltip(p_target));
4637
help_bit->connect("request_hide", callable_mp(static_cast<Node *>(tooltip), &Node::queue_free));
4638
tooltip->add_child(help_bit);
4639
p_target->add_child(tooltip);
4640
4641
help_bit->update_content_height();
4642
tooltip->popup_under_cursor();
4643
4644
return _make_invisible_control();
4645
}
4646
4647
// Copy-paste from `Viewport::_gui_show_tooltip()`.
4648
void EditorHelpBitTooltip::popup_under_cursor() {
4649
Point2 mouse_pos = get_mouse_position();
4650
Point2 tooltip_offset = GLOBAL_GET_CACHED(Point2, "display/mouse_cursor/tooltip_position_offset");
4651
Rect2 r(mouse_pos + tooltip_offset, get_contents_minimum_size());
4652
r.size = r.size.min(get_max_size());
4653
4654
Window *window = get_parent_visible_window();
4655
Rect2i vr;
4656
if (is_embedded()) {
4657
vr = get_embedder()->get_visible_rect();
4658
} else {
4659
vr = window->get_usable_parent_rect();
4660
}
4661
4662
if (!DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_SELF_FITTING_WINDOWS) || is_embedded()) {
4663
if (r.size.x + r.position.x > vr.size.x + vr.position.x) {
4664
// Place it in the opposite direction. If it fails, just hug the border.
4665
r.position.x = mouse_pos.x - r.size.x - tooltip_offset.x;
4666
4667
if (r.position.x < vr.position.x) {
4668
r.position.x = vr.position.x + vr.size.x - r.size.x;
4669
}
4670
} else if (r.position.x < vr.position.x) {
4671
r.position.x = vr.position.x;
4672
}
4673
4674
if (r.size.y + r.position.y > vr.size.y + vr.position.y) {
4675
// Same as above.
4676
r.position.y = mouse_pos.y - r.size.y - tooltip_offset.y;
4677
4678
if (r.position.y < vr.position.y) {
4679
r.position.y = vr.position.y + vr.size.y - r.size.y;
4680
}
4681
} else if (r.position.y < vr.position.y) {
4682
r.position.y = vr.position.y;
4683
}
4684
}
4685
4686
// When `FLAG_POPUP` is false, it prevents the editor from losing focus when displaying the tooltip.
4687
// This way, clicks and double-clicks are still available outside the tooltip.
4688
set_flag(Window::FLAG_POPUP, false);
4689
set_flag(Window::FLAG_NO_FOCUS, true);
4690
popup(r);
4691
}
4692
4693
EditorHelpBitTooltip::EditorHelpBitTooltip(Control *p_target) {
4694
ERR_FAIL_NULL(p_target);
4695
4696
set_theme_type_variation("TooltipPanel");
4697
4698
timer = memnew(Timer);
4699
timer->set_wait_time(0.25);
4700
timer->connect("timeout", callable_mp(static_cast<Node *>(this), &Node::queue_free));
4701
add_child(timer);
4702
4703
p_target->connect(SceneStringName(mouse_exited), callable_mp(this, &EditorHelpBitTooltip::_start_timer));
4704
p_target->connect(SceneStringName(gui_input), callable_mp(this, &EditorHelpBitTooltip::_target_gui_input));
4705
4706
set_process_internal(true);
4707
}
4708
4709
/// EditorHelpHighlighter ///
4710
4711
EditorHelpHighlighter *EditorHelpHighlighter::singleton = nullptr;
4712
4713
void EditorHelpHighlighter::create_singleton() {
4714
ERR_FAIL_COND(singleton != nullptr);
4715
singleton = memnew(EditorHelpHighlighter);
4716
}
4717
4718
void EditorHelpHighlighter::free_singleton() {
4719
ERR_FAIL_NULL(singleton);
4720
memdelete(singleton);
4721
singleton = nullptr;
4722
}
4723
4724
EditorHelpHighlighter *EditorHelpHighlighter::get_singleton() {
4725
return singleton;
4726
}
4727
4728
EditorHelpHighlighter::HighlightData EditorHelpHighlighter::_get_highlight_data(Language p_language, const String &p_source, bool p_use_cache) {
4729
switch (p_language) {
4730
case LANGUAGE_GDSCRIPT:
4731
#ifndef MODULE_GDSCRIPT_ENABLED
4732
ERR_FAIL_V_MSG(HighlightData(), "GDScript module is disabled.");
4733
#endif
4734
break;
4735
case LANGUAGE_CSHARP:
4736
#ifndef MODULE_MONO_ENABLED
4737
ERR_FAIL_V_MSG(HighlightData(), "Mono module is disabled.");
4738
#endif
4739
break;
4740
default:
4741
ERR_FAIL_V_MSG(HighlightData(), "Invalid parameter \"p_language\".");
4742
}
4743
4744
if (p_use_cache) {
4745
const HashMap<String, HighlightData>::ConstIterator E = highlight_data_caches[p_language].find(p_source);
4746
if (E) {
4747
return E->value;
4748
}
4749
}
4750
4751
text_edits[p_language]->set_text(p_source);
4752
if (scripts[p_language].is_valid()) { // See GH-89610.
4753
scripts[p_language]->set_source_code(p_source);
4754
}
4755
highlighters[p_language]->_update_cache();
4756
4757
HighlightData result;
4758
4759
int source_offset = 0;
4760
int result_index = 0;
4761
for (int i = 0; i < text_edits[p_language]->get_line_count(); i++) {
4762
const Dictionary dict = highlighters[p_language]->_get_line_syntax_highlighting_impl(i);
4763
4764
result.resize(result.size() + dict.size());
4765
4766
const Variant *key = nullptr;
4767
int prev_column = -1;
4768
while ((key = dict.next(key)) != nullptr) {
4769
const int column = *key;
4770
ERR_FAIL_COND_V(column <= prev_column, HighlightData());
4771
prev_column = column;
4772
4773
const Color color = dict[*key].operator Dictionary().get("color", Color());
4774
4775
result.write[result_index] = { source_offset + column, color };
4776
result_index++;
4777
}
4778
4779
source_offset += text_edits[p_language]->get_line(i).length() + 1; // Plus newline.
4780
}
4781
4782
if (p_use_cache) {
4783
highlight_data_caches[p_language][p_source] = result;
4784
}
4785
4786
return result;
4787
}
4788
4789
void EditorHelpHighlighter::highlight(RichTextLabel *p_rich_text_label, Language p_language, const String &p_source, bool p_use_cache) {
4790
ERR_FAIL_NULL(p_rich_text_label);
4791
4792
const HighlightData highlight_data = _get_highlight_data(p_language, p_source, p_use_cache);
4793
4794
if (!highlight_data.is_empty()) {
4795
for (int i = 1; i < highlight_data.size(); i++) {
4796
const Pair<int, Color> &prev = highlight_data[i - 1];
4797
const Pair<int, Color> &curr = highlight_data[i];
4798
p_rich_text_label->push_color(prev.second);
4799
p_rich_text_label->add_text(p_source.substr(prev.first, curr.first - prev.first));
4800
p_rich_text_label->pop(); // color
4801
}
4802
4803
const Pair<int, Color> &last = highlight_data[highlight_data.size() - 1];
4804
p_rich_text_label->push_color(last.second);
4805
p_rich_text_label->add_text(p_source.substr(last.first));
4806
p_rich_text_label->pop(); // color
4807
}
4808
}
4809
4810
void EditorHelpHighlighter::reset_cache() {
4811
const Color text_color = EDITOR_GET("text_editor/theme/highlighting/text_color");
4812
4813
#ifdef MODULE_GDSCRIPT_ENABLED
4814
highlight_data_caches[LANGUAGE_GDSCRIPT].clear();
4815
text_edits[LANGUAGE_GDSCRIPT]->add_theme_color_override(SceneStringName(font_color), text_color);
4816
#endif
4817
4818
#ifdef MODULE_MONO_ENABLED
4819
highlight_data_caches[LANGUAGE_CSHARP].clear();
4820
text_edits[LANGUAGE_CSHARP]->add_theme_color_override(SceneStringName(font_color), text_color);
4821
#endif
4822
}
4823
4824
EditorHelpHighlighter::EditorHelpHighlighter() {
4825
const Color text_color = EDITOR_GET("text_editor/theme/highlighting/text_color");
4826
4827
#ifdef MODULE_GDSCRIPT_ENABLED
4828
TextEdit *gdscript_text_edit = memnew(TextEdit);
4829
gdscript_text_edit->add_theme_color_override(SceneStringName(font_color), text_color);
4830
4831
Ref<GDScript> gdscript;
4832
gdscript.instantiate();
4833
4834
Ref<GDScriptSyntaxHighlighter> gdscript_highlighter;
4835
gdscript_highlighter.instantiate();
4836
gdscript_highlighter->set_text_edit(gdscript_text_edit);
4837
gdscript_highlighter->_set_edited_resource(gdscript);
4838
4839
text_edits[LANGUAGE_GDSCRIPT] = gdscript_text_edit;
4840
scripts[LANGUAGE_GDSCRIPT] = gdscript;
4841
highlighters[LANGUAGE_GDSCRIPT] = gdscript_highlighter;
4842
#endif
4843
4844
#ifdef MODULE_MONO_ENABLED
4845
TextEdit *csharp_text_edit = memnew(TextEdit);
4846
csharp_text_edit->add_theme_color_override(SceneStringName(font_color), text_color);
4847
4848
// See GH-89610.
4849
//Ref<CSharpScript> csharp;
4850
//csharp.instantiate();
4851
4852
Ref<EditorStandardSyntaxHighlighter> csharp_highlighter;
4853
csharp_highlighter.instantiate();
4854
csharp_highlighter->set_text_edit(csharp_text_edit);
4855
//csharp_highlighter->_set_edited_resource(csharp);
4856
csharp_highlighter->_set_script_language(CSharpLanguage::get_singleton());
4857
4858
text_edits[LANGUAGE_CSHARP] = csharp_text_edit;
4859
//scripts[LANGUAGE_CSHARP] = csharp;
4860
highlighters[LANGUAGE_CSHARP] = csharp_highlighter;
4861
#endif
4862
}
4863
4864
EditorHelpHighlighter::~EditorHelpHighlighter() {
4865
#ifdef MODULE_GDSCRIPT_ENABLED
4866
memdelete(text_edits[LANGUAGE_GDSCRIPT]);
4867
#endif
4868
4869
#ifdef MODULE_MONO_ENABLED
4870
memdelete(text_edits[LANGUAGE_CSHARP]);
4871
#endif
4872
}
4873
4874
/// FindBar ///
4875
4876
FindBar::FindBar() {
4877
search_text = memnew(LineEdit);
4878
search_text->set_keep_editing_on_text_submit(true);
4879
add_child(search_text);
4880
search_text->set_placeholder(TTR("Search"));
4881
search_text->set_tooltip_text(TTR("Search"));
4882
search_text->set_accessibility_name(TTRC("Search Documentation"));
4883
search_text->set_custom_minimum_size(Size2(100 * EDSCALE, 0));
4884
search_text->set_h_size_flags(SIZE_EXPAND_FILL);
4885
search_text->connect(SceneStringName(text_changed), callable_mp(this, &FindBar::_search_text_changed));
4886
search_text->connect(SceneStringName(text_submitted), callable_mp(this, &FindBar::_search_text_submitted));
4887
4888
matches_label = memnew(Label);
4889
add_child(matches_label);
4890
matches_label->set_focus_mode(FOCUS_ACCESSIBILITY);
4891
matches_label->hide();
4892
4893
find_prev = memnew(Button);
4894
find_prev->set_theme_type_variation(SceneStringName(FlatButton));
4895
find_prev->set_disabled(results_count < 1);
4896
find_prev->set_tooltip_text(TTR("Previous Match"));
4897
add_child(find_prev);
4898
find_prev->set_focus_mode(FOCUS_ACCESSIBILITY);
4899
find_prev->connect(SceneStringName(pressed), callable_mp(this, &FindBar::search_prev));
4900
4901
find_next = memnew(Button);
4902
find_next->set_theme_type_variation(SceneStringName(FlatButton));
4903
find_next->set_disabled(results_count < 1);
4904
find_next->set_tooltip_text(TTR("Next Match"));
4905
add_child(find_next);
4906
find_next->set_focus_mode(FOCUS_ACCESSIBILITY);
4907
find_next->connect(SceneStringName(pressed), callable_mp(this, &FindBar::search_next));
4908
4909
hide_button = memnew(Button);
4910
hide_button->set_theme_type_variation(SceneStringName(FlatButton));
4911
hide_button->set_tooltip_text(TTR("Hide"));
4912
hide_button->set_focus_mode(FOCUS_ACCESSIBILITY);
4913
hide_button->connect(SceneStringName(pressed), callable_mp(this, &FindBar::_hide_bar));
4914
hide_button->set_v_size_flags(SIZE_SHRINK_CENTER);
4915
add_child(hide_button);
4916
}
4917
4918
void FindBar::popup_search() {
4919
show();
4920
bool grabbed_focus = false;
4921
if (!search_text->has_focus()) {
4922
search_text->grab_focus();
4923
grabbed_focus = true;
4924
}
4925
4926
if (!search_text->get_text().is_empty()) {
4927
search_text->select_all();
4928
search_text->set_caret_column(search_text->get_text().length());
4929
if (grabbed_focus) {
4930
rich_text_label->deselect();
4931
results_count_to_current = 0;
4932
_search();
4933
}
4934
}
4935
}
4936
4937
void FindBar::_notification(int p_what) {
4938
switch (p_what) {
4939
case NOTIFICATION_THEME_CHANGED: {
4940
find_prev->set_button_icon(get_editor_theme_icon(SNAME("MoveUp")));
4941
find_next->set_button_icon(get_editor_theme_icon(SNAME("MoveDown")));
4942
hide_button->set_button_icon(get_editor_theme_icon(SNAME("Close")));
4943
matches_label->add_theme_color_override(SceneStringName(font_color), results_count > 0 ? get_theme_color(SceneStringName(font_color), SNAME("Label")) : get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
4944
} break;
4945
4946
case NOTIFICATION_VISIBILITY_CHANGED: {
4947
set_process_input(is_visible_in_tree());
4948
} break;
4949
}
4950
}
4951
4952
void FindBar::set_rich_text_label(RichTextLabel *p_rich_text_label) {
4953
rich_text_label = p_rich_text_label;
4954
}
4955
4956
bool FindBar::search_next() {
4957
return _search();
4958
}
4959
4960
bool FindBar::search_prev() {
4961
return _search(true);
4962
}
4963
4964
bool FindBar::_search(bool p_search_previous) {
4965
String stext = search_text->get_text();
4966
bool keep = prev_search == stext;
4967
bool ret = rich_text_label->search(stext, keep, p_search_previous);
4968
4969
prev_search = stext;
4970
if (!keep) {
4971
results_count_to_current = 0;
4972
}
4973
4974
if (ret) {
4975
_update_results_count(p_search_previous);
4976
} else {
4977
results_count = 0;
4978
results_count_to_current = 0;
4979
}
4980
_update_matches_label();
4981
4982
return ret;
4983
}
4984
4985
void FindBar::_update_results_count(bool p_search_previous) {
4986
results_count = 0;
4987
4988
String searched = search_text->get_text();
4989
if (searched.is_empty()) {
4990
return;
4991
}
4992
4993
String full_text = rich_text_label->get_parsed_text();
4994
4995
int from_pos = 0;
4996
4997
while (true) {
4998
int pos = full_text.findn(searched, from_pos);
4999
if (pos == -1) {
5000
break;
5001
}
5002
5003
results_count++;
5004
from_pos = pos + searched.length();
5005
}
5006
5007
results_count_to_current += (p_search_previous) ? -1 : 1;
5008
if (results_count_to_current > results_count) {
5009
results_count_to_current = results_count_to_current - results_count;
5010
} else if (results_count_to_current <= 0) {
5011
results_count_to_current = results_count;
5012
}
5013
}
5014
5015
void FindBar::_update_matches_label() {
5016
if (search_text->get_text().is_empty() || results_count == -1) {
5017
matches_label->hide();
5018
} else {
5019
matches_label->show();
5020
5021
matches_label->add_theme_color_override(SceneStringName(font_color), results_count > 0 ? get_theme_color(SceneStringName(font_color), SNAME("Label")) : get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
5022
if (results_count == 0) {
5023
matches_label->set_text(TTR("No match"));
5024
} else if (results_count_to_current == 0) {
5025
matches_label->set_text(vformat(TTRN("%d match", "%d matches", results_count), results_count));
5026
} else {
5027
matches_label->set_text(vformat(TTRN("%d of %d match", "%d of %d matches", results_count), results_count_to_current, results_count));
5028
}
5029
}
5030
find_prev->set_disabled(results_count < 1);
5031
find_next->set_disabled(results_count < 1);
5032
}
5033
5034
void FindBar::_hide_bar() {
5035
if (search_text->has_focus()) {
5036
rich_text_label->grab_focus();
5037
}
5038
5039
hide();
5040
}
5041
5042
// Implemented in input(..) as the LineEdit consumes the Escape pressed key.
5043
void FindBar::input(const Ref<InputEvent> &p_event) {
5044
ERR_FAIL_COND(p_event.is_null());
5045
5046
Ref<InputEventKey> k = p_event;
5047
if (k.is_valid() && k->is_action_pressed(SNAME("ui_cancel"), false, true)) {
5048
Control *focus_owner = get_viewport()->gui_get_focus_owner();
5049
5050
if (rich_text_label->has_focus() || (focus_owner && is_ancestor_of(focus_owner))) {
5051
_hide_bar();
5052
accept_event();
5053
}
5054
}
5055
}
5056
5057
void FindBar::_search_text_changed(const String &p_text) {
5058
search_next();
5059
}
5060
5061
void FindBar::_search_text_submitted(const String &p_text) {
5062
if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) {
5063
search_prev();
5064
} else {
5065
search_next();
5066
}
5067
}
5068
5069