Browse Source

Editor: Display deprecated/experimental messages in tooltips

Danil Alexeev 1 year ago
parent
commit
a714cb9f65

+ 12 - 5
editor/connections_dialog.cpp

@@ -47,6 +47,7 @@
 #include "scene/gui/check_box.h"
 #include "scene/gui/label.h"
 #include "scene/gui/line_edit.h"
+#include "scene/gui/margin_container.h"
 #include "scene/gui/option_button.h"
 #include "scene/gui/popup_menu.h"
 #include "scene/gui/spin_box.h"
@@ -872,7 +873,13 @@ ConnectDialog::~ConnectDialog() {
 
 Control *ConnectionsDockTree::make_custom_tooltip(const String &p_text) const {
 	// If it's not a doc tooltip, fallback to the default one.
-	return p_text.contains("::") ? nullptr : memnew(EditorHelpTooltip(p_text));
+	if (p_text.contains("::")) {
+		return nullptr;
+	}
+
+	EditorHelpBit *help_bit = memnew(EditorHelpBit(p_text));
+	EditorHelpBitTooltip::show_tooltip(help_bit, const_cast<ConnectionsDockTree *>(this));
+	return memnew(Control); // Make the standard tooltip invisible.
 }
 
 struct _ConnectionsDockMethodInfoSort {
@@ -1458,8 +1465,8 @@ void ConnectionsDock::update_tree() {
 
 			section_item = tree->create_item(root);
 			section_item->set_text(0, class_name);
-			// `|` separators used in `EditorHelpTooltip` for formatting.
-			section_item->set_tooltip_text(0, "class|" + doc_class_name + "||");
+			// `|` separators used in `EditorHelpBit`.
+			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);
@@ -1490,8 +1497,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")));
-			// `|` separators used in `EditorHelpTooltip` for formatting.
-			signal_item->set_tooltip_text(0, "signal|" + doc_class_name + "|" + String(signal_name) + "|" + signame.trim_prefix(mi.name));
+			// `|` separators used in `EditorHelpBit`.
+			signal_item->set_tooltip_text(0, "signal|" + doc_class_name + "|" + String(signal_name));
 
 			// List existing connections.
 			List<Object::Connection> existing_connections;

+ 1 - 1
editor/connections_dialog.h

@@ -191,7 +191,7 @@ public:
 
 //////////////////////////////////////////
 
-// Custom `Tree` needed to use `EditorHelpTooltip` to display signal documentation.
+// Custom `Tree` needed to use `EditorHelpBit` to display signal documentation.
 class ConnectionsDockTree : public Tree {
 	virtual Control *make_custom_tooltip(const String &p_text) const;
 };

+ 3 - 13
editor/create_dialog.cpp

@@ -202,8 +202,7 @@ void CreateDialog::_update_search() {
 		select_type(_top_result(candidates, search_text));
 	} else {
 		favorite->set_disabled(true);
-		help_bit->set_text(vformat(TTR("No results for \"%s\"."), search_text));
-		help_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5));
+		help_bit->set_custom_text(String(), String(), vformat(TTR("No results for \"%s\"."), search_text.replace("[", "[lb]")));
 		get_ok_button()->set_disabled(true);
 		search_options->deselect_all();
 	}
@@ -502,17 +501,7 @@ 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);
 
-	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, 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.
-		help_bit->set_text(vformat(TTR("No description available for %s."), vformat("[b]%s[/b]", p_type)));
-		help_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5));
-	}
+	help_bit->parse_symbol("class|" + p_type + "|");
 
 	favorite->set_disabled(false);
 	favorite->set_pressed(favorite_list.has(p_type));
@@ -837,6 +826,7 @@ CreateDialog::CreateDialog() {
 	vbc->add_margin_child(TTR("Matches:"), search_options, true);
 
 	help_bit = memnew(EditorHelpBit);
+	help_bit->set_content_height_limits(64 * EDSCALE, 64 * EDSCALE);
 	help_bit->connect("request_hide", callable_mp(this, &CreateDialog::_hide_requested));
 	vbc->add_margin_child(TTR("Description:"), help_bit);
 

+ 8 - 14
editor/editor_build_profile.cpp

@@ -593,6 +593,10 @@ void EditorBuildProfileManager::_action_confirm() {
 	}
 }
 
+void EditorBuildProfileManager::_hide_requested() {
+	_cancel_pressed(); // From AcceptDialog.
+}
+
 void EditorBuildProfileManager::_fill_classes_from(TreeItem *p_parent, const String &p_class, const String &p_selected) {
 	TreeItem *class_item = class_list->create_item(p_parent);
 	class_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
@@ -646,21 +650,10 @@ 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 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->parse_symbol("class|" + md.operator String() + "|");
 	} else if (md.get_type() == Variant::INT) {
 		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));
+		description_bit->set_custom_text(TTR(item->get_text(0)), String(), TTRGET(build_option_description));
 	}
 }
 
