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/check_box.h"
 #include "scene/gui/label.h"
 #include "scene/gui/label.h"
 #include "scene/gui/line_edit.h"
 #include "scene/gui/line_edit.h"
+#include "scene/gui/margin_container.h"
 #include "scene/gui/option_button.h"
 #include "scene/gui/option_button.h"
 #include "scene/gui/popup_menu.h"
 #include "scene/gui/popup_menu.h"
 #include "scene/gui/spin_box.h"
 #include "scene/gui/spin_box.h"
@@ -872,7 +873,13 @@ ConnectDialog::~ConnectDialog() {
 
 
 Control *ConnectionsDockTree::make_custom_tooltip(const String &p_text) const {
 Control *ConnectionsDockTree::make_custom_tooltip(const String &p_text) const {
 	// If it's not a doc tooltip, fallback to the default one.
 	// 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 {
 struct _ConnectionsDockMethodInfoSort {
@@ -1458,8 +1465,8 @@ void ConnectionsDock::update_tree() {
 
 
 			section_item = tree->create_item(root);
 			section_item = tree->create_item(root);
 			section_item->set_text(0, class_name);
 			section_item->set_text(0, class_name);
-			// `|` separators used in `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_icon(0, class_icon);
 			section_item->set_selectable(0, false);
 			section_item->set_selectable(0, false);
 			section_item->set_editable(0, false);
 			section_item->set_editable(0, false);
@@ -1490,8 +1497,8 @@ void ConnectionsDock::update_tree() {
 			sinfo["args"] = argnames;
 			sinfo["args"] = argnames;
 			signal_item->set_metadata(0, sinfo);
 			signal_item->set_metadata(0, sinfo);
 			signal_item->set_icon(0, get_editor_theme_icon(SNAME("Signal")));
 			signal_item->set_icon(0, get_editor_theme_icon(SNAME("Signal")));
-			// `|` separators used in `EditorHelpTooltip` for formatting.
-			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 existing connections.
 			List<Object::Connection> 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 {
 class ConnectionsDockTree : public Tree {
 	virtual Control *make_custom_tooltip(const String &p_text) const;
 	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));
 		select_type(_top_result(candidates, search_text));
 	} else {
 	} else {
 		favorite->set_disabled(true);
 		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);
 		get_ok_button()->set_disabled(true);
 		search_options->deselect_all();
 		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);
 	to_select->select(0);
 	search_options->scroll_to_item(to_select, p_center_on_item);
 	search_options->scroll_to_item(to_select, p_center_on_item);
 
 
-	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_disabled(false);
 	favorite->set_pressed(favorite_list.has(p_type));
 	favorite->set_pressed(favorite_list.has(p_type));
@@ -837,6 +826,7 @@ CreateDialog::CreateDialog() {
 	vbc->add_margin_child(TTR("Matches:"), search_options, true);
 	vbc->add_margin_child(TTR("Matches:"), search_options, true);
 
 
 	help_bit = memnew(EditorHelpBit);
 	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));
 	help_bit->connect("request_hide", callable_mp(this, &CreateDialog::_hide_requested));
 	vbc->add_margin_child(TTR("Description:"), help_bit);
 	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) {
 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);
 	TreeItem *class_item = class_list->create_item(p_parent);
 	class_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
 	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);
 	Variant md = item->get_metadata(0);
 	if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
 	if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
-		String 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) {
 	} else if (md.get_type() == Variant::INT) {
 		String build_option_description = EditorBuildProfile::get_build_option_description(EditorBuildProfile::BuildOption((int)md));
 		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);
 	main_vbc->add_margin_child(TTR("Configure Engine Compilation Profile:"), class_list, true);
 
 
 	description_bit = memnew(EditorHelpBit);
 	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);
 	main_vbc->add_margin_child(TTR("Description:"), description_bit, false);
 
 
 	confirm_dialog = memnew(ConfirmationDialog);
 	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 _profile_action(int p_action);
 	void _action_confirm();
 	void _action_confirm();
