Browse Source

Merge pull request #82051 from YeldhamDev/i_just_wanted_to_add_tooltips_to_theme_items_man

Revamp how documentation tooltips work
Rémi Verschelde 1 year ago
parent
commit
bb30c8377c

+ 11 - 74
editor/connections_dialog.cpp

@@ -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 {
-	// `p_text` is expected to be something like this:
-	// - `class|Control||Control brief description.`;
-	// - `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;
+	// If it's not a doc tooltip, fallback to the default one.
+	return p_text.contains("::") ? nullptr : memnew(EditorHelpTooltip(p_text));
 }
 
 struct _ConnectionsDockMethodInfoSort {
@@ -1341,7 +1315,6 @@ void ConnectionsDock::update_tree() {
 	while (native_base != StringName()) {
 		String class_name;
 		String doc_class_name;
-		String class_brief;
 		Ref<Texture2D> class_icon;
 		List<MethodInfo> class_signals;
 
@@ -1355,21 +1328,8 @@ void ConnectionsDock::update_tree() {
 			if (doc_class_name.is_empty()) {
 				doc_class_name = script_base->get_path().trim_prefix("res://").quote();
 			}
-
-			// For a script class, the cache is filled each time.
-			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();
-				}
+			if (!doc_class_name.is_empty() && !doc_data->class_list.find(doc_class_name)) {
+				doc_class_name = String();
 			}
 
 			class_icon = editor_data.get_script_icon(script_base);
@@ -1398,18 +1358,9 @@ void ConnectionsDock::update_tree() {
 			script_base = base;
 		} else {
 			class_name = native_base;
-			doc_class_name = class_name;
-
-			HashMap<String, DocData::ClassDoc>::ConstIterator F = 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 = native_base;
+
+			if (!doc_data->class_list.find(doc_class_name)) {
 				doc_class_name = String();
 			}
 
@@ -1434,8 +1385,8 @@ void ConnectionsDock::update_tree() {
 
 			section_item = tree->create_item(root);
 			section_item->set_text(0, class_name);
-			// `|` separators used in `make_custom_tooltip()` for formatting.
-			section_item->set_tooltip_text(0, "class|" + class_name + "||" + class_brief);
+			// `|` separators used in `EditorHelpTooltip` for formatting.
+			section_item->set_tooltip_text(0, "class|" + doc_class_name + "||");
 			section_item->set_icon(0, class_icon);
 			section_item->set_selectable(0, false);
 			section_item->set_editable(0, false);
@@ -1466,22 +1417,8 @@ void ConnectionsDock::update_tree() {
 			sinfo["args"] = argnames;
 			signal_item->set_metadata(0, sinfo);
 			signal_item->set_icon(0, get_editor_theme_icon(SNAME("Signal")));
-
-			// Set tooltip with the signal's documentation.
-			{
-				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);
-			}
+			// `|` separators used in `EditorHelpTooltip` for formatting.
+			signal_item->set_tooltip_text(0, "signal|" + doc_class_name + "|" + String(signal_name) + "|" + signame.trim_prefix(mi.name));
 
 			// List existing connections.
 			List<Object::Connection> existing_connections;

+ 0 - 2
editor/connections_dialog.h

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

+ 3 - 2
editor/create_dialog.cpp

@@ -500,10 +500,11 @@ void CreateDialog::select_type(const String &p_type, bool p_center_on_item) {
 	to_select->select(0);
 	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
 		// 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));
 	} else {
 		// Use nested `vformat()` as translators shouldn't interfere with BBCode tags.

+ 13 - 16
editor/editor_build_profile.cpp

@@ -646,24 +646,21 @@ void EditorBuildProfileManager::_class_list_item_selected() {
 
 	Variant md = item->get_metadata(0);
 	if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
-		String class_name = md;
-		String class_description;
-
-		DocTools *dd = EditorHelp::get_doc_data();
-		HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_name);
-		if (E) {
-			class_description = DTR(E->value.brief_description);
+		String text = description_bit->get_class_description(md);
+		if (!text.is_empty()) {
+			// 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).
+			description_bit->set_text(vformat("[b]%s[/b]: %s", md, text));
+			description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1));
+		} 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) {
-		int build_option_id = md;
-		String build_option_description = EditorBuildProfile::get_build_option_description(EditorBuildProfile::BuildOption(build_option_id));
-
-		description_bit->set_text(TTRGET(build_option_description));
-		return;
-	} else {
-		return;
+		String build_option_description = EditorBuildProfile::get_build_option_description(EditorBuildProfile::BuildOption((int)md));
+		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));
 	}
 }
 

+ 13 - 12
editor/editor_feature_profile.cpp

@@ -555,21 +555,22 @@ void EditorFeatureProfileManager::_class_list_item_selected() {
 
 	Variant md = item->get_metadata(0);
 	if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
-		String class_name = md;
-		String class_description;
-
-		DocTools *dd = EditorHelp::get_doc_data();
-		HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_name);
-		if (E) {
-			class_description = DTR(E->value.brief_description);
+		String text = description_bit->get_class_description(md);
+		if (!text.is_empty()) {
+			// 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).
+			description_bit->set_text(vformat("[b]%s[/b]: %s", md, text));
+			description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1));
+		} 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) {
-		int feature_id = md;
-		String feature_description = EditorFeatureProfile::get_feature_description(EditorFeatureProfile::Feature(feature_id));
+		String feature_description = EditorFeatureProfile::get_feature_description(EditorFeatureProfile::Feature((int)md));
+		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;
 	} else {
 		return;

+ 242 - 2
editor/editor_help.cpp

@@ -38,6 +38,7 @@
 #include "doc_data_compressed.gen.h"
 #include "editor/editor_node.h"
 #include "editor/editor_paths.h"
+#include "editor/editor_property_name_processor.h"
 #include "editor/editor_scale.h"
 #include "editor/editor_settings.h"
 #include "editor/editor_string_names.h"
@@ -2587,7 +2588,7 @@ DocTools *EditorHelp::get_doc_data() {
 	return doc;
 }
 
-//// EditorHelpBit ///
+/// EditorHelpBit ///
 
 void EditorHelpBit::_go_to_help(String p_what) {
 	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() {
 	ClassDB::bind_method(D_METHOD("set_text", "text"), &EditorHelpBit::set_text);
 	ADD_SIGNAL(MethodInfo("request_hide"));
@@ -2650,7 +2824,73 @@ EditorHelpBit::EditorHelpBit() {
 	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() {
 	search_text = memnew(LineEdit);

+ 27 - 0
editor/editor_help.h

@@ -232,6 +232,12 @@ public:
 class EditorHelpBit : public 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;
 	void _go_to_help(String p_what);
 	void _meta_clicked(String p_select);
@@ -243,9 +249,30 @@ protected:
 	void _notification(int p_what);
 
 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; }
 	void set_text(const String &p_text);
+
 	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

+ 58 - 124
editor/editor_inspector.cpp

@@ -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 {
-	String warn;
-	Color warn_color;
+	EditorHelpTooltip *tooltip = memnew(EditorHelpTooltip(p_text));
+
 	if (object->has_method("_get_property_warning")) {
-		warn = object->call("_get_property_warning", property);
-		warn_color = get_theme_color(SNAME("warning_color"));
+		String warn = object->call("_get_property_warning", property);
+		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) {
@@ -1178,7 +1148,8 @@ void EditorInspectorCategory::_notification(int p_what) {
 }
 
 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 {
@@ -2883,24 +2854,8 @@ void EditorInspector::update_tree() {
 			category->doc_class_name = doc_name;
 
 			if (use_doc_hints) {
-				String descr = "";
-				// Sets the category tooltip to show documentation.
-				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);
+				// `|` separator used in `EditorHelpTooltip` for formatting.
+				category->set_tooltip_text("class|" + doc_name + "||");
 			}
 
 			// Add editors at the start of a category.
@@ -3195,13 +3150,12 @@ void EditorInspector::update_tree() {
 			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) {
-			// Build the doc hint, to use as tooltip.
-
-			// Get the class name.
-			StringName classname = doc_name;
 			if (!object_class.is_empty()) {
 				classname = object_class;
 			} else if (Object::cast_to<MultiNodeEdit>(object)) {
@@ -3231,83 +3185,55 @@ void EditorInspector::update_tree() {
 				classname = get_edited_object()->get_class();
 			}
 
-			// Search for the property description in the cache.
-			HashMap<StringName, HashMap<StringName, PropertyDocInfo>>::Iterator E = doc_info_cache.find(classname);
+			// Search for the doc path in the cache.
+			HashMap<StringName, HashMap<StringName, String>>::Iterator E = doc_path_cache.find(classname);
 			if (E) {
-				HashMap<StringName, PropertyDocInfo>::Iterator F = E->value.find(propname);
+				HashMap<StringName, String>::Iterator F = E->value.find(propname);
 				if (F) {
 					found = true;
-					doc_info = F->value;
+					doc_path = F->value;
 				}
 			}
 
 			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);
 
-				// 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);
-				while (F && doc_info.description.is_empty()) {
-					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;
-						}
-					}
-
+				while (F) {
 					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_")) {
 						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]) {
-								doc_info.description = F->value.theme_properties[i].description;
-								if (is_native_class) {
-									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;
+								doc_path = doc_path_current;
+								theme_item_name = F->value.theme_properties[i].name;
 							}
 						}
-					}
 
-					if (!F->value.inherits.is_empty()) {
-						F = dd->class_list.find(F->value.inherits);
+						if (is_native_class) {
+							doc_path_cache[classname][propname] = doc_path;
+						}
 					} else {
-						break;
+						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 (is_native_class) {
-					doc_info_cache[classname][propname] = doc_info; // Do not cache the doc information of scripts.
+					if (!doc_path.is_empty() || F->value.inherits.is_empty()) {
+						break;
+					}
+					// Couldn't find the doc path in the class itself, try its super class.
+					F = dd->class_list.find(F->value.inherits);
 				}
 			}
 		}
@@ -3346,11 +3272,11 @@ void EditorInspector::update_tree() {
 
 				if (properties.size()) {
 					if (properties.size() == 1) {
-						//since it's one, associate:
+						// Since it's one, associate:
 						ep->property = properties[0];
 						ep->property_path = property_prefix + properties[0];
 						ep->property_usage = p.usage;
-						//and set label?
+						// And set label?
 					}
 					if (!editors[i].label.is_empty()) {
 						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("resource_selected", callable_mp(this, &EditorInspector::_resource_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);
-				ep->set_doc_path(doc_info.path);
+
+				if (use_doc_hints) {
+					// `|` 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_pin_flags();
 				ep->update_editor_property_status();

+ 1 - 7
editor/editor_inspector.h

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

+ 6 - 37
editor/property_selector.cpp

@@ -370,46 +370,15 @@ void PropertySelector::_item_selected() {
 		class_type = instance->get_class();
 	}
 
-	DocTools *dd = EditorHelp::get_doc_data();
 	String text;
-	if (properties) {
-		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.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);
+	while (!class_type.is_empty()) {
+		text = properties ? help_bit->get_property_description(class_type, name) : help_bit->get_method_description(class_type, name);
+		if (!text.is_empty()) {
+			break;
 		}
-	} 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()) {
-				break;
-			}
-
-			// The method may be from a parent class, keep looking.
-			class_type = ClassDB::get_parent_class(class_type);
-		}
+		// It may be from a parent class, keep looking.
+		class_type = ClassDB::get_parent_class(class_type);
 	}
 
 	if (!text.is_empty()) {