@@ -864,7 +857,8 @@ EditorBuildProfileManager::EditorBuildProfileManager() {
 	main_vbc->add_margin_child(TTR("Configure Engine Compilation Profile:"), class_list, true);
 
 	description_bit = memnew(EditorHelpBit);
-	description_bit->set_custom_minimum_size(Size2(0, 80) * EDSCALE);
+	description_bit->set_content_height_limits(80 * EDSCALE, 80 * EDSCALE);
+	description_bit->connect("request_hide", callable_mp(this, &EditorBuildProfileManager::_hide_requested));
 	main_vbc->add_margin_child(TTR("Description:"), description_bit, false);
 
 	confirm_dialog = memnew(ConfirmationDialog);

+ 1 - 0
editor/editor_build_profile.h

@@ -150,6 +150,7 @@ class EditorBuildProfileManager : public AcceptDialog {
 
 	void _profile_action(int p_action);
 	void _action_confirm();
+	void _hide_requested();
 
 	void _update_edited_profile();
 	void _fill_classes_from(TreeItem *p_parent, const String &p_class, const String &p_selected);

+ 8 - 15
editor/editor_feature_profile.cpp

@@ -493,6 +493,10 @@ void EditorFeatureProfileManager::_profile_selected(int p_what) {
 	_update_selected_profile();
 }
 
+void EditorFeatureProfileManager::_hide_requested() {
+	_cancel_pressed(); // From AcceptDialog.
+}
+
 void EditorFeatureProfileManager::_fill_classes_from(TreeItem *p_parent, const String &p_class, const String &p_selected) {
 	TreeItem *class_item = class_list->create_item(p_parent);
 	class_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
@@ -555,22 +559,10 @@ 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 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->parse_symbol("class|" + md.operator String() + "|");
 	} else if (md.get_type() == Variant::INT) {
 		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_custom_text(TTR(item->get_text(0)), String(), TTRGET(feature_description));
 		return;
 	} else {
 		return;
@@ -991,8 +983,9 @@ EditorFeatureProfileManager::EditorFeatureProfileManager() {
 	property_list_vbc->set_h_size_flags(Control::SIZE_EXPAND_FILL);
 
 	description_bit = memnew(EditorHelpBit);
+	description_bit->set_content_height_limits(80 * EDSCALE, 80 * EDSCALE);
+	description_bit->connect("request_hide", callable_mp(this, &EditorFeatureProfileManager::_hide_requested));
 	property_list_vbc->add_margin_child(TTR("Description:"), description_bit, false);
-	description_bit->set_custom_minimum_size(Size2(0, 80) * EDSCALE);
 
 	property_list = memnew(Tree);
 	property_list_vbc->add_margin_child(TTR("Extra Options:"), property_list, true);

+ 1 - 0
editor/editor_feature_profile.h

@@ -142,6 +142,7 @@ class EditorFeatureProfileManager : public AcceptDialog {
 
 	void _profile_action(int p_action);
 	void _profile_selected(int p_what);
+	void _hide_requested();
 
 	String current_profile;
 	void _update_profile_list(const String &p_select_profile = String());

+ 622 - 238
editor/editor_help.cpp

@@ -30,6 +30,7 @@
 
 #include "editor_help.h"
 
+#include "core/config/project_settings.h"
 #include "core/core_constants.h"
 #include "core/extension/gdextension.h"
 #include "core/input/input.h"
@@ -193,7 +194,6 @@ void EditorHelp::_update_theme_item_cache() {
 	class_desc->add_theme_font_override("normal_font", theme_cache.doc_font);
 	class_desc->add_theme_font_size_override("normal_font_size", theme_cache.doc_font_size);
 
-	class_desc->add_theme_color_override("selection_color", get_theme_color(SNAME("selection_color"), SNAME("EditorHelp")));
 	class_desc->add_theme_constant_override("line_separation", get_theme_constant(SNAME("line_separation"), SNAME("EditorHelp")));
 	class_desc->add_theme_constant_override("table_h_separation", get_theme_constant(SNAME("table_h_separation"), SNAME("EditorHelp")));
 	class_desc->add_theme_constant_override("table_v_separation", get_theme_constant(SNAME("table_v_separation"), SNAME("EditorHelp")));
@@ -222,29 +222,35 @@ void EditorHelp::_class_list_select(const String &p_select) {
 }
 
 void EditorHelp::_class_desc_select(const String &p_select) {
-	if (p_select.begins_with("$")) { // enum
-		String select = p_select.substr(1, p_select.length());
-		String class_name;
-		int rfind = select.rfind(".");
-		if (rfind != -1) {
-			class_name = select.substr(0, rfind);
-			select = select.substr(rfind + 1);
+	if (p_select.begins_with("$")) { // Enum.
+		const String link = p_select.substr(1);
+
+		String enum_class_name;
+		String enum_name;
+		if (CoreConstants::is_global_enum(link)) {
+			enum_class_name = "@GlobalScope";
+			enum_name = link;
 		} else {
-			class_name = "@GlobalScope";
+			const int dot_pos = link.rfind(".");
+			if (dot_pos >= 0) {
+				enum_class_name = link.left(dot_pos);
+				enum_name = link.substr(dot_pos + 1);
+			} else {
+				enum_class_name = edited_class;
+				enum_name = link;
+			}
 		}
-		emit_signal(SNAME("go_to_help"), "class_enum:" + class_name + ":" + select);
-		return;
-	} else if (p_select.begins_with("#")) {
-		emit_signal(SNAME("go_to_help"), "class_name:" + p_select.substr(1, p_select.length()));
-		return;
-	} else if (p_select.begins_with("@")) {
-		int tag_end = p_select.find_char(' ');
 
-		String tag = p_select.substr(1, tag_end - 1);
-		String link = p_select.substr(tag_end + 1, p_select.length()).lstrip(" ");
+		emit_signal(SNAME("go_to_help"), "class_enum:" + enum_class_name + ":" + enum_name);
+	} else if (p_select.begins_with("#")) { // Class.
+		emit_signal(SNAME("go_to_help"), "class_name:" + p_select.substr(1));
+	} else if (p_select.begins_with("@")) { // Member.
+		const int tag_end = p_select.find_char(' ');
+		const String tag = p_select.substr(1, tag_end - 1);
+		const String link = p_select.substr(tag_end + 1).lstrip(" ");
 
 		String topic;
-		HashMap<String, int> *table = nullptr;
+		const HashMap<String, int> *table = nullptr;
 
 		if (tag == "method") {
 			topic = "class_method";
@@ -311,14 +317,14 @@ void EditorHelp::_class_desc_select(const String &p_select) {
 			}
 
 			if (link.contains(".")) {
-				int class_end = link.find_char('.');
-				emit_signal(SNAME("go_to_help"), topic + ":" + link.substr(0, class_end) + ":" + link.substr(class_end + 1, link.length()));
+				const int class_end = link.find_char('.');
+				emit_signal(SNAME("go_to_help"), topic + ":" + link.left(class_end) + ":" + link.substr(class_end + 1));
 			}
 		}
-	} else if (p_select.begins_with("http")) {
+	} else if (p_select.begins_with("http:") || p_select.begins_with("https:")) {
 		OS::get_singleton()->shell_open(p_select);
-	} else if (p_select.begins_with("^")) {
-		DisplayServer::get_singleton()->clipboard_set(p_select.trim_prefix("^"));
+	} else if (p_select.begins_with("^")) { // Copy button.
+		DisplayServer::get_singleton()->clipboard_set(p_select.substr(1));
 	}
 }
 
@@ -341,13 +347,15 @@ void EditorHelp::_class_desc_resized(bool p_force_update_theme) {
 	}
 }
 
-void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is_bitfield) {
+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) {
+	const Color type_color = p_owner_node->get_theme_color(SNAME("type_color"), SNAME("EditorHelp"));
+
 	if (p_type.is_empty() || p_type == "void") {
-		class_desc->push_color(Color(theme_cache.type_color, 0.5));
-		class_desc->push_hint(TTR("No return value."));
-		class_desc->add_text("void");
-		class_desc->pop(); // hint
-		class_desc->pop(); // color
+		p_rt->push_color(Color(type_color, 0.5));
+		p_rt->push_hint(TTR("No return value."));
+		p_rt->add_text("void");
+		p_rt->pop(); // hint
+		p_rt->pop(); // color
 		return;
 	}
 
@@ -359,12 +367,12 @@ void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is
 	String display_t; // For display purposes.
 	if (is_enum_type) {
 		link_t = p_enum; // The link for enums is always the full enum description
-		display_t = _contextualize_class_specifier(p_enum, edited_class);
+		display_t = _contextualize_class_specifier(p_enum, p_class);
 	} else {
-		display_t = _contextualize_class_specifier(p_type, edited_class);
+		display_t = _contextualize_class_specifier(p_type, p_class);
 	}
 
-	class_desc->push_color(theme_cache.type_color);
+	p_rt->push_color(type_color);
 	bool add_array = false;
 	if (can_ref) {
 		if (link_t.ends_with("[]")) {
@@ -372,37 +380,41 @@ void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is
 			link_t = link_t.trim_suffix("[]");
 			display_t = display_t.trim_suffix("[]");
 
-			class_desc->push_meta("#Array", RichTextLabel::META_UNDERLINE_ON_HOVER); // class
-			class_desc->add_text("Array");
-			class_desc->pop(); // meta
-			class_desc->add_text("[");
+			p_rt->push_meta("#Array", RichTextLabel::META_UNDERLINE_ON_HOVER); // class
+			p_rt->add_text("Array");
+			p_rt->pop(); // meta
+			p_rt->add_text("[");
 		} else if (is_bitfield) {
-			class_desc->push_color(Color(theme_cache.type_color, 0.5));
-			class_desc->push_hint(TTR("This value is an integer composed as a bitmask of the following flags."));
-			class_desc->add_text("BitField");
-			class_desc->pop(); // hint
-			class_desc->add_text("[");
-			class_desc->pop(); // color
+			p_rt->push_color(Color(type_color, 0.5));
+			p_rt->push_hint(TTR("This value is an integer composed as a bitmask of the following flags."));
+			p_rt->add_text("BitField");
+			p_rt->pop(); // hint
+			p_rt->add_text("[");
+			p_rt->pop(); // color
 		}
 
 		if (is_enum_type) {
-			class_desc->push_meta("$" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // enum
+			p_rt->push_meta("$" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // enum
 		} else {
-			class_desc->push_meta("#" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // class
+			p_rt->push_meta("#" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // class
 		}
 	}
-	class_desc->add_text(display_t);
+	p_rt->add_text(display_t);
 	if (can_ref) {
-		class_desc->pop(); // meta
+		p_rt->pop(); // meta
 		if (add_array) {
-			class_desc->add_text("]");
+			p_rt->add_text("]");
 		} else if (is_bitfield) {
-			class_desc->push_color(Color(theme_cache.type_color, 0.5));
-			class_desc->add_text("]");
-			class_desc->pop(); // color
+			p_rt->push_color(Color(type_color, 0.5));
+			p_rt->add_text("]");
+			p_rt->pop(); // color
 		}
 	}
-	class_desc->pop(); // color
+	p_rt->pop(); // color
+}
+
+void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is_bitfield) {
+	_add_type_to_rt(p_type, p_enum, p_is_bitfield, class_desc, this, edited_class);
 }
 
 void EditorHelp::_add_type_icon(const String &p_type, int p_size, const String &p_fallback) {
@@ -717,10 +729,10 @@ void EditorHelp::_update_method_list(MethodType p_method_type, const Vector<DocD
 
 		String group_prefix;
 		for (int i = 0; i < m.size(); i++) {
-			const String new_prefix = m[i].name.substr(0, 3);
+			const String new_prefix = m[i].name.left(3);
 			bool is_new_group = false;
 
-			if (i < m.size() - 1 && new_prefix == m[i + 1].name.substr(0, 3) && new_prefix != group_prefix) {
+			if (i < m.size() - 1 && new_prefix == m[i + 1].name.left(3) && new_prefix != group_prefix) {
 				is_new_group = i > 0;
 				group_prefix = new_prefix;
 			} else if (!group_prefix.is_empty() && new_prefix != group_prefix) {
@@ -748,7 +760,7 @@ void EditorHelp::_update_method_list(MethodType p_method_type, const Vector<DocD
 }
 
 void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc, MethodType p_method_type, const Vector<DocData::MethodDoc> &p_methods) {
-#define DTR_DOC(m_string) (p_classdoc.is_script_doc ? (m_string) : DTR(m_string))
+#define HANDLE_DOC(m_string) ((p_classdoc.is_script_doc ? (m_string) : DTR(m_string)).strip_edges())
 
 	class_desc->add_newline();
 	class_desc->add_newline();
@@ -807,7 +819,7 @@ void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc
 					TTRC("This constructor may be changed or removed in future versions."),
 					TTRC("This operator may be changed or removed in future versions."),
 				};
-				DEPRECATED_DOC_MSG(DTR_DOC(method.deprecated_message), TTRGET(messages_by_type[p_method_type]));
+				DEPRECATED_DOC_MSG(HANDLE_DOC(method.deprecated_message), TTRGET(messages_by_type[p_method_type]));
 			}
 
 			if (method.is_experimental) {
@@ -822,7 +834,7 @@ void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc
 					TTRC("This constructor may be changed or removed in future versions."),
 					TTRC("This operator may be changed or removed in future versions."),
 				};
-				EXPERIMENTAL_DOC_MSG(DTR_DOC(method.experimental_message), TTRGET(messages_by_type[p_method_type]));
+				EXPERIMENTAL_DOC_MSG(HANDLE_DOC(method.experimental_message), TTRGET(messages_by_type[p_method_type]));
 			}
 
 			if (!method.errors_returned.is_empty()) {
@@ -856,7 +868,7 @@ void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc
 				class_desc->pop(); // list
 			}
 
-			const String descr = DTR_DOC(method.description).strip_edges();
+			const String descr = HANDLE_DOC(method.description);
 			const bool is_documented = method.is_deprecated || method.is_experimental || !descr.is_empty();
 			if (!descr.is_empty()) {
 				if (has_prev_text) {
@@ -903,7 +915,7 @@ void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc
 		}
 	}
 
-#undef DTR_DOC
+#undef HANDLE_DOC
 }
 
 void EditorHelp::_update_doc() {
@@ -922,7 +934,7 @@ void EditorHelp::_update_doc() {
 
 	DocData::ClassDoc cd = doc->class_list[edited_class]; // Make a copy, so we can sort without worrying.
 
-#define DTR_DOC(m_string) (cd.is_script_doc ? (m_string) : DTR(m_string))
+#define HANDLE_DOC(m_string) ((cd.is_script_doc ? (m_string) : DTR(m_string)).strip_edges())
 
 	// Class name
 
@@ -940,12 +952,12 @@ void EditorHelp::_update_doc() {
 
 	if (cd.is_deprecated) {
 		class_desc->add_newline();
-		DEPRECATED_DOC_MSG(DTR_DOC(cd.deprecated_message), TTR("This class may be changed or removed in future versions."));
+		DEPRECATED_DOC_MSG(HANDLE_DOC(cd.deprecated_message), TTR("This class may be changed or removed in future versions."));
 	}
 
 	if (cd.is_experimental) {
 		class_desc->add_newline();
-		EXPERIMENTAL_DOC_MSG(DTR_DOC(cd.experimental_message), TTR("This class may be changed or removed in future versions."));
+		EXPERIMENTAL_DOC_MSG(HANDLE_DOC(cd.experimental_message), TTR("This class may be changed or removed in future versions."));
 	}
 
 	// Inheritance tree
@@ -1003,7 +1015,7 @@ void EditorHelp::_update_doc() {
 	bool has_description = false;
 
 	// Brief description
-	const String brief_class_descr = DTR_DOC(cd.brief_description).strip_edges();
+	const String brief_class_descr = HANDLE_DOC(cd.brief_description);
 	if (!brief_class_descr.is_empty()) {
 		has_description = true;
 
@@ -1022,7 +1034,7 @@ void EditorHelp::_update_doc() {
 	}
 
 	// Class description
-	const String class_descr = DTR_DOC(cd.description).strip_edges();
+	const String class_descr = HANDLE_DOC(cd.description);
 	if (!class_descr.is_empty()) {
 		has_description = true;
 
@@ -1106,9 +1118,9 @@ void EditorHelp::_update_doc() {
 		class_desc->push_color(theme_cache.symbol_color);
 
 		for (const DocData::TutorialDoc &tutorial : cd.tutorials) {
-			const String link = DTR_DOC(tutorial.link).strip_edges();
+			const String link = HANDLE_DOC(tutorial.link);
 
-			String link_text = DTR_DOC(tutorial.title).strip_edges();
+			String link_text = HANDLE_DOC(tutorial.title);
 			if (link_text.is_empty()) {
 				const int sep_pos = link.find("//");
 				if (sep_pos >= 0) {
@@ -1441,7 +1453,7 @@ void EditorHelp::_update_doc() {
 			_push_normal_font();
 			class_desc->push_color(theme_cache.comment_color);
 
-			const String descr = DTR_DOC(theme_item.description).strip_edges();
+			const String descr = HANDLE_DOC(theme_item.description);
 			if (!descr.is_empty()) {
 				_add_text(descr);
 			} else {
@@ -1538,13 +1550,13 @@ void EditorHelp::_update_doc() {
 			_push_normal_font();
 			class_desc->push_color(theme_cache.comment_color);
 
-			const String descr = DTR_DOC(signal.description).strip_edges();
+			const String descr = HANDLE_DOC(signal.description);
 			const bool is_multiline = descr.find_char('\n') > 0;
 			bool has_prev_text = false;
 
 			if (signal.is_deprecated) {
 				has_prev_text = true;
-				DEPRECATED_DOC_MSG(DTR_DOC(signal.deprecated_message), TTR("This signal may be changed or removed in future versions."));
+				DEPRECATED_DOC_MSG(HANDLE_DOC(signal.deprecated_message), TTR("This signal may be changed or removed in future versions."));
 			}
 
 			if (signal.is_experimental) {
@@ -1555,7 +1567,7 @@ void EditorHelp::_update_doc() {
 					}
 				}
 				has_prev_text = true;
-				EXPERIMENTAL_DOC_MSG(DTR_DOC(signal.experimental_message), TTR("This signal may be changed or removed in future versions."));
+				EXPERIMENTAL_DOC_MSG(HANDLE_DOC(signal.experimental_message), TTR("This signal may be changed or removed in future versions."));
 			}
 
 			if (!descr.is_empty()) {
@@ -1669,7 +1681,7 @@ void EditorHelp::_update_doc() {
 
 				// Enum description.
 				if (key != "@unnamed_enums" && cd.enums.has(key)) {
-					const String descr = DTR_DOC(cd.enums[key].description).strip_edges();
+					const String descr = HANDLE_DOC(cd.enums[key].description);
 					const bool is_multiline = descr.find_char('\n') > 0;
 					if (cd.enums[key].is_deprecated || cd.enums[key].is_experimental || !descr.is_empty()) {
 						class_desc->add_newline();
@@ -1682,7 +1694,7 @@ void EditorHelp::_update_doc() {
 
 						if (cd.enums[key].is_deprecated) {
 							has_prev_text = true;
-							DEPRECATED_DOC_MSG(DTR_DOC(cd.enums[key].deprecated_message), TTR("This enumeration may be changed or removed in future versions."));
+							DEPRECATED_DOC_MSG(HANDLE_DOC(cd.enums[key].deprecated_message), TTR("This enumeration may be changed or removed in future versions."));
 						}
 
 						if (cd.enums[key].is_experimental) {
@@ -1693,7 +1705,7 @@ void EditorHelp::_update_doc() {
 								}
 							}
 							has_prev_text = true;
-							EXPERIMENTAL_DOC_MSG(DTR_DOC(cd.enums[key].experimental_message), TTR("This enumeration may be changed or removed in future versions."));
+							EXPERIMENTAL_DOC_MSG(HANDLE_DOC(cd.enums[key].experimental_message), TTR("This enumeration may be changed or removed in future versions."));
 						}
 
 						if (!descr.is_empty()) {
@@ -1718,7 +1730,7 @@ void EditorHelp::_update_doc() {
 
 				bool prev_is_multiline = true; // Use a large margin for the first item.
 				for (const DocData::ConstantDoc &enum_value : E.value) {
-					const String descr = DTR_DOC(enum_value.description).strip_edges();
+					const String descr = HANDLE_DOC(enum_value.description);
 					const bool is_multiline = descr.find_char('\n') > 0;
 
 					class_desc->add_newline();
@@ -1766,7 +1778,7 @@ void EditorHelp::_update_doc() {
 
 						if (enum_value.is_deprecated) {
 							has_prev_text = true;
-							DEPRECATED_DOC_MSG(DTR_DOC(enum_value.deprecated_message), TTR("This constant may be changed or removed in future versions."));
+							DEPRECATED_DOC_MSG(HANDLE_DOC(enum_value.deprecated_message), TTR("This constant may be changed or removed in future versions."));
 						}
 
 						if (enum_value.is_experimental) {
@@ -1777,7 +1789,7 @@ void EditorHelp::_update_doc() {
 								}
 							}
 							has_prev_text = true;
-							EXPERIMENTAL_DOC_MSG(DTR_DOC(enum_value.experimental_message), TTR("This constant may be changed or removed in future versions."));
+							EXPERIMENTAL_DOC_MSG(HANDLE_DOC(enum_value.experimental_message), TTR("This constant may be changed or removed in future versions."));
 						}
 
 						if (!descr.is_empty()) {
@@ -1817,7 +1829,7 @@ void EditorHelp::_update_doc() {
 
 			bool prev_is_multiline = true; // Use a large margin for the first item.
 			for (const DocData::ConstantDoc &constant : constants) {
-				const String descr = DTR_DOC(constant.description).strip_edges();
+				const String descr = HANDLE_DOC(constant.description);
 				const bool is_multiline = descr.find_char('\n') > 0;
 
 				class_desc->add_newline();
@@ -1871,7 +1883,7 @@ void EditorHelp::_update_doc() {
 
 					if (constant.is_deprecated) {
 						has_prev_text = true;
-						DEPRECATED_DOC_MSG(DTR_DOC(constant.deprecated_message), TTR("This constant may be changed or removed in future versions."));
+						DEPRECATED_DOC_MSG(HANDLE_DOC(constant.deprecated_message), TTR("This constant may be changed or removed in future versions."));
 					}
 
 					if (constant.is_experimental) {
@@ -1882,7 +1894,7 @@ void EditorHelp::_update_doc() {
 							}
 						}
 						has_prev_text = true;
-						EXPERIMENTAL_DOC_MSG(DTR_DOC(constant.experimental_message), TTR("This constant may be changed or removed in future versions."));
+						EXPERIMENTAL_DOC_MSG(HANDLE_DOC(constant.experimental_message), TTR("This constant may be changed or removed in future versions."));
 					}
 
 					if (!descr.is_empty()) {
@@ -2000,7 +2012,7 @@ void EditorHelp::_update_doc() {
 			_push_normal_font();
 			class_desc->push_color(theme_cache.comment_color);
 
-			const String descr = DTR_DOC(annotation.description).strip_edges();
+			const String descr = HANDLE_DOC(annotation.description);
 			if (!descr.is_empty()) {
 				_add_text(descr);
 			} else {
@@ -2185,7 +2197,7 @@ void EditorHelp::_update_doc() {
 
 			if (prop.is_deprecated) {
 				has_prev_text = true;
-				DEPRECATED_DOC_MSG(DTR_DOC(prop.deprecated_message), TTR("This property may be changed or removed in future versions."));
+				DEPRECATED_DOC_MSG(HANDLE_DOC(prop.deprecated_message), TTR("This property may be changed or removed in future versions."));
 			}
 
 			if (prop.is_experimental) {
@@ -2194,10 +2206,10 @@ void EditorHelp::_update_doc() {
 					class_desc->add_newline();
 				}
 				has_prev_text = true;
-				EXPERIMENTAL_DOC_MSG(DTR_DOC(prop.experimental_message), TTR("This property may be changed or removed in future versions."));
+				EXPERIMENTAL_DOC_MSG(HANDLE_DOC(prop.experimental_message), TTR("This property may be changed or removed in future versions."));
 			}
 
-			const String descr = DTR_DOC(prop.description).strip_edges();
+			const String descr = HANDLE_DOC(prop.description);
 			if (!descr.is_empty()) {
 				if (has_prev_text) {
 					class_desc->add_newline();
@@ -2251,7 +2263,7 @@ void EditorHelp::_update_doc() {
 	// Free the scroll.
 	scroll_locked = false;
 
-#undef DTR_DOC
+#undef HANDLE_DOC
 }
 
 void EditorHelp::_request_help(const String &p_string) {
@@ -2331,7 +2343,7 @@ void EditorHelp::_help_callback(const String &p_topic) {
 	}
 }
 
-static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control *p_owner_node, const String &p_class) {
+static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, const Control *p_owner_node, const String &p_class) {
 	const DocTools *doc = EditorHelp::get_doc_data();
 
 	bool is_native = false;
@@ -2452,7 +2464,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control
 		const String tag = bbcode.substr(brk_pos + 1, brk_end - brk_pos - 1);
 
 		if (tag.begins_with("/")) {
-			bool tag_ok = tag_stack.size() && tag_stack.front()->get() == tag.substr(1, tag.length());
+			bool tag_ok = tag_stack.size() && tag_stack.front()->get() == tag.substr(1);
 
 			if (!tag_ok) {
 				p_rt->add_text("[");
@@ -2467,8 +2479,8 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control
 			}
 		} 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 ")) {
 			const int tag_end = tag.find_char(' ');
-			const String link_tag = tag.substr(0, tag_end);
-			const String link_target = tag.substr(tag_end + 1, tag.length()).lstrip(" ");
+			const String link_tag = tag.left(tag_end);
+			const String link_target = tag.substr(tag_end + 1).lstrip(" ");
 
 			Color target_color = link_color;
 			RichTextLabel::MetaUnderline underline_mode = RichTextLabel::META_UNDERLINE_ON_HOVER;
@@ -2520,7 +2532,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control
 			pos = brk_end + 1;
 		} else if (tag.begins_with("param ")) {
 			const int tag_end = tag.find_char(' ');
-			const String param_name = tag.substr(tag_end + 1, tag.length()).lstrip(" ");
+			const String param_name = tag.substr(tag_end + 1).lstrip(" ");
 
 			// Use monospace font with translucent background color to make code easier to distinguish from other text.
 			p_rt->push_font(doc_code_font);
@@ -2741,7 +2753,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control
 			pos = brk_end + 1;
 			tag_stack.push_front(tag);
 		} else if (tag.begins_with("url=")) {
-			String url = tag.substr(4, tag.length());
+			String url = tag.substr(4);
 			p_rt->push_meta(url);
 
 			pos = brk_end + 1;
@@ -2751,13 +2763,13 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control
 			int height = 0;
 			bool size_in_percent = false;
 			if (tag.length() > 4) {
-				Vector<String> subtags = tag.substr(4, tag.length()).split(" ");
+				Vector<String> subtags = tag.substr(4).split(" ");
 				HashMap<String, String> bbcode_options;
 				for (int i = 0; i < subtags.size(); i++) {
 					const String &expr = subtags[i];
 					int value_pos = expr.find_char('=');
 					if (value_pos > -1) {
-						bbcode_options[expr.substr(0, value_pos)] = expr.substr(value_pos + 1).unquote();
+						bbcode_options[expr.left(value_pos)] = expr.substr(value_pos + 1).unquote();
 					}
 				}
 				HashMap<String, String>::Iterator width_option = bbcode_options.find("width");
@@ -2787,14 +2799,14 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control
 			pos = end;
 			tag_stack.push_front("img");
 		} else if (tag.begins_with("color=")) {
-			String col = tag.substr(6, tag.length());
+			String col = tag.substr(6);
 			Color color = Color::from_string(col, Color());
 			p_rt->push_color(color);
 
 			pos = brk_end + 1;
 			tag_stack.push_front("color");
 		} else if (tag.begins_with("font=")) {
-			String font_path = tag.substr(5, tag.length());
+			String font_path = tag.substr(5);
 			Ref<Font> font = ResourceLoader::load(font_path, "Font");
 			if (font.is_valid()) {
 				p_rt->push_font(font);
@@ -3120,68 +3132,50 @@ DocTools *EditorHelp::get_doc_data() {
 
 /// EditorHelpBit ///
 
-void EditorHelpBit::_go_to_help(const String &p_what) {
-	EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT);
-	ScriptEditor::get_singleton()->goto_help(p_what);
-	emit_signal(SNAME("request_hide"));
-}
+#define HANDLE_DOC(m_string) ((is_native ? DTR(m_string) : (m_string)).strip_edges())
 
-void EditorHelpBit::_meta_clicked(const String &p_select) {
-	if (p_select.begins_with("$")) { // enum
-		String select = p_select.substr(1, p_select.length());
-		String class_name;
-		int rfind = select.rfind(".");
-		if (rfind != -1) {
-			class_name = select.substr(0, rfind);
-			select = select.substr(rfind + 1);
-		} else {
-			class_name = "@GlobalScope";
-		}
-		_go_to_help("class_enum:" + class_name + ":" + select);
-		return;
-	} else if (p_select.begins_with("#")) {
-		_go_to_help("class_name:" + p_select.substr(1, p_select.length()));
-		return;
-	} else if (p_select.begins_with("@")) {
-		String m = p_select.substr(1, p_select.length());
-
-		if (m.contains(".")) {
-			_go_to_help("class_method:" + m.get_slice(".", 0) + ":" + m.get_slice(".", 0)); // Must go somewhere else.
-		}
-	}
-}
-
-String EditorHelpBit::get_class_description(const StringName &p_class_name) const {
+EditorHelpBit::HelpData EditorHelpBit::_get_class_help_data(const StringName &p_class_name) {
 	if (doc_class_cache.has(p_class_name)) {
 		return doc_class_cache[p_class_name];
 	}
 
-	String description;
+	HelpData result;
 
 	const 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.
 		const bool is_native = !E->value.is_script_doc;
-		description = is_native ? DTR(E->value.brief_description) : E->value.brief_description;
+
+		result.description = HANDLE_DOC(E->value.brief_description);
+		if (E->value.is_deprecated) {
+			if (E->value.deprecated_message.is_empty()) {
+				result.deprecated_message = TTR("This class may be changed or removed in future versions.");
+			} else {
+				result.deprecated_message = HANDLE_DOC(E->value.deprecated_message);
+			}
+		}
+		if (E->value.is_experimental) {
+			if (E->value.experimental_message.is_empty()) {
+				result.experimental_message = TTR("This class may be changed or removed in future versions.");
+			} else {
+				result.experimental_message = HANDLE_DOC(E->value.experimental_message);
+			}
+		}
 
 		if (is_native) {
-			doc_class_cache[p_class_name] = description;
+			doc_class_cache[p_class_name] = result;
 		}
 	}
 
-	return description;
+	return result;
 }
 
-String EditorHelpBit::get_property_description(const StringName &p_class_name, const StringName &p_property_name) const {
-	if (!custom_description.is_empty()) {
-		return custom_description;
-	}
-
+EditorHelpBit::HelpData EditorHelpBit::_get_property_help_data(const StringName &p_class_name, const StringName &p_property_name) {
 	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;
+	HelpData result;
 
 	const DocTools *dd = EditorHelp::get_doc_data();
 	const HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name);
@@ -3190,7 +3184,22 @@ String EditorHelpBit::get_property_description(const StringName &p_class_name, c
 		const bool is_native = !E->value.is_script_doc;
 
 		for (const DocData::PropertyDoc &property : E->value.properties) {
-			String description_current = is_native ? DTR(property.description) : property.description;
+			HelpData current;
+			current.description = HANDLE_DOC(property.description);
+			if (property.is_deprecated) {
+				if (property.deprecated_message.is_empty()) {
+					current.deprecated_message = TTR("This property may be changed or removed in future versions.");
+				} else {
+					current.deprecated_message = HANDLE_DOC(property.deprecated_message);
+				}
+			}
+			if (property.is_experimental) {
+				if (property.experimental_message.is_empty()) {
+					current.experimental_message = TTR("This property may be changed or removed in future versions.");
+				} else {
+					current.experimental_message = HANDLE_DOC(property.experimental_message);
+				}
+			}
 
 			String enum_class_name;
 			String enum_name;
@@ -3215,18 +3224,19 @@ String EditorHelpBit::get_property_description(const StringName &p_class_name, c
 						if (constant.enumeration == enum_name && !constant.name.ends_with("_MAX")) {
 							// Prettify the enum value display, so that "<ENUM_NAME>_<ITEM>" becomes "Item".
 							const String item_name = EditorPropertyNameProcessor::get_singleton()->process_name(constant.name, EditorPropertyNameProcessor::STYLE_CAPITALIZED).trim_prefix(enum_prefix);
-							String item_descr = (is_native ? DTR(constant.description) : constant.description).strip_edges();
+							String item_descr = HANDLE_DOC(constant.description);
 							if (item_descr.is_empty()) {
-								item_descr = ("[i]" + DTR("No description available.") + "[/i]");
+								item_descr = "[color=<EditorHelpBitCommentColor>][i]" + TTR("No description available.") + "[/i][/color]";
 							}
-							description_current += vformat("\n[b]%s:[/b] %s", item_name, item_descr);
+							current.description += vformat("\n[b]%s:[/b] %s", item_name, item_descr);
 						}
 					}
+					current.description = current.description.lstrip("\n");
 				}
 			}
 
 			if (property.name == p_property_name) {
-				description = description_current;
+				result = current;
 
 				if (!is_native) {
 					break;
@@ -3234,20 +3244,20 @@ String EditorHelpBit::get_property_description(const StringName &p_class_name, c
 			}
 
 			if (is_native) {
-				doc_property_cache[p_class_name][property.name] = description_current;
+				doc_property_cache[p_class_name][property.name] = current;
 			}
 		}
 	}
 
-	return description;
+	return result;
 }
 
-String EditorHelpBit::get_method_description(const StringName &p_class_name, const StringName &p_method_name) const {
+EditorHelpBit::HelpData EditorHelpBit::_get_method_help_data(const StringName &p_class_name, const StringName &p_method_name) {
 	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;
+	HelpData result;
 
 	const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
 	if (E) {
@@ -3255,10 +3265,30 @@ String EditorHelpBit::get_method_description(const StringName &p_class_name, con
 		const bool is_native = !E->value.is_script_doc;
 
 		for (const DocData::MethodDoc &method : E->value.methods) {
-			String description_current = is_native ? DTR(method.description) : method.description;
+			HelpData current;
+			current.description = HANDLE_DOC(method.description);
+			if (method.is_deprecated) {
+				if (method.deprecated_message.is_empty()) {
+					current.deprecated_message = TTR("This method may be changed or removed in future versions.");
+				} else {
+					current.deprecated_message = HANDLE_DOC(method.deprecated_message);
+				}
+			}
+			if (method.is_experimental) {
+				if (method.experimental_message.is_empty()) {
+					current.experimental_message = TTR("This method may be changed or removed in future versions.");
+				} else {
+					current.experimental_message = HANDLE_DOC(method.experimental_message);
+				}
+			}
+			current.doc_type = { method.return_type, method.return_enum, method.return_is_bitfield };
+			for (const DocData::ArgumentDoc &argument : method.arguments) {
+				const DocType argument_type = { argument.type, argument.enumeration, argument.is_bitfield };
+				current.arguments.push_back({ argument.name, argument_type, argument.default_value });
+			}
 
 			if (method.name == p_method_name) {
-				description = description_current;
+				result = current;
 
 				if (!is_native) {
 					break;
@@ -3266,20 +3296,20 @@ String EditorHelpBit::get_method_description(const StringName &p_class_name, con
 			}
 
 			if (is_native) {
-				doc_method_cache[p_class_name][method.name] = description_current;
+				doc_method_cache[p_class_name][method.name] = current;
 			}
 		}
 	}
 
-	return description;
+	return result;
 }
 
-String EditorHelpBit::get_signal_description(const StringName &p_class_name, const StringName &p_signal_name) const {
+EditorHelpBit::HelpData EditorHelpBit::_get_signal_help_data(const StringName &p_class_name, const StringName &p_signal_name) {
 	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;
+	HelpData result;
 
 	const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
 	if (E) {
@@ -3287,10 +3317,29 @@ String EditorHelpBit::get_signal_description(const StringName &p_class_name, con
 		const bool is_native = !E->value.is_script_doc;
 
 		for (const DocData::MethodDoc &signal : E->value.signals) {
-			String description_current = is_native ? DTR(signal.description) : signal.description;
+			HelpData current;
+			current.description = HANDLE_DOC(signal.description);
+			if (signal.is_deprecated) {
+				if (signal.deprecated_message.is_empty()) {
+					current.deprecated_message = TTR("This signal may be changed or removed in future versions.");
+				} else {
+					current.deprecated_message = HANDLE_DOC(signal.deprecated_message);
+				}
+			}
+			if (signal.is_experimental) {
+				if (signal.experimental_message.is_empty()) {
+					current.experimental_message = TTR("This signal may be changed or removed in future versions.");
+				} else {
+					current.experimental_message = HANDLE_DOC(signal.experimental_message);
+				}
+			}
+			for (const DocData::ArgumentDoc &argument : signal.arguments) {
+				const DocType argument_type = { argument.type, argument.enumeration, argument.is_bitfield };
+				current.arguments.push_back({ argument.name, argument_type, argument.default_value });
+			}
 
 			if (signal.name == p_signal_name) {
-				description = description_current;
+				result = current;
 
 				if (!is_native) {
 					break;
@@ -3298,20 +3347,20 @@ String EditorHelpBit::get_signal_description(const StringName &p_class_name, con
 			}
 
 			if (is_native) {
-				doc_signal_cache[p_class_name][signal.name] = description_current;
+				doc_signal_cache[p_class_name][signal.name] = current;
 			}
 		}
 	}
 
-	return description;
+	return result;
 }
 
-String EditorHelpBit::get_theme_item_description(const StringName &p_class_name, const StringName &p_theme_item_name) const {
+EditorHelpBit::HelpData EditorHelpBit::_get_theme_item_help_data(const StringName &p_class_name, const StringName &p_theme_item_name) {
 	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;
+	HelpData result;
 
 	bool found = false;
 	const DocTools *dd = EditorHelp::get_doc_data();
@@ -3321,142 +3370,477 @@ String EditorHelpBit::get_theme_item_description(const StringName &p_class_name,
 		const bool is_native = !E->value.is_script_doc;
 
 		for (const DocData::ThemeItemDoc &theme_item : E->value.theme_properties) {
-			String description_current = is_native ? DTR(theme_item.description) : theme_item.description;
+			HelpData current;
+			current.description = HANDLE_DOC(theme_item.description);
 
 			if (theme_item.name == p_theme_item_name) {
-				description = description_current;
+				result = current;
 				found = true;
-
 				if (!is_native) {
 					break;
 				}
 			}
 
 			if (is_native) {
-				doc_theme_item_cache[p_class_name][theme_item.name] = description_current;
+				doc_theme_item_cache[p_class_name][theme_item.name] = current;
 			}
 		}
 
 		if (found || E->value.inherits.is_empty()) {
 			break;
 		}
+
 		// Check for inherited theme items.
 		E = dd->class_list.find(E->value.inherits);
 	}
 
-	return description;
+	return result;
+}
+
+#undef HANDLE_DOC
+
+void EditorHelpBit::_add_type_to_title(const DocType &p_doc_type) {
+	_add_type_to_rt(p_doc_type.type, p_doc_type.enumeration, p_doc_type.is_bitfield, title, this, symbol_class_name);
+}
+
+void EditorHelpBit::_update_labels() {
+	const Ref<Font> doc_bold_font = get_theme_font(SNAME("doc_bold"), EditorStringName(EditorFonts));
+
+	if (!symbol_visible_type.is_empty() || !symbol_name.is_empty()) {
+		title->clear();
+
+		title->push_font(doc_bold_font);
+
+		if (!symbol_visible_type.is_empty()) {
+			title->push_color(get_theme_color(SNAME("title_color"), SNAME("EditorHelp")));
+			title->add_text(symbol_visible_type);
+			title->pop(); // color
+		}
+
+		if (!symbol_visible_type.is_empty() && !symbol_name.is_empty()) {
+			title->add_text(" ");
+		}
+
+		if (!symbol_name.is_empty()) {
+			title->push_underline();
+			title->add_text(symbol_name);
+			title->pop(); // underline
+		}
+
+		title->pop(); // font
+
+		if (symbol_type == "method" || symbol_type == "signal") {
+			const Color symbol_color = get_theme_color(SNAME("symbol_color"), SNAME("EditorHelp"));
+			const Color value_color = get_theme_color(SNAME("value_color"), SNAME("EditorHelp"));
+
+			title->push_font(get_theme_font(SNAME("doc_source"), EditorStringName(EditorFonts)));
+			title->push_font_size(get_theme_font_size(SNAME("doc_source_size"), EditorStringName(EditorFonts)) * 0.9);
+
+			title->push_color(symbol_color);
+			title->add_text("(");
+			title->pop(); // color
+
+			for (int i = 0; i < help_data.arguments.size(); i++) {
+				const ArgumentData &argument = help_data.arguments[i];
+
+				if (i > 0) {
+					title->push_color(symbol_color);
+					title->add_text(", ");
+					title->pop(); // color
+				}
+
+				title->add_text(argument.name);
+
+				title->push_color(symbol_color);
+				title->add_text(": ");
+				title->pop(); // color
+
+				_add_type_to_title(argument.doc_type);
+
+				if (!argument.default_value.is_empty()) {
+					title->push_color(symbol_color);
+					title->add_text(" = ");
+					title->pop(); // color
+
+					title->push_color(value_color);
+					title->add_text(argument.default_value);
+					title->pop(); // color
+				}
+			}
+
+			title->push_color(symbol_color);
+			title->add_text(")");
+			title->pop(); // color
+
+			if (symbol_type == "method") {
+				title->push_color(symbol_color);
+				title->add_text(" -> ");
+				title->pop(); // color
+
+				_add_type_to_title(help_data.doc_type);
+			}
+
+			title->pop(); // font_size
+			title->pop(); // font
+		}
+
+		title->show();
+	} else {
+		title->hide();
+	}
+
+	content->clear();
+
+	bool has_prev_text = false;
+
+	if (!help_data.deprecated_message.is_empty()) {
+		has_prev_text = true;
+
+		Ref<Texture2D> error_icon = get_editor_theme_icon(SNAME("StatusError"));
+		content->add_image(error_icon, error_icon->get_width(), error_icon->get_height());
+		content->add_text(" ");
+		content->push_color(get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
+		content->push_font(doc_bold_font);
+		content->add_text(TTR("Deprecated:"));
+		content->pop(); // font
+		content->pop(); // color
+		content->add_text(" ");
+		_add_text_to_rt(help_data.deprecated_message, content, this, symbol_class_name);
+	}
+
+	if (!help_data.experimental_message.is_empty()) {
+		if (has_prev_text) {
+			content->add_newline();
+			content->add_newline();
+		}
+		has_prev_text = true;
+
+		Ref<Texture2D> warning_icon = get_editor_theme_icon(SNAME("NodeWarning"));
+		content->add_image(warning_icon, warning_icon->get_width(), warning_icon->get_height());
+		content->add_text(" ");
+		content->push_color(get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
+		content->push_font(doc_bold_font);
+		content->add_text(TTR("Experimental:"));
+		content->pop(); // font
+		content->pop(); // color
+		content->add_text(" ");
+		_add_text_to_rt(help_data.experimental_message, content, this, symbol_class_name);
+	}
+
+	if (!help_data.description.is_empty()) {
+		if (has_prev_text) {
+			content->add_newline();
+			content->add_newline();
+		}
+		has_prev_text = true;
+
+		const Color comment_color = get_theme_color(SNAME("comment_color"), SNAME("EditorHelp"));
+		_add_text_to_rt(help_data.description.replace("<EditorHelpBitCommentColor>", comment_color.to_html()), content, this, symbol_class_name);
+	}
+
+	if (is_inside_tree()) {
+		update_content_height();
+	}
+}
+
+void EditorHelpBit::_go_to_help(const String &p_what) {
+	EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT);
+	ScriptEditor::get_singleton()->goto_help(p_what);
+	emit_signal(SNAME("request_hide"));
+}
+
+void EditorHelpBit::_meta_clicked(const String &p_select) {
+	if (p_select.begins_with("$")) { // Enum.
+		const String link = p_select.substr(1);
+
+		String enum_class_name;
+		String enum_name;
+		if (CoreConstants::is_global_enum(link)) {
+			enum_class_name = "@GlobalScope";
+			enum_name = link;
+		} else {
+			const int dot_pos = link.rfind(".");
+			if (dot_pos >= 0) {
+				enum_class_name = link.left(dot_pos);
+				enum_name = link.substr(dot_pos + 1);
+			} else {
+				enum_class_name = symbol_class_name;
+				enum_name = link;
+			}
+		}
+
+		_go_to_help("class_enum:" + enum_class_name + ":" + enum_name);
+	} else if (p_select.begins_with("#")) { // Class.
+		_go_to_help("class_name:" + p_select.substr(1));
+	} else if (p_select.begins_with("@")) { // Member.
+		const int tag_end = p_select.find_char(' ');
+		const String tag = p_select.substr(1, tag_end - 1);
+		const String link = p_select.substr(tag_end + 1).lstrip(" ");
+
+		String topic;
+		if (tag == "method") {
+			topic = "class_method";
+		} else if (tag == "constructor") {
+			topic = "class_method";
+		} else if (tag == "operator") {
+			topic = "class_method";
+		} else if (tag == "member") {
+			topic = "class_property";
+		} else if (tag == "enum") {
+			topic = "class_enum";
+		} else if (tag == "signal") {
+			topic = "class_signal";
+		} else if (tag == "constant") {
+			topic = "class_constant";
+		} else if (tag == "annotation") {
+			topic = "class_annotation";
+		} else if (tag == "theme_item") {
+			topic = "class_theme_item";
+		} else {
+			return;
+		}
+
+		if (link.contains(".")) {
+			const int class_end = link.find_char('.');
+			_go_to_help(topic + ":" + link.left(class_end) + ":" + link.substr(class_end + 1));
+		} else {
+			_go_to_help(topic + ":" + symbol_class_name + ":" + link);
+		}
+	} else if (p_select.begins_with("http:") || p_select.begins_with("https:")) {
+		OS::get_singleton()->shell_open(p_select);
+	} else if (p_select.begins_with("^")) { // Copy button.
+		DisplayServer::get_singleton()->clipboard_set(p_select.substr(1));
+	}
 }
 
 void EditorHelpBit::_bind_methods() {
-	ClassDB::bind_method(D_METHOD("set_text", "text"), &EditorHelpBit::set_text);
 	ADD_SIGNAL(MethodInfo("request_hide"));
 }
 
 void EditorHelpBit::_notification(int p_what) {
 	switch (p_what) {
-		case NOTIFICATION_THEME_CHANGED: {
-			rich_text->add_theme_color_override("selection_color", get_theme_color(SNAME("selection_color"), SNAME("EditorHelp")));
-			rich_text->clear();
-			_add_text_to_rt(text, rich_text, this, doc_class_name);
-			rich_text->reset_size(); // Force recalculating size after parsing bbcode.
-		} break;
+		case NOTIFICATION_THEME_CHANGED:
+			_update_labels();
+			break;
 	}
 }
 
-void EditorHelpBit::set_text(const String &p_text) {
-	text = p_text;
-	rich_text->clear();
-	_add_text_to_rt(text, rich_text, this, doc_class_name);
+void EditorHelpBit::parse_symbol(const String &p_symbol) {
+	const PackedStringArray slices = p_symbol.split("|", true, 2);
+	ERR_FAIL_COND_MSG(slices.size() < 3, "Invalid doc id. The expected format is 'item_type|class_name|item_name'.");
+
+	const String &item_type = slices[0];
+	const String &class_name = slices[1];
+	const String &item_name = slices[2];
+
+	String visible_type;
+	String name = item_name;
+
+	if (item_type == "class") {
+		visible_type = TTR("Class:");
+		name = class_name;
+		help_data = _get_class_help_data(class_name);
+	} else if (item_type == "property") {
+		if (name.begins_with("metadata/")) {
+			visible_type = TTR("Metadata:");
+			name = name.trim_prefix("metadata/");
+		} else if (class_name == "ProjectSettings" || class_name == "EditorSettings") {
+			visible_type = TTR("Setting:");
+		} else {
+			visible_type = TTR("Property:");
+		}
+		help_data = _get_property_help_data(class_name, item_name);
+	} else if (item_type == "internal_property") {
+		visible_type = TTR("Internal Property:");
+		help_data = HelpData();
+		help_data.description = "[color=<EditorHelpBitCommentColor>][i]" + TTR("This property can only be set in the Inspector.") + "[/i][/color]";
+	} else if (item_type == "method") {
+		visible_type = TTR("Method:");
+		help_data = _get_method_help_data(class_name, item_name);
+	} else if (item_type == "signal") {
+		visible_type = TTR("Signal:");
+		help_data = _get_signal_help_data(class_name, item_name);
+	} else if (item_type == "theme_item") {
+		visible_type = TTR("Theme Property:");
+		help_data = _get_theme_item_help_data(class_name, item_name);
+	} else {
+		ERR_FAIL_MSG("Invalid tooltip type '" + item_type + "'. Valid types are 'class', 'property', 'internal_property', 'method', 'signal', and 'theme_item'.");
+	}
+
+	symbol_class_name = class_name;
+	symbol_type = item_type;
+	symbol_visible_type = visible_type;
+	symbol_name = name;
+
+	if (help_data.description.is_empty()) {
+		help_data.description = "[color=<EditorHelpBitCommentColor>][i]" + TTR("No description available.") + "[/i][/color]";
+	}
+
+	if (is_inside_tree()) {
+		_update_labels();
+	}
 }
 
-EditorHelpBit::EditorHelpBit() {
-	rich_text = memnew(RichTextLabel);
-	add_child(rich_text);
-	rich_text->connect("meta_clicked", callable_mp(this, &EditorHelpBit::_meta_clicked));
-	rich_text->set_fit_content(true);
-	set_custom_minimum_size(Size2(0, 50 * EDSCALE));
+void EditorHelpBit::set_custom_text(const String &p_type, const String &p_name, const String &p_description) {
+	symbol_class_name = String();
+	symbol_type = String();
+	symbol_visible_type = p_type;
+	symbol_name = p_name;
+
+	help_data = HelpData();
+	help_data.description = p_description;
+
+	if (is_inside_tree()) {
+		_update_labels();
+	}
 }
 
-/// EditorHelpTooltip ///
+void EditorHelpBit::prepend_description(const String &p_text) {
+	if (help_data.description.is_empty()) {
+		help_data.description = p_text;
+	} else {
+		help_data.description = p_text + "\n" + help_data.description;
+	}
 
-void EditorHelpTooltip::_notification(int p_what) {
-	switch (p_what) {
-		case NOTIFICATION_POSTINITIALIZE: {
-			if (!tooltip_text.is_empty()) {
-				parse_tooltip(tooltip_text);
-			}
-		} break;
+	if (is_inside_tree()) {
+		_update_labels();
 	}
 }
 
-// `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;
+void EditorHelpBit::set_content_height_limits(float p_min, float p_max) {
+	ERR_FAIL_COND(p_min > p_max);
+	content_min_height = p_min;
+	content_max_height = p_max;
 
-	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'.");
+	if (is_inside_tree()) {
+		update_content_height();
+	}
+}
 
-	const String &type = slices[0];
-	const String &class_name = slices[1];
-	const String &property_name = slices[2];
-	const String &property_args = slices[3];
+void EditorHelpBit::update_content_height() {
+	float content_height = content->get_content_height();
+	const Ref<StyleBox> style = content->get_theme_stylebox("normal");
+	if (style.is_valid()) {
+		content_height += style->get_content_margin(SIDE_TOP) + style->get_content_margin(SIDE_BOTTOM);
+	}
+	content->set_custom_minimum_size(Size2(content->get_custom_minimum_size().x, CLAMP(content_height, content_min_height, content_max_height)));
+}
 
-	doc_class_name = class_name;
+EditorHelpBit::EditorHelpBit(const String &p_symbol) {
+	add_theme_constant_override("separation", 0);
+
+	title = memnew(RichTextLabel);
+	title->set_theme_type_variation("EditorHelpBitTitle");
+	title->set_fit_content(true);
+	title->set_selection_enabled(true);
+	//title->set_context_menu_enabled(true); // TODO: Fix opening context menu hides tooltip.
+	title->connect("meta_clicked", callable_mp(this, &EditorHelpBit::_meta_clicked));
+	title->hide();
+	add_child(title);
+
+	content_min_height = 48 * EDSCALE;
+	content_max_height = 360 * EDSCALE;
+
+	content = memnew(RichTextLabel);
+	content->set_theme_type_variation("EditorHelpBitContent");
+	content->set_custom_minimum_size(Size2(512 * EDSCALE, content_min_height));
+	content->set_selection_enabled(true);
+	//content->set_context_menu_enabled(true); // TODO: Fix opening context menu hides tooltip.
+	content->connect("meta_clicked", callable_mp(this, &EditorHelpBit::_meta_clicked));
+	add_child(content);
+
+	if (!p_symbol.is_empty()) {
+		parse_symbol(p_symbol);
+	}
+}
 
-	String formatted_text;
+/// EditorHelpBitTooltip ///
 
-	// Exclude internal properties, they are not documented.
-	if (type == "internal_property") {
-		formatted_text = "[i]" + TTR("This property can only be set in the Inspector.") + "[/i]";
-		set_text(formatted_text);
-		return;
+void EditorHelpBitTooltip::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_WM_MOUSE_ENTER:
+			timer->stop();
+			break;
+		case NOTIFICATION_WM_MOUSE_EXIT:
+			timer->start();
+			break;
 	}
+}
 
-	String title;
-	String description;
+// Forwards non-mouse input to the parent viewport.
+void EditorHelpBitTooltip::_input_from_window(const Ref<InputEvent> &p_event) {
+	if (p_event->is_action_pressed(SNAME("ui_cancel"), false, true)) {
+		hide(); // Will be deleted on its timer.
+	} else {
+		const Ref<InputEventMouse> mouse_event = p_event;
+		if (mouse_event.is_null()) {
+			get_parent_viewport()->push_input(p_event);
+		}
+	}
+}
+
+void EditorHelpBitTooltip::show_tooltip(EditorHelpBit *p_help_bit, Control *p_target) {
+	ERR_FAIL_NULL(p_help_bit);
+	EditorHelpBitTooltip *tooltip = memnew(EditorHelpBitTooltip(p_target));
+	p_help_bit->connect("request_hide", callable_mp(static_cast<Window *>(tooltip), &Window::hide)); // Will be deleted on its timer.
+	tooltip->add_child(p_help_bit);
+	p_target->get_viewport()->add_child(tooltip);
+	p_help_bit->update_content_height();
+	tooltip->popup_under_cursor();
+}
 
-	if (type == "class") {
-		title = class_name;
-		description = get_class_description(class_name);
-		formatted_text = TTR("Class:");
+// Copy-paste from `Viewport::_gui_show_tooltip()`.
+void EditorHelpBitTooltip::popup_under_cursor() {
+	Point2 mouse_pos = get_mouse_position();
+	Point2 tooltip_offset = GLOBAL_GET("display/mouse_cursor/tooltip_position_offset");
+	Rect2 r(mouse_pos + tooltip_offset, get_contents_minimum_size());
+	r.size = r.size.min(get_max_size());
+
+	Window *window = get_parent_visible_window();
+	Rect2i vr;
+	if (is_embedded()) {
+		vr = get_embedder()->get_visible_rect();
 	} else {
-		title = property_name;
+		vr = window->get_usable_parent_rect();
+	}
 
-		if (type == "property") {
-			description = get_property_description(class_name, property_name);
-			if (property_name.begins_with("metadata/")) {
-				formatted_text = TTR("Metadata:");
-			} else {
-				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 Property:");
-		} else {
-			ERR_FAIL_MSG("Invalid tooltip type '" + type + "'. Valid types are 'class', 'property', 'method', 'signal', and 'theme_item'.");
+	if (r.size.x + r.position.x > vr.size.x + vr.position.x) {
+		// Place it in the opposite direction. If it fails, just hug the border.
+		r.position.x = mouse_pos.x - r.size.x - tooltip_offset.x;
+
+		if (r.position.x < vr.position.x) {
+			r.position.x = vr.position.x + vr.size.x - r.size.x;
+		}
+	} else if (r.position.x < vr.position.x) {
+		r.position.x = vr.position.x;
+	}
+
+	if (r.size.y + r.position.y > vr.size.y + vr.position.y) {
+		// Same as above.
+		r.position.y = mouse_pos.y - r.size.y - tooltip_offset.y;
+
+		if (r.position.y < vr.position.y) {
+			r.position.y = vr.position.y + vr.size.y - r.size.y;
 		}
+	} else if (r.position.y < vr.position.y) {
+		r.position.y = vr.position.y;
 	}
 
-	// Metadata special handling replaces "Property:" with "Metadata": above.
-	formatted_text += " [u][b]" + title.trim_prefix("metadata/") + "[/b][/u]" + property_args.replace("[", "[lb]") + "\n";
-	formatted_text += description.is_empty() ? "[i]" + TTR("No description available.") + "[/i]" : description;
-	set_text(formatted_text);
+	set_flag(Window::FLAG_NO_FOCUS, true);
+	popup(r);
 }
 
-EditorHelpTooltip::EditorHelpTooltip(const String &p_text, const String &p_custom_description) {
-	tooltip_text = p_text;
-	custom_description = p_custom_description;
+EditorHelpBitTooltip::EditorHelpBitTooltip(Control *p_target) {
+	set_theme_type_variation("TooltipPanel");
+
+	timer = memnew(Timer);
+	timer->set_wait_time(0.2);
+	timer->connect("timeout", callable_mp(static_cast<Node *>(this), &Node::queue_free));
+	add_child(timer);
 
-	get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 0));
+	ERR_FAIL_NULL(p_target);
+	p_target->connect("mouse_entered", callable_mp(timer, &Timer::stop));
+	p_target->connect("mouse_exited", callable_mp(timer, &Timer::start).bind(-1));
 }
 
 #if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED)

+ 65 - 27
editor/editor_help.h

@@ -35,9 +35,9 @@
 #include "editor/code_editor.h"
 #include "editor/doc_tools.h"
 #include "editor/editor_plugin.h"
-#include "scene/gui/margin_container.h"
 #include "scene/gui/menu_button.h"
 #include "scene/gui/panel_container.h"
+#include "scene/gui/popup.h"
 #include "scene/gui/rich_text_label.h"
 #include "scene/gui/split_container.h"
 #include "scene/gui/tab_container.h"
@@ -251,53 +251,91 @@ public:
 	~EditorHelp();
 };
 
-class EditorHelpBit : public MarginContainer {
-	GDCLASS(EditorHelpBit, MarginContainer);
+class EditorHelpBit : public VBoxContainer {
+	GDCLASS(EditorHelpBit, VBoxContainer);
 
-	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;
+	struct DocType {
+		String type;
+		String enumeration;
+		bool is_bitfield = false;
+	};
+
+	struct ArgumentData {
+		String name;
+		DocType doc_type;
+		String default_value;
+	};
+
+	struct HelpData {
+		String description;
+		String deprecated_message;
+		String experimental_message;
+		DocType doc_type; // For method return type.
+		Vector<ArgumentData> arguments; // For methods and signals.
+	};
+
+	inline static HashMap<StringName, HelpData> doc_class_cache;
+	inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_property_cache;
+	inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_method_cache;
+	inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_signal_cache;
+	inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_theme_item_cache;
+
+	RichTextLabel *title = nullptr;
+	RichTextLabel *content = nullptr;
 
-	RichTextLabel *rich_text = nullptr;
+	String symbol_class_name;
+	String symbol_type;
+	String symbol_visible_type;
+	String symbol_name;
+
+	HelpData help_data;
+
+	float content_min_height = 0.0;
+	float content_max_height = 0.0;
+
+	static HelpData _get_class_help_data(const StringName &p_class_name);
+	static HelpData _get_property_help_data(const StringName &p_class_name, const StringName &p_property_name);
+	static HelpData _get_method_help_data(const StringName &p_class_name, const StringName &p_method_name);
+	static HelpData _get_signal_help_data(const StringName &p_class_name, const StringName &p_signal_name);
+	static HelpData _get_theme_item_help_data(const StringName &p_class_name, const StringName &p_theme_item_name);
+
+	void _add_type_to_title(const DocType &p_doc_type);
+	void _update_labels();
 	void _go_to_help(const String &p_what);
 	void _meta_clicked(const String &p_select);
 
-	String text;
-
 protected:
-	String doc_class_name;
-	String custom_description;
-
 	static void _bind_methods();
 	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;
+	void parse_symbol(const String &p_symbol);
+	void set_custom_text(const String &p_type, const String &p_name, const String &p_description);
+	void prepend_description(const String &p_text);
 
-	RichTextLabel *get_rich_text() { return rich_text; }
-	void set_text(const String &p_text);
+	void set_content_height_limits(float p_min, float p_max);
+	void update_content_height();
 
-	EditorHelpBit();
+	EditorHelpBit(const String &p_symbol = String());
 };
 
-class EditorHelpTooltip : public EditorHelpBit {
-	GDCLASS(EditorHelpTooltip, EditorHelpBit);
+// Standard tooltips do not allow you to hover over them.
+// This class is intended as a temporary workaround.
+class EditorHelpBitTooltip : public PopupPanel {
+	GDCLASS(EditorHelpBitTooltip, PopupPanel);
 
-	String tooltip_text;
+	Timer *timer = nullptr;
 
 protected:
 	void _notification(int p_what);
+	virtual void _input_from_window(const Ref<InputEvent> &p_event) override;
 
 public:
-	void parse_tooltip(const String &p_text);
+	static void show_tooltip(EditorHelpBit *p_help_bit, Control *p_target);
+
+	void popup_under_cursor();
 
-	EditorHelpTooltip(const String &p_text = String(), const String &p_custom_description = String());
+	EditorHelpBitTooltip(Control *p_target);
 };
 
 #if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED)

+ 37 - 28
editor/editor_inspector.cpp

@@ -44,6 +44,7 @@
 #include "editor/plugins/script_editor_plugin.h"
 #include "editor/themes/editor_scale.h"
 #include "editor/themes/editor_theme_manager.h"
+#include "scene/gui/margin_container.h"
 #include "scene/gui/spin_box.h"
 #include "scene/gui/texture_rect.h"
 #include "scene/property_utils.h"
@@ -916,34 +917,35 @@ void EditorProperty::_update_pin_flags() {
 }
 
 Control *EditorProperty::make_custom_tooltip(const String &p_text) const {
-	EditorHelpBit *tooltip = nullptr;
+	String custom_warning;
+	if (object->has_method("_get_property_warning")) {
+		custom_warning = object->call("_get_property_warning", property);
+	}
 
-	if (has_doc_tooltip) {
-		String custom_description;
+	if (has_doc_tooltip || !custom_warning.is_empty()) {
+		EditorHelpBit *help_bit = memnew(EditorHelpBit);
 
-		const EditorInspector *inspector = get_parent_inspector();
-		if (inspector) {
-			custom_description = inspector->get_custom_property_description(p_text);
-		}
-		tooltip = memnew(EditorHelpTooltip(p_text, custom_description));
-	}
+		if (has_doc_tooltip) {
+			help_bit->parse_symbol(p_text);
 
-	if (object->has_method("_get_property_warning")) {
-		String warn = object->call("_get_property_warning", property);
-		if (!warn.is_empty()) {
-			String prev_text;
-			if (tooltip == nullptr) {
-				tooltip = memnew(EditorHelpBit());
-				tooltip->set_text(p_text);
-				tooltip->get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 0));
-			} else {
-				prev_text = tooltip->get_rich_text()->get_text() + "\n";
+			const EditorInspector *inspector = get_parent_inspector();
+			if (inspector) {
+				const String custom_description = inspector->get_custom_property_description(p_text);
+				if (!custom_description.is_empty()) {
+					help_bit->prepend_description(custom_description);
+				}
 			}
-			tooltip->set_text(prev_text + "[b][color=" + get_theme_color(SNAME("warning_color")).to_html(false) + "]" + warn + "[/color][/b]");
 		}
+
+		if (!custom_warning.is_empty()) {
+			help_bit->prepend_description("[b][color=" + get_theme_color(SNAME("warning_color")).to_html(false) + "]" + custom_warning + "[/color][/b]");
+		}
+
+		EditorHelpBitTooltip::show_tooltip(help_bit, const_cast<EditorProperty *>(this));
+		return memnew(Control); // Make the standard tooltip invisible.
 	}
 
-	return tooltip;
+	return nullptr;
 }
 
 void EditorProperty::menu_option(int p_option) {
@@ -1178,7 +1180,14 @@ void EditorInspectorCategory::_notification(int p_what) {
 }
 
 Control *EditorInspectorCategory::make_custom_tooltip(const String &p_text) const {
-	return doc_class_name.is_empty() ? nullptr : memnew(EditorHelpTooltip(p_text));
+	// If it's not a doc tooltip, fallback to the default one.
+	if (doc_class_name.is_empty()) {
+		return nullptr;
+	}
+
+	EditorHelpBit *help_bit = memnew(EditorHelpBit(p_text));
+	EditorHelpBitTooltip::show_tooltip(help_bit, const_cast<EditorInspectorCategory *>(this));
+	return memnew(Control); // Make the standard tooltip invisible.
 }
 
 Size2 EditorInspectorCategory::get_minimum_size() const {
@@ -2887,8 +2896,8 @@ void EditorInspector::update_tree() {
 				category->doc_class_name = doc_name;
 
 				if (use_doc_hints) {
-					// `|` separator used in `EditorHelpTooltip` for formatting.
-					category->set_tooltip_text("class|" + doc_name + "||");
+					// `|` separators used in `EditorHelpBit`.
+					category->set_tooltip_text("class|" + doc_name + "|");
 				}
 			}
 
@@ -3368,15 +3377,15 @@ void EditorInspector::update_tree() {
 				ep->connect("object_id_selected", callable_mp(this, &EditorInspector::_object_id_selected), CONNECT_DEFERRED);
 
 				if (use_doc_hints) {
-					// `|` separator used in `EditorHelpTooltip` for formatting.
+					// `|` separators used in `EditorHelpBit`.
 					if (theme_item_name.is_empty()) {
 						if (p.usage & PROPERTY_USAGE_INTERNAL) {
-							ep->set_tooltip_text("internal_property|" + classname + "|" + property_prefix + p.name + "|");
+							ep->set_tooltip_text("internal_property|" + classname + "|" + property_prefix + p.name);
 						} else {
-							ep->set_tooltip_text("property|" + classname + "|" + property_prefix + p.name + "|");
+							ep->set_tooltip_text("property|" + classname + "|" + property_prefix + p.name);
 						}
 					} else {
-						ep->set_tooltip_text("theme_item|" + classname + "|" + theme_item_name + "|");
+						ep->set_tooltip_text("theme_item|" + classname + "|" + theme_item_name);
 					}
 					ep->has_doc_tooltip = true;
 				}

+ 1 - 0
editor/editor_inspector.h

@@ -41,6 +41,7 @@ class ConfirmationDialog;
 class EditorInspector;
 class EditorValidationPanel;
 class LineEdit;
+class MarginContainer;
 class OptionButton;
 class PanelContainer;
 class PopupMenu;

+ 1 - 0
editor/editor_properties_array_dict.cpp

@@ -40,6 +40,7 @@
 #include "editor/inspector_dock.h"
 #include "editor/themes/editor_scale.h"
 #include "scene/gui/button.h"
+#include "scene/gui/margin_container.h"
 #include "scene/resources/packed_scene.h"
 
 bool EditorPropertyArrayObject::_set(const StringName &p_name, const Variant &p_value) {

+ 1 - 0
editor/editor_properties_array_dict.h

@@ -37,6 +37,7 @@
 
 class Button;
 class EditorSpinSlider;
+class MarginContainer;
 
 class EditorPropertyArrayObject : public RefCounted {
 	GDCLASS(EditorPropertyArrayObject, RefCounted);

+ 5 - 3
editor/plugins/theme_editor_plugin.cpp

@@ -2267,7 +2267,9 @@ ThemeTypeDialog::ThemeTypeDialog() {
 ///////////////////////
 
 Control *ThemeItemLabel::make_custom_tooltip(const String &p_text) const {
-	return memnew(EditorHelpTooltip(p_text));
+	EditorHelpBit *help_bit = memnew(EditorHelpBit(p_text));
+	EditorHelpBitTooltip::show_tooltip(help_bit, const_cast<ThemeItemLabel *>(this));
+	return memnew(Control); // Make the standard tooltip invisible.
 }
 
 VBoxContainer *ThemeTypeEditor::_create_item_list(Theme::DataType p_data_type) {
@@ -2436,8 +2438,8 @@ HBoxContainer *ThemeTypeEditor::_create_property_control(Theme::DataType p_data_
 	item_name->set_h_size_flags(SIZE_EXPAND_FILL);
 	item_name->set_clip_text(true);
 	item_name->set_text(p_item_name);
-	// `|` separators used in `EditorHelpTooltip` for formatting.
-	item_name->set_tooltip_text("theme_item|" + edited_type + "|" + p_item_name + "|");
+	// `|` separators used in `EditorHelpBit`.
+	item_name->set_tooltip_text("theme_item|" + edited_type + "|" + p_item_name);
 	item_name->set_mouse_filter(Control::MOUSE_FILTER_STOP);
 	item_name_container->add_child(item_name);
 

+ 1 - 1
editor/plugins/theme_editor_plugin.h

@@ -321,7 +321,7 @@ public:
 	ThemeTypeDialog();
 };
 
-// Custom `Label` needed to use `EditorHelpTooltip` to display theme item documentation.
+// Custom `Label` needed to use `EditorHelpBit` to display theme item documentation.
 class ThemeItemLabel : public Label {
 	virtual Control *make_custom_tooltip(const String &p_text) const;
 };

+ 14 - 19
editor/property_selector.cpp

@@ -87,7 +87,7 @@ void PropertySelector::_update_search() {
 	}
 
 	search_options->clear();
-	help_bit->set_text("");
+	help_bit->set_custom_text(String(), String(), String());
 
 	TreeItem *root = search_options->create_item();
 
@@ -353,7 +353,7 @@ void PropertySelector::_confirmed() {
 }
 
 void PropertySelector::_item_selected() {
-	help_bit->set_text("");
+	help_bit->set_custom_text(String(), String(), String());
 
 	TreeItem *item = search_options->get_selected();
 	if (!item) {
@@ -372,25 +372,21 @@ void PropertySelector::_item_selected() {
 
 	String text;
 	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;
+		if (properties) {
+			if (ClassDB::has_property(class_type, name, true)) {
+				help_bit->parse_symbol("property|" + class_type + "|" + name);
+				break;
+			}
+		} else {
+			if (ClassDB::has_method(class_type, name, true)) {
+				help_bit->parse_symbol("method|" + class_type + "|" + name);
+				break;
+			}
 		}
 
 		// It may be from a parent class, keep looking.
 		class_type = ClassDB::get_parent_class(class_type);
 	}
-
-	if (!text.is_empty()) {
-		// Display both property 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", name, 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.
-		help_bit->set_text(vformat(TTR("No description available for %s."), vformat("[b]%s[/b]", name)));
-		help_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5));
-	}
 }
 
 void PropertySelector::_hide_requested() {
@@ -569,8 +565,7 @@ PropertySelector::PropertySelector() {
 	search_options->set_hide_folding(true);
 
 	help_bit = memnew(EditorHelpBit);
-	vbc->add_margin_child(TTR("Description:"), help_bit);
-	help_bit->get_rich_text()->set_fit_content(false);
-	help_bit->get_rich_text()->set_custom_minimum_size(Size2(help_bit->get_rich_text()->get_minimum_size().x, 135 * EDSCALE));
+	help_bit->set_content_height_limits(80 * EDSCALE, 80 * EDSCALE);
 	help_bit->connect("request_hide", callable_mp(this, &PropertySelector::_hide_requested));
+	vbc->add_margin_child(TTR("Description:"), help_bit);
 }

+ 22 - 0
editor/themes/editor_theme_manager.cpp

@@ -2158,6 +2158,28 @@ void EditorThemeManager::_populate_editor_styles(const Ref<EditorTheme> &p_theme
 		p_theme->set_constant("text_highlight_v_padding", "EditorHelp", 2 * EDSCALE);
 	}
 
+	// EditorHelpBitTitle.
+	{
+		Ref<StyleBoxFlat> style = p_config.tree_panel_style->duplicate();
+		style->set_bg_color(p_config.dark_theme ? style->get_bg_color().lightened(0.04) : style->get_bg_color().darkened(0.04));
+		style->set_border_color(p_config.dark_theme ? style->get_border_color().lightened(0.04) : style->get_border_color().darkened(0.04));
+		style->set_corner_radius(CORNER_BOTTOM_LEFT, 0);
+		style->set_corner_radius(CORNER_BOTTOM_RIGHT, 0);
+
+		p_theme->set_type_variation("EditorHelpBitTitle", "RichTextLabel");
+		p_theme->set_stylebox("normal", "EditorHelpBitTitle", style);
+	}
+
+	// EditorHelpBitContent.
+	{
+		Ref<StyleBoxFlat> style = p_config.tree_panel_style->duplicate();
+		style->set_corner_radius(CORNER_TOP_LEFT, 0);
+		style->set_corner_radius(CORNER_TOP_RIGHT, 0);
+
+		p_theme->set_type_variation("EditorHelpBitContent", "RichTextLabel");
+		p_theme->set_stylebox("normal", "EditorHelpBitContent", style);
+	}
+
 	// Asset Library.
 	p_theme->set_stylebox("bg", "AssetLib", p_config.base_empty_style);
 	p_theme->set_stylebox("panel", "AssetLib", p_config.content_panel_style);