+	void _hide_requested();
 
 
 	void _update_edited_profile();
 	void _update_edited_profile();
 	void _fill_classes_from(TreeItem *p_parent, const String &p_class, const String &p_selected);
 	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();
 	_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) {
 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);
 	TreeItem *class_item = class_list->create_item(p_parent);
 	class_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
 	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);
 	Variant md = item->get_metadata(0);
 	if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
 	if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
-		String 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) {
 	} else if (md.get_type() == Variant::INT) {
 		String feature_description = EditorFeatureProfile::get_feature_description(EditorFeatureProfile::Feature((int)md));
 		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;
 		return;
 	} else {
 	} else {
 		return;
 		return;
@@ -991,8 +983,9 @@ EditorFeatureProfileManager::EditorFeatureProfileManager() {
 	property_list_vbc->set_h_size_flags(Control::SIZE_EXPAND_FILL);
 	property_list_vbc->set_h_size_flags(Control::SIZE_EXPAND_FILL);
 
 
 	description_bit = memnew(EditorHelpBit);
 	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);
 	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 = memnew(Tree);
 	property_list_vbc->add_margin_child(TTR("Extra Options:"), property_list, true);
 	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_action(int p_action);
 	void _profile_selected(int p_what);
 	void _profile_selected(int p_what);
+	void _hide_requested();
 
 
 	String current_profile;
 	String current_profile;
 	void _update_profile_list(const String &p_select_profile = String());
 	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 "editor_help.h"
 
 
+#include "core/config/project_settings.h"
 #include "core/core_constants.h"
 #include "core/core_constants.h"
 #include "core/extension/gdextension.h"
 #include "core/extension/gdextension.h"
 #include "core/input/input.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_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_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("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_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")));
 	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) {
 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 {
 		} 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;
 		String topic;
-		HashMap<String, int> *table = nullptr;
+		const HashMap<String, int> *table = nullptr;
 
 
 		if (tag == "method") {
 		if (tag == "method") {
 			topic = "class_method";
 			topic = "class_method";
@@ -311,14 +317,14 @@ void EditorHelp::_class_desc_select(const String &p_select) {
 			}
 			}
 
 
 			if (link.contains(".")) {
 			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);
 		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") {
 	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;
 		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.
 	String display_t; // For display purposes.
 	if (is_enum_type) {
 	if (is_enum_type) {
 		link_t = p_enum; // The link for enums is always the full enum description
 		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 {
 	} 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;
 	bool add_array = false;
 	if (can_ref) {
 	if (can_ref) {
 		if (link_t.ends_with("[]")) {
 		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("[]");
 			link_t = link_t.trim_suffix("[]");
 			display_t = display_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) {
 		} 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) {
 		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 {
 		} 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) {
 	if (can_ref) {
-		class_desc->pop(); // meta
+		p_rt->pop(); // meta
 		if (add_array) {
 		if (add_array) {
-			class_desc->add_text("]");
+			p_rt->add_text("]");
 		} else if (is_bitfield) {
 		} 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) {
 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;
 		String group_prefix;
 		for (int i = 0; i < m.size(); i++) {
 		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;
 			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;
 				is_new_group = i > 0;
 				group_prefix = new_prefix;
 				group_prefix = new_prefix;
 			} else if (!group_prefix.is_empty() && new_prefix != group_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) {
 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();
 	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 constructor may be changed or removed in future versions."),
 					TTRC("This operator 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) {
 			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 constructor may be changed or removed in future versions."),
 					TTRC("This operator 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()) {
 			if (!method.errors_returned.is_empty()) {
@@ -856,7 +868,7 @@ void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc
 				class_desc->pop(); // list
 				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();
 			const bool is_documented = method.is_deprecated || method.is_experimental || !descr.is_empty();
 			if (!descr.is_empty()) {
 			if (!descr.is_empty()) {
 				if (has_prev_text) {
 				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() {
 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.
 	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
 	// Class name
 
 
@@ -940,12 +952,12 @@ void EditorHelp::_update_doc() {
 
 
 	if (cd.is_deprecated) {
 	if (cd.is_deprecated) {
 		class_desc->add_newline();
 		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) {
 	if (cd.is_experimental) {
 		class_desc->add_newline();
 		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
 	// Inheritance tree
@@ -1003,7 +1015,7 @@ void EditorHelp::_update_doc() {
 	bool has_description = false;
 	bool has_description = false;
 
 
 	// Brief description
 	// 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()) {
 	if (!brief_class_descr.is_empty()) {
 		has_description = true;
 		has_description = true;
 
 
@@ -1022,7 +1034,7 @@ void EditorHelp::_update_doc() {
 	}
 	}
 
 
 	// Class description
 	// 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()) {
 	if (!class_descr.is_empty()) {
 		has_description = true;
 		has_description = true;
 
 
@@ -1106,9 +1118,9 @@ void EditorHelp::_update_doc() {
 		class_desc->push_color(theme_cache.symbol_color);
 		class_desc->push_color(theme_cache.symbol_color);
 
 
 		for (const DocData::TutorialDoc &tutorial : cd.tutorials) {
 		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()) {
 			if (link_text.is_empty()) {
 				const int sep_pos = link.find("//");
 				const int sep_pos = link.find("//");
 				if (sep_pos >= 0) {
 				if (sep_pos >= 0) {
@@ -1441,7 +1453,7 @@ void EditorHelp::_update_doc() {
 			_push_normal_font();
 			_push_normal_font();
 			class_desc->push_color(theme_cache.comment_color);
 			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()) {
 			if (!descr.is_empty()) {
 				_add_text(descr);
 				_add_text(descr);
 			} else {
 			} else {
@@ -1538,13 +1550,13 @@ void EditorHelp::_update_doc() {
 			_push_normal_font();
 			_push_normal_font();
 			class_desc->push_color(theme_cache.comment_color);
 			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;
 			const bool is_multiline = descr.find_char('\n') > 0;
 			bool has_prev_text = false;
 			bool has_prev_text = false;
 
 
 			if (signal.is_deprecated) {
 			if (signal.is_deprecated) {
 				has_prev_text = true;
 				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) {
 			if (signal.is_experimental) {
@@ -1555,7 +1567,7 @@ void EditorHelp::_update_doc() {
 					}
 					}
 				}
 				}
 				has_prev_text = true;
 				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()) {
 			if (!descr.is_empty()) {
@@ -1669,7 +1681,7 @@ void EditorHelp::_update_doc() {
 
 
 				// Enum description.
 				// Enum description.
 				if (key != "@unnamed_enums" && cd.enums.has(key)) {
 				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;
 					const bool is_multiline = descr.find_char('\n') > 0;
 					if (cd.enums[key].is_deprecated || cd.enums[key].is_experimental || !descr.is_empty()) {
 					if (cd.enums[key].is_deprecated || cd.enums[key].is_experimental || !descr.is_empty()) {
 						class_desc->add_newline();
 						class_desc->add_newline();
@@ -1682,7 +1694,7 @@ void EditorHelp::_update_doc() {
 
 
 						if (cd.enums[key].is_deprecated) {
 						if (cd.enums[key].is_deprecated) {
 							has_prev_text = true;
 							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) {
 						if (cd.enums[key].is_experimental) {
@@ -1693,7 +1705,7 @@ void EditorHelp::_update_doc() {
 								}
 								}
 							}
 							}
 							has_prev_text = true;
 							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()) {
 						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.
 				bool prev_is_multiline = true; // Use a large margin for the first item.
 				for (const DocData::ConstantDoc &enum_value : E.value) {
 				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;
 					const bool is_multiline = descr.find_char('\n') > 0;
 
 
 					class_desc->add_newline();
 					class_desc->add_newline();
@@ -1766,7 +1778,7 @@ void EditorHelp::_update_doc() {
 
 
 						if (enum_value.is_deprecated) {
 						if (enum_value.is_deprecated) {
 							has_prev_text = true;
 							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) {
 						if (enum_value.is_experimental) {
@@ -1777,7 +1789,7 @@ void EditorHelp::_update_doc() {
 								}
 								}
 							}
 							}
 							has_prev_text = true;
 							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()) {
 						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.
 			bool prev_is_multiline = true; // Use a large margin for the first item.
 			for (const DocData::ConstantDoc &constant : constants) {
 			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;
 				const bool is_multiline = descr.find_char('\n') > 0;
 
 
 				class_desc->add_newline();
 				class_desc->add_newline();
@@ -1871,7 +1883,7 @@ void EditorHelp::_update_doc() {
 
 
 					if (constant.is_deprecated) {
 					if (constant.is_deprecated) {
 						has_prev_text = true;
 						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) {
 					if (constant.is_experimental) {
@@ -1882,7 +1894,7 @@ void EditorHelp::_update_doc() {
 							}
 							}
 						}
 						}
 						has_prev_text = true;
 						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()) {
 					if (!descr.is_empty()) {
@@ -2000,7 +2012,7 @@ void EditorHelp::_update_doc() {
 			_push_normal_font();
 			_push_normal_font();
 			class_desc->push_color(theme_cache.comment_color);
 			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()) {
 			if (!descr.is_empty()) {
 				_add_text(descr);
 				_add_text(descr);
 			} else {
 			} else {
@@ -2185,7 +2197,7 @@ void EditorHelp::_update_doc() {
 
 
 			if (prop.is_deprecated) {
 			if (prop.is_deprecated) {
 				has_prev_text = true;
 				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) {
 			if (prop.is_experimental) {
@@ -2194,10 +2206,10 @@ void EditorHelp::_update_doc() {
 					class_desc->add_newline();
 					class_desc->add_newline();
 				}
 				}
 				has_prev_text = true;
 				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 (!descr.is_empty()) {
 				if (has_prev_text) {
 				if (has_prev_text) {
 					class_desc->add_newline();
 					class_desc->add_newline();
@@ -2251,7 +2263,7 @@ void EditorHelp::_update_doc() {
 	// Free the scroll.
 	// Free the scroll.
 	scroll_locked = false;
 	scroll_locked = false;
 
 
-#undef DTR_DOC
+#undef HANDLE_DOC
 }
 }
 
 
 void EditorHelp::_request_help(const String &p_string) {
 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();
 	const DocTools *doc = EditorHelp::get_doc_data();
 
 
 	bool is_native = false;
 	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);
 		const String tag = bbcode.substr(brk_pos + 1, brk_end - brk_pos - 1);
 
 
 		if (tag.begins_with("/")) {
 		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) {
 			if (!tag_ok) {
 				p_rt->add_text("[");
 				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 ")) {
 		} 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 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;
 			Color target_color = link_color;
 			RichTextLabel::MetaUnderline underline_mode = RichTextLabel::META_UNDERLINE_ON_HOVER;
 			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;
 			pos = brk_end + 1;
 		} else if (tag.begins_with("param ")) {
 		} else if (tag.begins_with("param ")) {
 			const int tag_end = tag.find_char(' ');
 			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.
 			// Use monospace font with translucent background color to make code easier to distinguish from other text.
 			p_rt->push_font(doc_code_font);
 			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;
 			pos = brk_end + 1;
 			tag_stack.push_front(tag);
 			tag_stack.push_front(tag);
 		} else if (tag.begins_with("url=")) {
 		} else if (tag.begins_with("url=")) {
-			String url = tag.substr(4, tag.length());
+			String url = tag.substr(4);
 			p_rt->push_meta(url);
 			p_rt->push_meta(url);
 
 
 			pos = brk_end + 1;
 			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;
 			int height = 0;
 			bool size_in_percent = false;
 			bool size_in_percent = false;
 			if (tag.length() > 4) {
 			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;
 				HashMap<String, String> bbcode_options;
 				for (int i = 0; i < subtags.size(); i++) {
 				for (int i = 0; i < subtags.size(); i++) {
 					const String &expr = subtags[i];
 					const String &expr = subtags[i];
 					int value_pos = expr.find_char('=');
 					int value_pos = expr.find_char('=');
 					if (value_pos > -1) {
 					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");
 				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;
 			pos = end;
 			tag_stack.push_front("img");
 			tag_stack.push_front("img");
 		} else if (tag.begins_with("color=")) {
 		} 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());
 			Color color = Color::from_string(col, Color());
 			p_rt->push_color(color);
 			p_rt->push_color(color);
 
 
 			pos = brk_end + 1;
 			pos = brk_end + 1;
 			tag_stack.push_front("color");
 			tag_stack.push_front("color");
 		} else if (tag.begins_with("font=")) {
 		} 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");
 			Ref<Font> font = ResourceLoader::load(font_path, "Font");
 			if (font.is_valid()) {
 			if (font.is_valid()) {
 				p_rt->push_font(font);
 				p_rt->push_font(font);
@@ -3120,68 +3132,50 @@ DocTools *EditorHelp::get_doc_data() {
 
 
 /// EditorHelpBit ///
 /// 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)) {
 	if (doc_class_cache.has(p_class_name)) {
 		return doc_class_cache[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);
 	const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
 	if (E) {
 	if (E) {
 		// Non-native class shouldn't be cached, nor translated.
 		// Non-native class shouldn't be cached, nor translated.
 		const bool is_native = !E->value.is_script_doc;
 		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) {
 		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)) {
 	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];
 		return doc_property_cache[p_class_name][p_property_name];
 	}
 	}
 
 
-	String description;
+	HelpData result;
 
 
 	const DocTools *dd = EditorHelp::get_doc_data();
 	const DocTools *dd = EditorHelp::get_doc_data();
 	const HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name);
 	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;
 		const bool is_native = !E->value.is_script_doc;
 
 
 		for (const DocData::PropertyDoc &property : E->value.properties) {
 		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_class_name;
 			String enum_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")) {
 						if (constant.enumeration == enum_name && !constant.name.ends_with("_MAX")) {
 							// Prettify the enum value display, so that "<ENUM_NAME>_<ITEM>" becomes "Item".
 							// 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);
 							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()) {
 							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) {
 			if (property.name == p_property_name) {
-				description = description_current;
+				result = current;
 
 
 				if (!is_native) {
 				if (!is_native) {
 					break;
 					break;
@@ -3234,20 +3244,20 @@ String EditorHelpBit::get_property_description(const StringName &p_class_name, c
 			}
 			}
 
 
 			if (is_native) {
 			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)) {
 	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];
 		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);
 	const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
 	if (E) {
 	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;
 		const bool is_native = !E->value.is_script_doc;
 
 
 		for (const DocData::MethodDoc &method : E->value.methods) {
 		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) {
 			if (method.name == p_method_name) {
-				description = description_current;
+				result = current;
 
 
 				if (!is_native) {
 				if (!is_native) {
 					break;
 					break;
@@ -3266,20 +3296,20 @@ String EditorHelpBit::get_method_description(const StringName &p_class_name, con
 			}
 			}
 
 
 			if (is_native) {
 			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)) {
 	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];
 		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);
 	const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
 	if (E) {
 	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;
 		const bool is_native = !E->value.is_script_doc;
 
 
 		for (const DocData::MethodDoc &signal : E->value.signals) {
 		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) {
 			if (signal.name == p_signal_name) {
-				description = description_current;
+				result = current;
 
 
 				if (!is_native) {
 				if (!is_native) {
 					break;
 					break;
@@ -3298,20 +3347,20 @@ String EditorHelpBit::get_signal_description(const StringName &p_class_name, con
 			}
 			}
 
 
 			if (is_native) {
 			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)) {
 	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];
 		return doc_theme_item_cache[p_class_name][p_theme_item_name];
 	}
 	}
 
 
-	String description;
+	HelpData result;
 
 
 	bool found = false;
 	bool found = false;
 	const DocTools *dd = EditorHelp::get_doc_data();
 	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;
 		const bool is_native = !E->value.is_script_doc;
 
 
 		for (const DocData::ThemeItemDoc &theme_item : E->value.theme_properties) {
 		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) {
 			if (theme_item.name == p_theme_item_name) {
-				description = description_current;
+				result = current;
 				found = true;
 				found = true;
-
 				if (!is_native) {
 				if (!is_native) {
 					break;
 					break;
 				}
 				}
 			}
 			}
 
 
 			if (is_native) {
 			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()) {
 		if (found || E->value.inherits.is_empty()) {
 			break;
 			break;
 		}
 		}
+
 		// Check for inherited theme items.
 		// Check for inherited theme items.
 		E = dd->class_list.find(E->value.inherits);
 		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() {
 void EditorHelpBit::_bind_methods() {
-	ClassDB::bind_method(D_METHOD("set_text", "text"), &EditorHelpBit::set_text);
 	ADD_SIGNAL(MethodInfo("request_hide"));
 	ADD_SIGNAL(MethodInfo("request_hide"));
 }
 }
 
 
 void EditorHelpBit::_notification(int p_what) {
 void EditorHelpBit::_notification(int p_what) {
 	switch (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 {
 	} 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)
 #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/code_editor.h"
 #include "editor/doc_tools.h"
 #include "editor/doc_tools.h"
 #include "editor/editor_plugin.h"
 #include "editor/editor_plugin.h"
-#include "scene/gui/margin_container.h"
 #include "scene/gui/menu_button.h"
 #include "scene/gui/menu_button.h"
 #include "scene/gui/panel_container.h"
 #include "scene/gui/panel_container.h"
+#include "scene/gui/popup.h"
 #include "scene/gui/rich_text_label.h"
 #include "scene/gui/rich_text_label.h"
 #include "scene/gui/split_container.h"
 #include "scene/gui/split_container.h"
 #include "scene/gui/tab_container.h"
 #include "scene/gui/tab_container.h"
@@ -251,53 +251,91 @@ public:
 	~EditorHelp();
 	~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 _go_to_help(const String &p_what);
 	void _meta_clicked(const String &p_select);
 	void _meta_clicked(const String &p_select);
 
 
-	String text;
-
 protected:
 protected:
-	String doc_class_name;
-	String custom_description;
-
 	static void _bind_methods();
 	static void _bind_methods();
 	void _notification(int p_what);
 	void _notification(int p_what);
 
 
 public:
 public:
-	String get_class_description(const StringName &p_class_name) const;
-	String get_property_description(const StringName &p_class_name, const StringName &p_property_name) const;
-	String get_method_description(const StringName &p_class_name, const StringName &p_method_name) const;
-	String get_signal_description(const StringName &p_class_name, const StringName &p_signal_name) const;
-	String get_theme_item_description(const StringName &p_class_name, const StringName &p_theme_item_name) const;
+	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:
 protected:
 	void _notification(int p_what);
 	void _notification(int p_what);
+	virtual void _input_from_window(const Ref<InputEvent> &p_event) override;
 
 
 public:
 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)
 #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/plugins/script_editor_plugin.h"
 #include "editor/themes/editor_scale.h"
 #include "editor/themes/editor_scale.h"
 #include "editor/themes/editor_theme_manager.h"
 #include "editor/themes/editor_theme_manager.h"
+#include "scene/gui/margin_container.h"
 #include "scene/gui/spin_box.h"
 #include "scene/gui/spin_box.h"
 #include "scene/gui/texture_rect.h"
 #include "scene/gui/texture_rect.h"
 #include "scene/property_utils.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 {
 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) {
 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 {
 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 {
 Size2 EditorInspectorCategory::get_minimum_size() const {
@@ -2887,8 +2896,8 @@ void EditorInspector::update_tree() {
 				category->doc_class_name = doc_name;
 				category->doc_class_name = doc_name;
 
 
 				if (use_doc_hints) {
 				if (use_doc_hints) {
-					// `|` 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);
 				ep->connect("object_id_selected", callable_mp(this, &EditorInspector::_object_id_selected), CONNECT_DEFERRED);
 
 
 				if (use_doc_hints) {
 				if (use_doc_hints) {
-					// `|` separator used in `EditorHelpTooltip` for formatting.
+					// `|` separators used in `EditorHelpBit`.
 					if (theme_item_name.is_empty()) {
 					if (theme_item_name.is_empty()) {
 						if (p.usage & PROPERTY_USAGE_INTERNAL) {
 						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 {
 						} else {
-							ep->set_tooltip_text("property|" + classname + "|" + property_prefix + p.name + "|");
+							ep->set_tooltip_text("property|" + classname + "|" + property_prefix + p.name);
 						}
 						}
 					} else {
 					} 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;
 					ep->has_doc_tooltip = true;
 				}
 				}

+ 1 - 0
editor/editor_inspector.h

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

+ 1 - 0
editor/editor_properties_array_dict.cpp

@@ -40,6 +40,7 @@
 #include "editor/inspector_dock.h"
 #include "editor/inspector_dock.h"
 #include "editor/themes/editor_scale.h"
 #include "editor/themes/editor_scale.h"
 #include "scene/gui/button.h"
 #include "scene/gui/button.h"
+#include "scene/gui/margin_container.h"
 #include "scene/resources/packed_scene.h"
 #include "scene/resources/packed_scene.h"
 
 
 bool EditorPropertyArrayObject::_set(const StringName &p_name, const Variant &p_value) {
 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 Button;
 class EditorSpinSlider;
 class EditorSpinSlider;
+class MarginContainer;
 
 
 class EditorPropertyArrayObject : public RefCounted {
 class EditorPropertyArrayObject : public RefCounted {
 	GDCLASS(EditorPropertyArrayObject, 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 {
 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) {
 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_h_size_flags(SIZE_EXPAND_FILL);
 	item_name->set_clip_text(true);
 	item_name->set_clip_text(true);
 	item_name->set_text(p_item_name);
 	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->set_mouse_filter(Control::MOUSE_FILTER_STOP);
 	item_name_container->add_child(item_name);
 	item_name_container->add_child(item_name);
 
 

+ 1 - 1
editor/plugins/theme_editor_plugin.h

@@ -321,7 +321,7 @@ public:
 	ThemeTypeDialog();
 	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 {
 class ThemeItemLabel : public Label {
 	virtual Control *make_custom_tooltip(const String &p_text) const;
 	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();
 	search_options->clear();
-	help_bit->set_text("");
+	help_bit->set_custom_text(String(), String(), String());
 
 
 	TreeItem *root = search_options->create_item();
 	TreeItem *root = search_options->create_item();
 
 
@@ -353,7 +353,7 @@ void PropertySelector::_confirmed() {
 }
 }
 
 
 void PropertySelector::_item_selected() {
 void PropertySelector::_item_selected() {
-	help_bit->set_text("");
+	help_bit->set_custom_text(String(), String(), String());
 
 
 	TreeItem *item = search_options->get_selected();
 	TreeItem *item = search_options->get_selected();
 	if (!item) {
 	if (!item) {
@@ -372,25 +372,21 @@ void PropertySelector::_item_selected() {
 
 
 	String text;
 	String text;
 	while (!class_type.is_empty()) {
 	while (!class_type.is_empty()) {
-		text = properties ? help_bit->get_property_description(class_type, name) : help_bit->get_method_description(class_type, name);
-		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.
 		// It may be from a parent class, keep looking.
 		class_type = ClassDB::get_parent_class(class_type);
 		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() {
 void PropertySelector::_hide_requested() {
@@ -569,8 +565,7 @@ PropertySelector::PropertySelector() {
 	search_options->set_hide_folding(true);
 	search_options->set_hide_folding(true);
 
 
 	help_bit = memnew(EditorHelpBit);
 	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));
 	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);
 		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.
 	// Asset Library.
 	p_theme->set_stylebox("bg", "AssetLib", p_config.base_empty_style);
 	p_theme->set_stylebox("bg", "AssetLib", p_config.base_empty_style);
 	p_theme->set_stylebox("panel", "AssetLib", p_config.content_panel_style);
 	p_theme->set_stylebox("panel", "AssetLib", p_config.content_panel_style);