Revamp how documentation tooltips work

This commit is contained in:
Michael Alexsander 2023-09-20 23:54:51 -03:00
parent f5696c311c
commit ae91644c73
No known key found for this signature in database
GPG key ID: A9C91EE110F4EABA
10 changed files with 373 additions and 275 deletions

View file

@ -835,35 +835,9 @@ ConnectDialog::~ConnectDialog() {
////////////////////////////////////////// //////////////////////////////////////////
// Originally copied and adapted from EditorProperty, try to keep style in sync.
Control *ConnectionsDockTree::make_custom_tooltip(const String &p_text) const { Control *ConnectionsDockTree::make_custom_tooltip(const String &p_text) const {
// `p_text` is expected to be something like this: // If it's not a doc tooltip, fallback to the default one.
// - `class|Control||Control brief description.`; return p_text.contains("::") ? nullptr : memnew(EditorHelpTooltip(p_text));
// - `signal|gui_input|(event: InputEvent)|gui_input description.`;
// - `../../.. :: _on_gui_input()`.
// Note that the description can be empty or contain `|`.
PackedStringArray slices = p_text.split("|", true, 3);
if (slices.size() < 4) {
return nullptr; // Use default tooltip instead.
}
String item_type = (slices[0] == "class") ? TTR("Class:") : TTR("Signal:");
String item_name = slices[1].strip_edges();
String item_params = slices[2].strip_edges();
String item_descr = slices[3].strip_edges();
String text = item_type + " [u][b]" + item_name + "[/b][/u]" + item_params + "\n";
if (item_descr.is_empty()) {
text += "[i]" + TTR("No description.") + "[/i]";
} else {
text += item_descr;
}
EditorHelpBit *help_bit = memnew(EditorHelpBit);
help_bit->get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 1));
help_bit->set_text(text);
return help_bit;
} }
struct _ConnectionsDockMethodInfoSort { struct _ConnectionsDockMethodInfoSort {
@ -1341,7 +1315,6 @@ void ConnectionsDock::update_tree() {
while (native_base != StringName()) { while (native_base != StringName()) {
String class_name; String class_name;
String doc_class_name; String doc_class_name;
String class_brief;
Ref<Texture2D> class_icon; Ref<Texture2D> class_icon;
List<MethodInfo> class_signals; List<MethodInfo> class_signals;
@ -1355,21 +1328,8 @@ void ConnectionsDock::update_tree() {
if (doc_class_name.is_empty()) { if (doc_class_name.is_empty()) {
doc_class_name = script_base->get_path().trim_prefix("res://").quote(); doc_class_name = script_base->get_path().trim_prefix("res://").quote();
} }
if (!doc_class_name.is_empty() && !doc_data->class_list.find(doc_class_name)) {
// For a script class, the cache is filled each time. doc_class_name = String();
if (!doc_class_name.is_empty()) {
if (descr_cache.has(doc_class_name)) {
descr_cache[doc_class_name].clear();
}
HashMap<String, DocData::ClassDoc>::ConstIterator F = doc_data->class_list.find(doc_class_name);
if (F) {
class_brief = F->value.brief_description;
for (int i = 0; i < F->value.signals.size(); i++) {
descr_cache[doc_class_name][F->value.signals[i].name] = F->value.signals[i].description;
}
} else {
doc_class_name = String();
}
} }
class_icon = editor_data.get_script_icon(script_base); class_icon = editor_data.get_script_icon(script_base);
@ -1398,18 +1358,9 @@ void ConnectionsDock::update_tree() {
script_base = base; script_base = base;
} else { } else {
class_name = native_base; class_name = native_base;
doc_class_name = class_name; doc_class_name = native_base;
HashMap<String, DocData::ClassDoc>::ConstIterator F = doc_data->class_list.find(doc_class_name); if (!doc_data->class_list.find(doc_class_name)) {
if (F) {
class_brief = DTR(F->value.brief_description);
// For a native class, the cache is filled once.
if (!descr_cache.has(doc_class_name)) {
for (int i = 0; i < F->value.signals.size(); i++) {
descr_cache[doc_class_name][F->value.signals[i].name] = DTR(F->value.signals[i].description);
}
}
} else {
doc_class_name = String(); doc_class_name = String();
} }
@ -1434,8 +1385,8 @@ void ConnectionsDock::update_tree() {
section_item = tree->create_item(root); section_item = tree->create_item(root);
section_item->set_text(0, class_name); section_item->set_text(0, class_name);
// `|` separators used in `make_custom_tooltip()` for formatting. // `|` separators used in `EditorHelpTooltip` for formatting.
section_item->set_tooltip_text(0, "class|" + class_name + "||" + class_brief); section_item->set_tooltip_text(0, "class|" + doc_class_name + "||");
section_item->set_icon(0, class_icon); section_item->set_icon(0, class_icon);
section_item->set_selectable(0, false); section_item->set_selectable(0, false);
section_item->set_editable(0, false); section_item->set_editable(0, false);
@ -1466,22 +1417,8 @@ void ConnectionsDock::update_tree() {
sinfo["args"] = argnames; sinfo["args"] = argnames;
signal_item->set_metadata(0, sinfo); signal_item->set_metadata(0, sinfo);
signal_item->set_icon(0, get_editor_theme_icon(SNAME("Signal"))); signal_item->set_icon(0, get_editor_theme_icon(SNAME("Signal")));
// `|` separators used in `EditorHelpTooltip` for formatting.
// Set tooltip with the signal's documentation. signal_item->set_tooltip_text(0, "signal|" + doc_class_name + "|" + String(signal_name) + "|" + signame.trim_prefix(mi.name));
{
String descr;
HashMap<StringName, HashMap<StringName, String>>::ConstIterator G = descr_cache.find(doc_class_name);
if (G) {
HashMap<StringName, String>::ConstIterator F = G->value.find(signal_name);
if (F) {
descr = F->value;
}
}
// `|` separators used in `make_custom_tooltip()` for formatting.
signal_item->set_tooltip_text(0, "signal|" + String(signal_name) + "|" + signame.trim_prefix(mi.name) + "|" + descr);
}
// List existing connections. // List existing connections.
List<Object::Connection> existing_connections; List<Object::Connection> existing_connections;

View file

@ -231,8 +231,6 @@ class ConnectionsDock : public VBoxContainer {
PopupMenu *slot_menu = nullptr; PopupMenu *slot_menu = nullptr;
LineEdit *search_box = nullptr; LineEdit *search_box = nullptr;
HashMap<StringName, HashMap<StringName, String>> descr_cache;
void _filter_changed(const String &p_text); void _filter_changed(const String &p_text);
void _make_or_edit_connection(); void _make_or_edit_connection();

View file

@ -500,10 +500,11 @@ void CreateDialog::select_type(const String &p_type, bool p_center_on_item) {
to_select->select(0); to_select->select(0);
search_options->scroll_to_item(to_select, p_center_on_item); search_options->scroll_to_item(to_select, p_center_on_item);
if (EditorHelp::get_doc_data()->class_list.has(p_type) && !DTR(EditorHelp::get_doc_data()->class_list[p_type].brief_description).is_empty()) { String text = help_bit->get_class_description(p_type);
if (!text.is_empty()) {
// Display both class name and description, since the help bit may be displayed // Display both class name and description, since the help bit may be displayed
// far away from the location (especially if the dialog was resized to be taller). // far away from the location (especially if the dialog was resized to be taller).
help_bit->set_text(vformat("[b]%s[/b]: %s", p_type, DTR(EditorHelp::get_doc_data()->class_list[p_type].brief_description))); help_bit->set_text(vformat("[b]%s[/b]: %s", p_type, text));
help_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); help_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1));
} else { } else {
// Use nested `vformat()` as translators shouldn't interfere with BBCode tags. // Use nested `vformat()` as translators shouldn't interfere with BBCode tags.

View file

@ -646,24 +646,21 @@ void EditorBuildProfileManager::_class_list_item_selected() {
Variant md = item->get_metadata(0); Variant md = item->get_metadata(0);
if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) { if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
String class_name = md; String text = description_bit->get_class_description(md);
String class_description; if (!text.is_empty()) {
// Display both class name and description, since the help bit may be displayed
DocTools *dd = EditorHelp::get_doc_data(); // far away from the location (especially if the dialog was resized to be taller).
HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_name); description_bit->set_text(vformat("[b]%s[/b]: %s", md, text));
if (E) { description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1));
class_description = DTR(E->value.brief_description); } else {
// Use nested `vformat()` as translators shouldn't interfere with BBCode tags.
description_bit->set_text(vformat(TTR("No description available for %s."), vformat("[b]%s[/b]", md)));
description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5));
} }
description_bit->set_text(class_description);
} else if (md.get_type() == Variant::INT) { } else if (md.get_type() == Variant::INT) {
int build_option_id = md; String build_option_description = EditorBuildProfile::get_build_option_description(EditorBuildProfile::BuildOption((int)md));
String build_option_description = EditorBuildProfile::get_build_option_description(EditorBuildProfile::BuildOption(build_option_id)); description_bit->set_text(vformat("[b]%s[/b]: %s", TTR(item->get_text(0)), TTRGET(build_option_description)));
description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1));
description_bit->set_text(TTRGET(build_option_description));
return;
} else {
return;
} }
} }

