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