View file

@ -555,21 +555,22 @@ void EditorFeatureProfileManager::_class_list_item_selected() {
Variant md = item->get_metadata(0); Variant md = item->get_metadata(0);
if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) { if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
String class_name = md; String text = description_bit->get_class_description(md);
String class_description; if (!text.is_empty()) {
// Display both class name and description, since the help bit may be displayed
DocTools *dd = EditorHelp::get_doc_data(); // far away from the location (especially if the dialog was resized to be taller).
HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_name); description_bit->set_text(vformat("[b]%s[/b]: %s", md, text));
if (E) { description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1));
class_description = DTR(E->value.brief_description); } else {
// Use nested `vformat()` as translators shouldn't interfere with BBCode tags.
description_bit->set_text(vformat(TTR("No description available for %s."), vformat("[b]%s[/b]", md)));
description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5));
} }
description_bit->set_text(class_description);
} else if (md.get_type() == Variant::INT) { } else if (md.get_type() == Variant::INT) {
int feature_id = md; String feature_description = EditorFeatureProfile::get_feature_description(EditorFeatureProfile::Feature((int)md));
String feature_description = EditorFeatureProfile::get_feature_description(EditorFeatureProfile::Feature(feature_id)); description_bit->set_text(vformat("[b]%s[/b]: %s", TTR(item->get_text(0)), TTRGET(feature_description)));
description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1));
description_bit->set_text(TTRGET(feature_description));
return; return;
} else { } else {
return; return;

View file

@ -38,6 +38,7 @@
#include "doc_data_compressed.gen.h" #include "doc_data_compressed.gen.h"
#include "editor/editor_node.h" #include "editor/editor_node.h"
#include "editor/editor_paths.h" #include "editor/editor_paths.h"
#include "editor/editor_property_name_processor.h"
#include "editor/editor_scale.h" #include "editor/editor_scale.h"
#include "editor/editor_settings.h" #include "editor/editor_settings.h"
#include "editor/editor_string_names.h" #include "editor/editor_string_names.h"
@ -2587,7 +2588,7 @@ DocTools *EditorHelp::get_doc_data() {
return doc; return doc;
} }
//// EditorHelpBit /// /// EditorHelpBit ///
void EditorHelpBit::_go_to_help(String p_what) { void EditorHelpBit::_go_to_help(String p_what) {
EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT); EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT);
@ -2620,6 +2621,179 @@ void EditorHelpBit::_meta_clicked(String p_select) {
} }
} }
String EditorHelpBit::get_class_description(const StringName &p_class_name) const {
if (doc_class_cache.has(p_class_name)) {
return doc_class_cache[p_class_name];
}
String description;
HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
if (E) {
// Non-native class shouldn't be cached, nor translated.
bool is_native = ClassDB::class_exists(p_class_name);
description = is_native ? DTR(E->value.brief_description) : E->value.brief_description;
if (is_native) {
doc_class_cache[p_class_name] = description;
}
}
return description;
}
String EditorHelpBit::get_property_description(const StringName &p_class_name, const StringName &p_property_name) const {
if (doc_property_cache.has(p_class_name) && doc_property_cache[p_class_name].has(p_property_name)) {
return doc_property_cache[p_class_name][p_property_name];
}
String description;
// Non-native properties shouldn't be cached, nor translated.
bool is_native = ClassDB::class_exists(p_class_name);
DocTools *dd = EditorHelp::get_doc_data();
HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name);
if (E) {
for (int i = 0; i < E->value.properties.size(); i++) {
String description_current = is_native ? DTR(E->value.properties[i].description) : E->value.properties[i].description;
const Vector<String> class_enum = E->value.properties[i].enumeration.split(".");
const String enum_name = class_enum.size() >= 2 ? class_enum[1] : "";
if (!enum_name.is_empty()) {
// Classes can use enums from other classes, so check from which it came.
HashMap<String, DocData::ClassDoc>::ConstIterator enum_class = dd->class_list.find(class_enum[0]);
if (enum_class) {
for (DocData::ConstantDoc val : enum_class->value.constants) {
// Don't display `_MAX` enum value descriptions, as these are never exposed in the inspector.
if (val.enumeration == enum_name && !val.name.ends_with("_MAX")) {
const String enum_value = EditorPropertyNameProcessor::get_singleton()->process_name(val.name, EditorPropertyNameProcessor::STYLE_CAPITALIZED);
const String enum_prefix = EditorPropertyNameProcessor::get_singleton()->process_name(enum_name, EditorPropertyNameProcessor::STYLE_CAPITALIZED) + " ";
const String enum_description = is_native ? DTR(val.description) : val.description;
// Prettify the enum value display, so that "<ENUM NAME>_<VALUE>" becomes "Value".
description_current = description_current.trim_prefix("\n") + vformat("\n[b]%s:[/b] %s", enum_value.trim_prefix(enum_prefix), enum_description.is_empty() ? ("[i]" + DTR("No description available.") + "[/i]") : enum_description);
}
}
}
}
if (E->value.properties[i].name == p_property_name) {
description = description_current;
if (!is_native) {
break;
}
}
if (is_native) {
doc_property_cache[p_class_name][E->value.properties[i].name] = description_current;
}
}
}
return description;
}
String EditorHelpBit::get_method_description(const StringName &p_class_name, const StringName &p_method_name) const {
if (doc_method_cache.has(p_class_name) && doc_method_cache[p_class_name].has(p_method_name)) {
return doc_method_cache[p_class_name][p_method_name];
}
String description;
HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
if (E) {
// Non-native methods shouldn't be cached, nor translated.
bool is_native = ClassDB::class_exists(p_class_name);
for (int i = 0; i < E->value.methods.size(); i++) {
String description_current = is_native ? DTR(E->value.methods[i].description) : E->value.methods[i].description;
if (E->value.methods[i].name == p_method_name) {
description = description_current;
if (!is_native) {
break;
}
}
if (is_native) {
doc_method_cache[p_class_name][E->value.methods[i].name] = description_current;
}
}
}
return description;
}
String EditorHelpBit::get_signal_description(const StringName &p_class_name, const StringName &p_signal_name) const {
if (doc_signal_cache.has(p_class_name) && doc_signal_cache[p_class_name].has(p_signal_name)) {
return doc_signal_cache[p_class_name][p_signal_name];
}
String description;
HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
if (E) {
// Non-native signals shouldn't be cached, nor translated.
bool is_native = ClassDB::class_exists(p_class_name);
for (int i = 0; i < E->value.signals.size(); i++) {
String description_current = is_native ? DTR(E->value.signals[i].description) : E->value.signals[i].description;
if (E->value.signals[i].name == p_signal_name) {
description = description_current;
if (!is_native) {
break;
}
}
if (is_native) {
doc_signal_cache[p_class_name][E->value.signals[i].name] = description_current;
}
}
}
return description;
}
String EditorHelpBit::get_theme_item_description(const StringName &p_class_name, const StringName &p_theme_item_name) const {
if (doc_theme_item_cache.has(p_class_name) && doc_theme_item_cache[p_class_name].has(p_theme_item_name)) {
return doc_theme_item_cache[p_class_name][p_theme_item_name];
}
String description;
bool found = false;
DocTools *dd = EditorHelp::get_doc_data();
HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name);
while (E) {
// Non-native theme items shouldn't be cached, nor translated.
bool is_native = ClassDB::class_exists(p_class_name);
for (int i = 0; i < E->value.theme_properties.size(); i++) {
String description_current = is_native ? DTR(E->value.theme_properties[i].description) : E->value.theme_properties[i].description;
if (E->value.theme_properties[i].name == p_theme_item_name) {
description = description_current;
found = true;
if (!is_native) {
break;
}
}
if (is_native) {
doc_theme_item_cache[p_class_name][E->value.theme_properties[i].name] = description_current;
}
}
if (found || E->value.inherits.is_empty()) {
break;
}
// Check for inherited theme items.
E = dd->class_list.find(E->value.inherits);
}
return description;
}
void EditorHelpBit::_bind_methods() { void EditorHelpBit::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_text", "text"), &EditorHelpBit::set_text); ClassDB::bind_method(D_METHOD("set_text", "text"), &EditorHelpBit::set_text);
ADD_SIGNAL(MethodInfo("request_hide")); ADD_SIGNAL(MethodInfo("request_hide"));
@ -2650,7 +2824,73 @@ EditorHelpBit::EditorHelpBit() {
set_custom_minimum_size(Size2(0, 50 * EDSCALE)); set_custom_minimum_size(Size2(0, 50 * EDSCALE));
} }
//// FindBar /// /// EditorHelpTooltip ///
void EditorHelpTooltip::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_POSTINITIALIZE: {
if (!tooltip_text.is_empty()) {
parse_tooltip(tooltip_text);
}
} break;
}
}
// `p_text` is expected to be something like these:
// - `class|Control||`;
// - `property|Control|size|`;
// - `signal|Control|gui_input|(event: InputEvent)`
void EditorHelpTooltip::parse_tooltip(const String &p_text) {
tooltip_text = p_text;
PackedStringArray slices = p_text.split("|", true, 3);
ERR_FAIL_COND_MSG(slices.size() < 4, "Invalid tooltip formatting. The expect string should be formatted as 'type|class|property|args'.");
String type = slices[0];
String class_name = slices[1];
String property_name = slices[2];
String property_args = slices[3];
String title;
String description;
String formatted_text;
if (type == "class") {
title = class_name;
description = get_class_description(class_name);
formatted_text = TTR("Class:");
} else {
title = property_name;
if (type == "property") {
description = get_property_description(class_name, property_name);
formatted_text = TTR("Property:");
} else if (type == "method") {
description = get_method_description(class_name, property_name);
formatted_text = TTR("Method:");
} else if (type == "signal") {
description = get_signal_description(class_name, property_name);
formatted_text = TTR("Signal:");
} else if (type == "theme_item") {
description = get_theme_item_description(class_name, property_name);
formatted_text = TTR("Theme Item:");
} else {
ERR_FAIL_MSG("Invalid tooltip type '" + type + "'. Valid types are 'class', 'property', 'method', 'signal', and 'theme_item'.");
}
}
formatted_text += " [u][b]" + title + "[/b][/u]" + property_args + "\n";
formatted_text += description.is_empty() ? "[i]" + TTR("No description available.") + "[/i]" : description;
set_text(formatted_text);
}
EditorHelpTooltip::EditorHelpTooltip(const String &p_text) {
tooltip_text = p_text;
get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 0));
}
/// FindBar ///
FindBar::FindBar() { FindBar::FindBar() {
search_text = memnew(LineEdit); search_text = memnew(LineEdit);

View file

@ -232,6 +232,12 @@ public:
class EditorHelpBit : public MarginContainer { class EditorHelpBit : public MarginContainer {
GDCLASS(EditorHelpBit, MarginContainer); GDCLASS(EditorHelpBit, MarginContainer);
inline static HashMap<StringName, String> doc_class_cache;
inline static HashMap<StringName, HashMap<StringName, String>> doc_property_cache;
inline static HashMap<StringName, HashMap<StringName, String>> doc_method_cache;
inline static HashMap<StringName, HashMap<StringName, String>> doc_signal_cache;
inline static HashMap<StringName, HashMap<StringName, String>> doc_theme_item_cache;
RichTextLabel *rich_text = nullptr; RichTextLabel *rich_text = nullptr;
void _go_to_help(String p_what); void _go_to_help(String p_what);
void _meta_clicked(String p_select); void _meta_clicked(String p_select);
@ -243,9 +249,30 @@ protected:
void _notification(int p_what); void _notification(int p_what);
public: public:
String get_class_description(const StringName &p_class_name) const;
String get_property_description(const StringName &p_class_name, const StringName &p_property_name) const;
String get_method_description(const StringName &p_class_name, const StringName &p_method_name) const;
String get_signal_description(const StringName &p_class_name, const StringName &p_signal_name) const;
String get_theme_item_description(const StringName &p_class_name, const StringName &p_theme_item_name) const;
RichTextLabel *get_rich_text() { return rich_text; } RichTextLabel *get_rich_text() { return rich_text; }
void set_text(const String &p_text); void set_text(const String &p_text);
EditorHelpBit(); EditorHelpBit();
}; };
class EditorHelpTooltip : public EditorHelpBit {
GDCLASS(EditorHelpTooltip, EditorHelpBit);
String tooltip_text;
protected:
void _notification(int p_what);
public:
void parse_tooltip(const String &p_text);
EditorHelpTooltip(const String &p_text = String());
};
#endif // EDITOR_HELP_H #endif // EDITOR_HELP_H

View file

@ -905,47 +905,17 @@ void EditorProperty::_update_pin_flags() {
} }
} }
static Control *make_help_bit(const String &p_item_type, const String &p_text, const String &p_warning, const Color &p_warn_color) {
// `p_text` is expected to be something like this:
// `item_name|Item description.`.
// Note that the description can be empty or contain `|`.
PackedStringArray slices = p_text.split("|", true, 1);
if (slices.size() < 2) {
return nullptr; // Use default tooltip instead.
}
String item_name = slices[0].strip_edges();
String item_descr = slices[1].strip_edges();
String text;
if (!p_item_type.is_empty()) {
text = p_item_type + " ";
}
text += "[u][b]" + item_name + "[/b][/u]\n";
if (item_descr.is_empty()) {
text += "[i]" + TTR("No description.") + "[/i]";
} else {
text += item_descr;
}
if (!p_warning.is_empty()) {
text += "\n[b][color=" + p_warn_color.to_html(false) + "]" + p_warning + "[/color][/b]";
}
EditorHelpBit *help_bit = memnew(EditorHelpBit);
help_bit->get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 1));
help_bit->set_text(text);
return help_bit;
}
Control *EditorProperty::make_custom_tooltip(const String &p_text) const { Control *EditorProperty::make_custom_tooltip(const String &p_text) const {
String warn; EditorHelpTooltip *tooltip = memnew(EditorHelpTooltip(p_text));
Color warn_color;
if (object->has_method("_get_property_warning")) { if (object->has_method("_get_property_warning")) {
warn = object->call("_get_property_warning", property); String warn = object->call("_get_property_warning", property);
warn_color = get_theme_color(SNAME("warning_color")); if (!warn.is_empty()) {
tooltip->set_text(tooltip->get_rich_text()->get_text() + "\n[b][color=" + get_theme_color(SNAME("warning_color")).to_html(false) + "]" + warn + "[/color][/b]");
}
} }
return make_help_bit(TTR("Property:"), p_text, warn, warn_color);
return tooltip;
} }
void EditorProperty::menu_option(int p_option) { void EditorProperty::menu_option(int p_option) {
@ -1178,7 +1148,8 @@ void EditorInspectorCategory::_notification(int p_what) {
} }
Control *EditorInspectorCategory::make_custom_tooltip(const String &p_text) const { Control *EditorInspectorCategory::make_custom_tooltip(const String &p_text) const {
return make_help_bit(TTR("Class:"), p_text, String(), Color()); // Far from perfect solution, as there's nothing that prevents a category from having a name that starts with that.
return p_text.begins_with("class|") ? memnew(EditorHelpTooltip(p_text)) : nullptr;
} }
Size2 EditorInspectorCategory::get_minimum_size() const { Size2 EditorInspectorCategory::get_minimum_size() const {
@ -2883,24 +2854,8 @@ void EditorInspector::update_tree() {
category->doc_class_name = doc_name; category->doc_class_name = doc_name;
if (use_doc_hints) { if (use_doc_hints) {
String descr = ""; // `|` separator used in `EditorHelpTooltip` for formatting.
// Sets the category tooltip to show documentation. category->set_tooltip_text("class|" + doc_name + "||");
if (!class_descr_cache.has(doc_name)) {
DocTools *dd = EditorHelp::get_doc_data();
HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(doc_name);
if (E) {
descr = E->value.brief_description;
}
if (ClassDB::class_exists(doc_name)) {
descr = DTR(descr); // Do not translate the class description of scripts.
class_descr_cache[doc_name] = descr; // Do not cache the class description of scripts.
}
} else {
descr = class_descr_cache[doc_name];
}
// `|` separator used in `make_help_bit()` for formatting.
category->set_tooltip_text(p.name + "|" + descr);
} }
// Add editors at the start of a category. // Add editors at the start of a category.
@ -3195,13 +3150,12 @@ void EditorInspector::update_tree() {
restart_request_props.insert(p.name); restart_request_props.insert(p.name);
} }
PropertyDocInfo doc_info; String doc_path;
String theme_item_name;
StringName classname = doc_name;
// Build the doc hint, to use as tooltip.
if (use_doc_hints) { if (use_doc_hints) {
// Build the doc hint, to use as tooltip.
// Get the class name.
StringName classname = doc_name;
if (!object_class.is_empty()) { if (!object_class.is_empty()) {
classname = object_class; classname = object_class;
} else if (Object::cast_to<MultiNodeEdit>(object)) { } else if (Object::cast_to<MultiNodeEdit>(object)) {
@ -3231,83 +3185,55 @@ void EditorInspector::update_tree() {
classname = get_edited_object()->get_class(); classname = get_edited_object()->get_class();
} }
// Search for the property description in the cache. // Search for the doc path in the cache.
HashMap<StringName, HashMap<StringName, PropertyDocInfo>>::Iterator E = doc_info_cache.find(classname); HashMap<StringName, HashMap<StringName, String>>::Iterator E = doc_path_cache.find(classname);
if (E) { if (E) {
HashMap<StringName, PropertyDocInfo>::Iterator F = E->value.find(propname); HashMap<StringName, String>::Iterator F = E->value.find(propname);
if (F) { if (F) {
found = true; found = true;
doc_info = F->value; doc_path = F->value;
} }
} }
if (!found) { if (!found) {
DocTools *dd = EditorHelp::get_doc_data();
// Do not cache the doc path information of scripts.
bool is_native_class = ClassDB::class_exists(classname); bool is_native_class = ClassDB::class_exists(classname);
// Build the property description String and add it to the cache.
DocTools *dd = EditorHelp::get_doc_data();
HashMap<String, DocData::ClassDoc>::ConstIterator F = dd->class_list.find(classname); HashMap<String, DocData::ClassDoc>::ConstIterator F = dd->class_list.find(classname);
while (F && doc_info.description.is_empty()) { while (F) {
for (int i = 0; i < F->value.properties.size(); i++) {
if (F->value.properties[i].name == propname.operator String()) {
doc_info.description = F->value.properties[i].description;
if (is_native_class) {
doc_info.description = DTR(doc_info.description); // Do not translate the property description of scripts.
}
const Vector<String> class_enum = F->value.properties[i].enumeration.split(".");
const String class_name = class_enum[0];
const String enum_name = class_enum.size() >= 2 ? class_enum[1] : "";
if (!enum_name.is_empty()) {
HashMap<String, DocData::ClassDoc>::ConstIterator enum_class = dd->class_list.find(class_name);
if (enum_class) {
for (DocData::ConstantDoc val : enum_class->value.constants) {
// Don't display `_MAX` enum value descriptions, as these are never exposed in the inspector.
if (val.enumeration == enum_name && !val.name.ends_with("_MAX")) {
const String enum_value = EditorPropertyNameProcessor::get_singleton()->process_name(val.name, EditorPropertyNameProcessor::STYLE_CAPITALIZED);
// Prettify the enum value display, so that "<ENUM NAME>_<VALUE>" becomes "Value".
String desc = val.description;
if (is_native_class) {
desc = DTR(desc); // Do not translate the enum value description of scripts.
}
desc = desc.trim_prefix("\n");
doc_info.description += vformat(
"\n[b]%s:[/b] %s",
enum_value.trim_prefix(EditorPropertyNameProcessor::get_singleton()->process_name(enum_name, EditorPropertyNameProcessor::STYLE_CAPITALIZED) + " "),
desc.is_empty() ? ("[i]" + TTR("No description.") + "[/i]") : desc);
}
}
}
}
doc_info.path = "class_property:" + F->value.name + ":" + F->value.properties[i].name;
break;
}
}
Vector<String> slices = propname.operator String().split("/"); Vector<String> slices = propname.operator String().split("/");
// Check if it's a theme item first.
if (slices.size() == 2 && slices[0].begins_with("theme_override_")) { if (slices.size() == 2 && slices[0].begins_with("theme_override_")) {
for (int i = 0; i < F->value.theme_properties.size(); i++) { for (int i = 0; i < F->value.theme_properties.size(); i++) {
String doc_path_current = "class_theme_item:" + F->value.name + ":" + F->value.theme_properties[i].name;
if (F->value.theme_properties[i].name == slices[1]) { if (F->value.theme_properties[i].name == slices[1]) {
doc_info.description = F->value.theme_properties[i].description; doc_path = doc_path_current;
if (is_native_class) { theme_item_name = F->value.theme_properties[i].name;
doc_info.description = DTR(doc_info.description); // Do not translate the theme item description of scripts. }
} }
doc_info.path = "class_theme_item:" + F->value.name + ":" + F->value.theme_properties[i].name;
break; if (is_native_class) {
doc_path_cache[classname][propname] = doc_path;
}
} else {
for (int i = 0; i < F->value.properties.size(); i++) {
String doc_path_current = "class_property:" + F->value.name + ":" + F->value.properties[i].name;
if (F->value.properties[i].name == propname.operator String()) {
doc_path = doc_path_current;
}
if (is_native_class) {
doc_path_cache[classname][propname] = doc_path;
} }
} }
} }
if (!F->value.inherits.is_empty()) { if (!doc_path.is_empty() || F->value.inherits.is_empty()) {
F = dd->class_list.find(F->value.inherits);
} else {
break; break;
} }
} // Couldn't find the doc path in the class itself, try its super class.
F = dd->class_list.find(F->value.inherits);
if (is_native_class) {
doc_info_cache[classname][propname] = doc_info; // Do not cache the doc information of scripts.
} }
} }
} }
@ -3346,11 +3272,11 @@ void EditorInspector::update_tree() {
if (properties.size()) { if (properties.size()) {
if (properties.size() == 1) { if (properties.size() == 1) {
//since it's one, associate: // Since it's one, associate:
ep->property = properties[0]; ep->property = properties[0];
ep->property_path = property_prefix + properties[0]; ep->property_path = property_prefix + properties[0];
ep->property_usage = p.usage; ep->property_usage = p.usage;
//and set label? // And set label?
} }
if (!editors[i].label.is_empty()) { if (!editors[i].label.is_empty()) {
ep->set_label(editors[i].label); ep->set_label(editors[i].label);
@ -3398,9 +3324,17 @@ void EditorInspector::update_tree() {
ep->connect("multiple_properties_changed", callable_mp(this, &EditorInspector::_multiple_properties_changed)); ep->connect("multiple_properties_changed", callable_mp(this, &EditorInspector::_multiple_properties_changed));
ep->connect("resource_selected", callable_mp(this, &EditorInspector::_resource_selected), CONNECT_DEFERRED); ep->connect("resource_selected", callable_mp(this, &EditorInspector::_resource_selected), CONNECT_DEFERRED);
ep->connect("object_id_selected", callable_mp(this, &EditorInspector::_object_id_selected), CONNECT_DEFERRED); ep->connect("object_id_selected", callable_mp(this, &EditorInspector::_object_id_selected), CONNECT_DEFERRED);
// `|` separator used in `make_help_bit()` for formatting.
ep->set_tooltip_text(property_prefix + p.name + "|" + doc_info.description); if (use_doc_hints) {
ep->set_doc_path(doc_info.path); // `|` separator used in `EditorHelpTooltip` for formatting.
if (theme_item_name.is_empty()) {
ep->set_tooltip_text("property|" + classname + "|" + property_prefix + p.name + "|");
} else {
ep->set_tooltip_text("theme_item|" + classname + "|" + theme_item_name + "|");
}
}
ep->set_doc_path(doc_path);
ep->update_property(); ep->update_property();
ep->_update_pin_flags(); ep->_update_pin_flags();
ep->update_editor_property_status(); ep->update_editor_property_status();

View file

@ -501,13 +501,7 @@ class EditorInspector : public ScrollContainer {
int property_focusable; int property_focusable;
int update_scroll_request; int update_scroll_request;
struct PropertyDocInfo { HashMap<StringName, HashMap<StringName, String>> doc_path_cache;
String description;
String path;
};
HashMap<StringName, HashMap<StringName, PropertyDocInfo>> doc_info_cache;
HashMap<StringName, String> class_descr_cache;
HashSet<StringName> restart_request_props; HashSet<StringName> restart_request_props;
HashMap<ObjectID, int> scroll_cache; HashMap<ObjectID, int> scroll_cache;

View file

@ -370,46 +370,15 @@ void PropertySelector::_item_selected() {
class_type = instance->get_class(); class_type = instance->get_class();
} }
DocTools *dd = EditorHelp::get_doc_data();
String text; String text;
if (properties) { while (!class_type.is_empty()) {
while (!class_type.is_empty()) { text = properties ? help_bit->get_property_description(class_type, name) : help_bit->get_method_description(class_type, name);
HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_type); if (!text.is_empty()) {
if (E) { break;
for (int i = 0; i < E->value.properties.size(); i++) {
if (E->value.properties[i].name == name) {
text = DTR(E->value.properties[i].description);
break;
}
}
}
if (!text.is_empty()) {
break;
}
// The property may be from a parent class, keep looking.
class_type = ClassDB::get_parent_class(class_type);
} }
} else {
while (!class_type.is_empty()) {
HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_type);
if (E) {
for (int i = 0; i < E->value.methods.size(); i++) {
if (E->value.methods[i].name == name) {
text = DTR(E->value.methods[i].description);
break;
}
}
}
if (!text.is_empty()) { // It may be from a parent class, keep looking.
break; class_type = ClassDB::get_parent_class(class_type);
}
// The method may be from a parent class, keep looking.
class_type = ClassDB::get_parent_class(class_type);
}
} }
if (!text.is_empty()) { if (!text.is_empty()) {