From a714cb9f65faefaa21bef240397ca6d249edd53c Mon Sep 17 00:00:00 2001 From: Danil Alexeev Date: Thu, 11 Apr 2024 11:21:44 +0300 Subject: [PATCH] Editor: Display deprecated/experimental messages in tooltips --- editor/connections_dialog.cpp | 17 +- editor/connections_dialog.h | 2 +- editor/create_dialog.cpp | 16 +- editor/editor_build_profile.cpp | 22 +- editor/editor_build_profile.h | 1 + editor/editor_feature_profile.cpp | 23 +- editor/editor_feature_profile.h | 1 + editor/editor_help.cpp | 884 +++++++++++++++++------- editor/editor_help.h | 92 ++- editor/editor_inspector.cpp | 73 +- editor/editor_inspector.h | 1 + editor/editor_properties_array_dict.cpp | 1 + editor/editor_properties_array_dict.h | 1 + editor/plugins/theme_editor_plugin.cpp | 8 +- editor/plugins/theme_editor_plugin.h | 2 +- editor/property_selector.cpp | 33 +- editor/themes/editor_theme_manager.cpp | 22 + 17 files changed, 819 insertions(+), 380 deletions(-) diff --git a/editor/connections_dialog.cpp b/editor/connections_dialog.cpp index de5f9ecf8971..f57e9cb5f842 100644 --- a/editor/connections_dialog.cpp +++ b/editor/connections_dialog.cpp @@ -47,6 +47,7 @@ #include "scene/gui/check_box.h" #include "scene/gui/label.h" #include "scene/gui/line_edit.h" +#include "scene/gui/margin_container.h" #include "scene/gui/option_button.h" #include "scene/gui/popup_menu.h" #include "scene/gui/spin_box.h" @@ -872,7 +873,13 @@ ConnectDialog::~ConnectDialog() { Control *ConnectionsDockTree::make_custom_tooltip(const String &p_text) const { // If it's not a doc tooltip, fallback to the default one. - return p_text.contains("::") ? nullptr : memnew(EditorHelpTooltip(p_text)); + if (p_text.contains("::")) { + return nullptr; + } + + EditorHelpBit *help_bit = memnew(EditorHelpBit(p_text)); + EditorHelpBitTooltip::show_tooltip(help_bit, const_cast(this)); + return memnew(Control); // Make the standard tooltip invisible. } struct _ConnectionsDockMethodInfoSort { @@ -1458,8 +1465,8 @@ void ConnectionsDock::update_tree() { section_item = tree->create_item(root); section_item->set_text(0, class_name); - // `|` separators used in `EditorHelpTooltip` for formatting. - section_item->set_tooltip_text(0, "class|" + doc_class_name + "||"); + // `|` separators used in `EditorHelpBit`. + section_item->set_tooltip_text(0, "class|" + doc_class_name + "|"); section_item->set_icon(0, class_icon); section_item->set_selectable(0, false); section_item->set_editable(0, false); @@ -1490,8 +1497,8 @@ void ConnectionsDock::update_tree() { sinfo["args"] = argnames; signal_item->set_metadata(0, sinfo); signal_item->set_icon(0, get_editor_theme_icon(SNAME("Signal"))); - // `|` separators used in `EditorHelpTooltip` for formatting. - signal_item->set_tooltip_text(0, "signal|" + doc_class_name + "|" + String(signal_name) + "|" + signame.trim_prefix(mi.name)); + // `|` separators used in `EditorHelpBit`. + signal_item->set_tooltip_text(0, "signal|" + doc_class_name + "|" + String(signal_name)); // List existing connections. List existing_connections; diff --git a/editor/connections_dialog.h b/editor/connections_dialog.h index f644d3335a98..78f1b76e2353 100644 --- a/editor/connections_dialog.h +++ b/editor/connections_dialog.h @@ -191,7 +191,7 @@ public: ////////////////////////////////////////// -// Custom `Tree` needed to use `EditorHelpTooltip` to display signal documentation. +// Custom `Tree` needed to use `EditorHelpBit` to display signal documentation. class ConnectionsDockTree : public Tree { virtual Control *make_custom_tooltip(const String &p_text) const; }; diff --git a/editor/create_dialog.cpp b/editor/create_dialog.cpp index f7914d3aaac0..b00f059b36ca 100644 --- a/editor/create_dialog.cpp +++ b/editor/create_dialog.cpp @@ -202,8 +202,7 @@ void CreateDialog::_update_search() { select_type(_top_result(candidates, search_text)); } else { favorite->set_disabled(true); - help_bit->set_text(vformat(TTR("No results for \"%s\"."), search_text)); - help_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5)); + help_bit->set_custom_text(String(), String(), vformat(TTR("No results for \"%s\"."), search_text.replace("[", "[lb]"))); get_ok_button()->set_disabled(true); search_options->deselect_all(); } @@ -502,17 +501,7 @@ void CreateDialog::select_type(const String &p_type, bool p_center_on_item) { to_select->select(0); search_options->scroll_to_item(to_select, p_center_on_item); - String text = help_bit->get_class_description(p_type); - if (!text.is_empty()) { - // Display both class name and description, since the help bit may be displayed - // far away from the location (especially if the dialog was resized to be taller). - help_bit->set_text(vformat("[b]%s[/b]: %s", p_type, text)); - help_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); - } else { - // Use nested `vformat()` as translators shouldn't interfere with BBCode tags. - help_bit->set_text(vformat(TTR("No description available for %s."), vformat("[b]%s[/b]", p_type))); - help_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5)); - } + help_bit->parse_symbol("class|" + p_type + "|"); favorite->set_disabled(false); favorite->set_pressed(favorite_list.has(p_type)); @@ -837,6 +826,7 @@ CreateDialog::CreateDialog() { vbc->add_margin_child(TTR("Matches:"), search_options, true); help_bit = memnew(EditorHelpBit); + help_bit->set_content_height_limits(64 * EDSCALE, 64 * EDSCALE); help_bit->connect("request_hide", callable_mp(this, &CreateDialog::_hide_requested)); vbc->add_margin_child(TTR("Description:"), help_bit); diff --git a/editor/editor_build_profile.cpp b/editor/editor_build_profile.cpp index c6ecaba230ce..c1db674cbed0 100644 --- a/editor/editor_build_profile.cpp +++ b/editor/editor_build_profile.cpp @@ -593,6 +593,10 @@ void EditorBuildProfileManager::_action_confirm() { } } +void EditorBuildProfileManager::_hide_requested() { + _cancel_pressed(); // From AcceptDialog. +} + void EditorBuildProfileManager::_fill_classes_from(TreeItem *p_parent, const String &p_class, const String &p_selected) { TreeItem *class_item = class_list->create_item(p_parent); class_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); @@ -646,21 +650,10 @@ void EditorBuildProfileManager::_class_list_item_selected() { Variant md = item->get_metadata(0); if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) { - String text = description_bit->get_class_description(md); - if (!text.is_empty()) { - // Display both class name and description, since the help bit may be displayed - // far away from the location (especially if the dialog was resized to be taller). - description_bit->set_text(vformat("[b]%s[/b]: %s", md, text)); - description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); - } else { - // Use nested `vformat()` as translators shouldn't interfere with BBCode tags. - description_bit->set_text(vformat(TTR("No description available for %s."), vformat("[b]%s[/b]", md))); - description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5)); - } + description_bit->parse_symbol("class|" + md.operator String() + "|"); } else if (md.get_type() == Variant::INT) { String build_option_description = EditorBuildProfile::get_build_option_description(EditorBuildProfile::BuildOption((int)md)); - description_bit->set_text(vformat("[b]%s[/b]: %s", TTR(item->get_text(0)), TTRGET(build_option_description))); - description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); + description_bit->set_custom_text(TTR(item->get_text(0)), String(), TTRGET(build_option_description)); } } @@ -864,7 +857,8 @@ EditorBuildProfileManager::EditorBuildProfileManager() { main_vbc->add_margin_child(TTR("Configure Engine Compilation Profile:"), class_list, true); description_bit = memnew(EditorHelpBit); - description_bit->set_custom_minimum_size(Size2(0, 80) * EDSCALE); + description_bit->set_content_height_limits(80 * EDSCALE, 80 * EDSCALE); + description_bit->connect("request_hide", callable_mp(this, &EditorBuildProfileManager::_hide_requested)); main_vbc->add_margin_child(TTR("Description:"), description_bit, false); confirm_dialog = memnew(ConfirmationDialog); diff --git a/editor/editor_build_profile.h b/editor/editor_build_profile.h index 649784afc3a8..a947365c7fe4 100644 --- a/editor/editor_build_profile.h +++ b/editor/editor_build_profile.h @@ -150,6 +150,7 @@ class EditorBuildProfileManager : public AcceptDialog { void _profile_action(int p_action); void _action_confirm(); + void _hide_requested(); void _update_edited_profile(); void _fill_classes_from(TreeItem *p_parent, const String &p_class, const String &p_selected); diff --git a/editor/editor_feature_profile.cpp b/editor/editor_feature_profile.cpp index 541bcd5e0280..86b7b3eb2f1f 100644 --- a/editor/editor_feature_profile.cpp +++ b/editor/editor_feature_profile.cpp @@ -493,6 +493,10 @@ void EditorFeatureProfileManager::_profile_selected(int p_what) { _update_selected_profile(); } +void EditorFeatureProfileManager::_hide_requested() { + _cancel_pressed(); // From AcceptDialog. +} + void EditorFeatureProfileManager::_fill_classes_from(TreeItem *p_parent, const String &p_class, const String &p_selected) { TreeItem *class_item = class_list->create_item(p_parent); class_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); @@ -555,22 +559,10 @@ void EditorFeatureProfileManager::_class_list_item_selected() { Variant md = item->get_metadata(0); if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) { - String text = description_bit->get_class_description(md); - if (!text.is_empty()) { - // Display both class name and description, since the help bit may be displayed - // far away from the location (especially if the dialog was resized to be taller). - description_bit->set_text(vformat("[b]%s[/b]: %s", md, text)); - description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); - } else { - // Use nested `vformat()` as translators shouldn't interfere with BBCode tags. - description_bit->set_text(vformat(TTR("No description available for %s."), vformat("[b]%s[/b]", md))); - description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5)); - } + description_bit->parse_symbol("class|" + md.operator String() + "|"); } else if (md.get_type() == Variant::INT) { String feature_description = EditorFeatureProfile::get_feature_description(EditorFeatureProfile::Feature((int)md)); - description_bit->set_text(vformat("[b]%s[/b]: %s", TTR(item->get_text(0)), TTRGET(feature_description))); - description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); - + description_bit->set_custom_text(TTR(item->get_text(0)), String(), TTRGET(feature_description)); return; } else { return; @@ -991,8 +983,9 @@ EditorFeatureProfileManager::EditorFeatureProfileManager() { property_list_vbc->set_h_size_flags(Control::SIZE_EXPAND_FILL); description_bit = memnew(EditorHelpBit); + description_bit->set_content_height_limits(80 * EDSCALE, 80 * EDSCALE); + description_bit->connect("request_hide", callable_mp(this, &EditorFeatureProfileManager::_hide_requested)); property_list_vbc->add_margin_child(TTR("Description:"), description_bit, false); - description_bit->set_custom_minimum_size(Size2(0, 80) * EDSCALE); property_list = memnew(Tree); property_list_vbc->add_margin_child(TTR("Extra Options:"), property_list, true); diff --git a/editor/editor_feature_profile.h b/editor/editor_feature_profile.h index 25ee1c9ba4b5..2fa6ae98135c 100644 --- a/editor/editor_feature_profile.h +++ b/editor/editor_feature_profile.h @@ -142,6 +142,7 @@ class EditorFeatureProfileManager : public AcceptDialog { void _profile_action(int p_action); void _profile_selected(int p_what); + void _hide_requested(); String current_profile; void _update_profile_list(const String &p_select_profile = String()); diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp index 1f7505633ba3..5cc09b710414 100644 --- a/editor/editor_help.cpp +++ b/editor/editor_help.cpp @@ -30,6 +30,7 @@ #include "editor_help.h" +#include "core/config/project_settings.h" #include "core/core_constants.h" #include "core/extension/gdextension.h" #include "core/input/input.h" @@ -193,7 +194,6 @@ void EditorHelp::_update_theme_item_cache() { class_desc->add_theme_font_override("normal_font", theme_cache.doc_font); class_desc->add_theme_font_size_override("normal_font_size", theme_cache.doc_font_size); - class_desc->add_theme_color_override("selection_color", get_theme_color(SNAME("selection_color"), SNAME("EditorHelp"))); class_desc->add_theme_constant_override("line_separation", get_theme_constant(SNAME("line_separation"), SNAME("EditorHelp"))); class_desc->add_theme_constant_override("table_h_separation", get_theme_constant(SNAME("table_h_separation"), SNAME("EditorHelp"))); class_desc->add_theme_constant_override("table_v_separation", get_theme_constant(SNAME("table_v_separation"), SNAME("EditorHelp"))); @@ -222,29 +222,35 @@ void EditorHelp::_class_list_select(const String &p_select) { } void EditorHelp::_class_desc_select(const String &p_select) { - if (p_select.begins_with("$")) { // enum - String select = p_select.substr(1, p_select.length()); - String class_name; - int rfind = select.rfind("."); - if (rfind != -1) { - class_name = select.substr(0, rfind); - select = select.substr(rfind + 1); - } else { - class_name = "@GlobalScope"; - } - 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(' '); + if (p_select.begins_with("$")) { // Enum. + const String link = p_select.substr(1); - String tag = p_select.substr(1, tag_end - 1); - String link = p_select.substr(tag_end + 1, p_select.length()).lstrip(" "); + 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 = edited_class; + enum_name = link; + } + } + + emit_signal(SNAME("go_to_help"), "class_enum:" + enum_class_name + ":" + enum_name); + } else if (p_select.begins_with("#")) { // Class. + emit_signal(SNAME("go_to_help"), "class_name:" + p_select.substr(1)); + } else if (p_select.begins_with("@")) { // Member. + const int tag_end = p_select.find_char(' '); + const String tag = p_select.substr(1, tag_end - 1); + const String link = p_select.substr(tag_end + 1).lstrip(" "); String topic; - HashMap *table = nullptr; + const HashMap *table = nullptr; if (tag == "method") { topic = "class_method"; @@ -311,14 +317,14 @@ void EditorHelp::_class_desc_select(const String &p_select) { } if (link.contains(".")) { - int class_end = link.find_char('.'); - emit_signal(SNAME("go_to_help"), topic + ":" + link.substr(0, class_end) + ":" + link.substr(class_end + 1, link.length())); + const int class_end = link.find_char('.'); + emit_signal(SNAME("go_to_help"), topic + ":" + link.left(class_end) + ":" + link.substr(class_end + 1)); } } - } else if (p_select.begins_with("http")) { + } else if (p_select.begins_with("http:") || p_select.begins_with("https:")) { OS::get_singleton()->shell_open(p_select); - } else if (p_select.begins_with("^")) { - DisplayServer::get_singleton()->clipboard_set(p_select.trim_prefix("^")); + } else if (p_select.begins_with("^")) { // Copy button. + DisplayServer::get_singleton()->clipboard_set(p_select.substr(1)); } } @@ -341,13 +347,15 @@ void EditorHelp::_class_desc_resized(bool p_force_update_theme) { } } -void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is_bitfield) { +static void _add_type_to_rt(const String &p_type, const String &p_enum, bool p_is_bitfield, RichTextLabel *p_rt, const Control *p_owner_node, const String &p_class) { + const Color type_color = p_owner_node->get_theme_color(SNAME("type_color"), SNAME("EditorHelp")); + if (p_type.is_empty() || p_type == "void") { - class_desc->push_color(Color(theme_cache.type_color, 0.5)); - class_desc->push_hint(TTR("No return value.")); - class_desc->add_text("void"); - class_desc->pop(); // hint - class_desc->pop(); // color + p_rt->push_color(Color(type_color, 0.5)); + p_rt->push_hint(TTR("No return value.")); + p_rt->add_text("void"); + p_rt->pop(); // hint + p_rt->pop(); // color return; } @@ -359,12 +367,12 @@ void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is String display_t; // For display purposes. if (is_enum_type) { link_t = p_enum; // The link for enums is always the full enum description - display_t = _contextualize_class_specifier(p_enum, edited_class); + display_t = _contextualize_class_specifier(p_enum, p_class); } else { - display_t = _contextualize_class_specifier(p_type, edited_class); + display_t = _contextualize_class_specifier(p_type, p_class); } - class_desc->push_color(theme_cache.type_color); + p_rt->push_color(type_color); bool add_array = false; if (can_ref) { if (link_t.ends_with("[]")) { @@ -372,37 +380,41 @@ void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is link_t = link_t.trim_suffix("[]"); display_t = display_t.trim_suffix("[]"); - class_desc->push_meta("#Array", RichTextLabel::META_UNDERLINE_ON_HOVER); // class - class_desc->add_text("Array"); - class_desc->pop(); // meta - class_desc->add_text("["); + p_rt->push_meta("#Array", RichTextLabel::META_UNDERLINE_ON_HOVER); // class + p_rt->add_text("Array"); + p_rt->pop(); // meta + p_rt->add_text("["); } else if (is_bitfield) { - class_desc->push_color(Color(theme_cache.type_color, 0.5)); - class_desc->push_hint(TTR("This value is an integer composed as a bitmask of the following flags.")); - class_desc->add_text("BitField"); - class_desc->pop(); // hint - class_desc->add_text("["); - class_desc->pop(); // color + p_rt->push_color(Color(type_color, 0.5)); + p_rt->push_hint(TTR("This value is an integer composed as a bitmask of the following flags.")); + p_rt->add_text("BitField"); + p_rt->pop(); // hint + p_rt->add_text("["); + p_rt->pop(); // color } if (is_enum_type) { - class_desc->push_meta("$" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // enum + p_rt->push_meta("$" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // enum } else { - class_desc->push_meta("#" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // class + p_rt->push_meta("#" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // class } } - class_desc->add_text(display_t); + p_rt->add_text(display_t); if (can_ref) { - class_desc->pop(); // meta + p_rt->pop(); // meta if (add_array) { - class_desc->add_text("]"); + p_rt->add_text("]"); } else if (is_bitfield) { - class_desc->push_color(Color(theme_cache.type_color, 0.5)); - class_desc->add_text("]"); - class_desc->pop(); // color + p_rt->push_color(Color(type_color, 0.5)); + p_rt->add_text("]"); + p_rt->pop(); // color } } - class_desc->pop(); // color + p_rt->pop(); // color +} + +void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is_bitfield) { + _add_type_to_rt(p_type, p_enum, p_is_bitfield, class_desc, this, edited_class); } void EditorHelp::_add_type_icon(const String &p_type, int p_size, const String &p_fallback) { @@ -717,10 +729,10 @@ void EditorHelp::_update_method_list(MethodType p_method_type, const Vector 0; group_prefix = new_prefix; } else if (!group_prefix.is_empty() && new_prefix != group_prefix) { @@ -748,7 +760,7 @@ void EditorHelp::_update_method_list(MethodType p_method_type, const Vector &p_methods) { -#define DTR_DOC(m_string) (p_classdoc.is_script_doc ? (m_string) : DTR(m_string)) +#define HANDLE_DOC(m_string) ((p_classdoc.is_script_doc ? (m_string) : DTR(m_string)).strip_edges()) class_desc->add_newline(); class_desc->add_newline(); @@ -807,7 +819,7 @@ void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc TTRC("This constructor may be changed or removed in future versions."), TTRC("This operator may be changed or removed in future versions."), }; - DEPRECATED_DOC_MSG(DTR_DOC(method.deprecated_message), TTRGET(messages_by_type[p_method_type])); + DEPRECATED_DOC_MSG(HANDLE_DOC(method.deprecated_message), TTRGET(messages_by_type[p_method_type])); } if (method.is_experimental) { @@ -822,7 +834,7 @@ void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc TTRC("This constructor may be changed or removed in future versions."), TTRC("This operator may be changed or removed in future versions."), }; - EXPERIMENTAL_DOC_MSG(DTR_DOC(method.experimental_message), TTRGET(messages_by_type[p_method_type])); + EXPERIMENTAL_DOC_MSG(HANDLE_DOC(method.experimental_message), TTRGET(messages_by_type[p_method_type])); } if (!method.errors_returned.is_empty()) { @@ -856,7 +868,7 @@ void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc class_desc->pop(); // list } - const String descr = DTR_DOC(method.description).strip_edges(); + const String descr = HANDLE_DOC(method.description); const bool is_documented = method.is_deprecated || method.is_experimental || !descr.is_empty(); if (!descr.is_empty()) { if (has_prev_text) { @@ -903,7 +915,7 @@ void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc } } -#undef DTR_DOC +#undef HANDLE_DOC } void EditorHelp::_update_doc() { @@ -922,7 +934,7 @@ void EditorHelp::_update_doc() { DocData::ClassDoc cd = doc->class_list[edited_class]; // Make a copy, so we can sort without worrying. -#define DTR_DOC(m_string) (cd.is_script_doc ? (m_string) : DTR(m_string)) +#define HANDLE_DOC(m_string) ((cd.is_script_doc ? (m_string) : DTR(m_string)).strip_edges()) // Class name @@ -940,12 +952,12 @@ void EditorHelp::_update_doc() { if (cd.is_deprecated) { class_desc->add_newline(); - DEPRECATED_DOC_MSG(DTR_DOC(cd.deprecated_message), TTR("This class may be changed or removed in future versions.")); + DEPRECATED_DOC_MSG(HANDLE_DOC(cd.deprecated_message), TTR("This class may be changed or removed in future versions.")); } if (cd.is_experimental) { class_desc->add_newline(); - EXPERIMENTAL_DOC_MSG(DTR_DOC(cd.experimental_message), TTR("This class may be changed or removed in future versions.")); + EXPERIMENTAL_DOC_MSG(HANDLE_DOC(cd.experimental_message), TTR("This class may be changed or removed in future versions.")); } // Inheritance tree @@ -1003,7 +1015,7 @@ void EditorHelp::_update_doc() { bool has_description = false; // Brief description - const String brief_class_descr = DTR_DOC(cd.brief_description).strip_edges(); + const String brief_class_descr = HANDLE_DOC(cd.brief_description); if (!brief_class_descr.is_empty()) { has_description = true; @@ -1022,7 +1034,7 @@ void EditorHelp::_update_doc() { } // Class description - const String class_descr = DTR_DOC(cd.description).strip_edges(); + const String class_descr = HANDLE_DOC(cd.description); if (!class_descr.is_empty()) { has_description = true; @@ -1106,9 +1118,9 @@ void EditorHelp::_update_doc() { class_desc->push_color(theme_cache.symbol_color); for (const DocData::TutorialDoc &tutorial : cd.tutorials) { - const String link = DTR_DOC(tutorial.link).strip_edges(); + const String link = HANDLE_DOC(tutorial.link); - String link_text = DTR_DOC(tutorial.title).strip_edges(); + String link_text = HANDLE_DOC(tutorial.title); if (link_text.is_empty()) { const int sep_pos = link.find("//"); if (sep_pos >= 0) { @@ -1441,7 +1453,7 @@ void EditorHelp::_update_doc() { _push_normal_font(); class_desc->push_color(theme_cache.comment_color); - const String descr = DTR_DOC(theme_item.description).strip_edges(); + const String descr = HANDLE_DOC(theme_item.description); if (!descr.is_empty()) { _add_text(descr); } else { @@ -1538,13 +1550,13 @@ void EditorHelp::_update_doc() { _push_normal_font(); class_desc->push_color(theme_cache.comment_color); - const String descr = DTR_DOC(signal.description).strip_edges(); + const String descr = HANDLE_DOC(signal.description); const bool is_multiline = descr.find_char('\n') > 0; bool has_prev_text = false; if (signal.is_deprecated) { has_prev_text = true; - DEPRECATED_DOC_MSG(DTR_DOC(signal.deprecated_message), TTR("This signal may be changed or removed in future versions.")); + DEPRECATED_DOC_MSG(HANDLE_DOC(signal.deprecated_message), TTR("This signal may be changed or removed in future versions.")); } if (signal.is_experimental) { @@ -1555,7 +1567,7 @@ void EditorHelp::_update_doc() { } } has_prev_text = true; - EXPERIMENTAL_DOC_MSG(DTR_DOC(signal.experimental_message), TTR("This signal may be changed or removed in future versions.")); + EXPERIMENTAL_DOC_MSG(HANDLE_DOC(signal.experimental_message), TTR("This signal may be changed or removed in future versions.")); } if (!descr.is_empty()) { @@ -1669,7 +1681,7 @@ void EditorHelp::_update_doc() { // Enum description. if (key != "@unnamed_enums" && cd.enums.has(key)) { - const String descr = DTR_DOC(cd.enums[key].description).strip_edges(); + const String descr = HANDLE_DOC(cd.enums[key].description); const bool is_multiline = descr.find_char('\n') > 0; if (cd.enums[key].is_deprecated || cd.enums[key].is_experimental || !descr.is_empty()) { class_desc->add_newline(); @@ -1682,7 +1694,7 @@ void EditorHelp::_update_doc() { if (cd.enums[key].is_deprecated) { has_prev_text = true; - DEPRECATED_DOC_MSG(DTR_DOC(cd.enums[key].deprecated_message), TTR("This enumeration may be changed or removed in future versions.")); + DEPRECATED_DOC_MSG(HANDLE_DOC(cd.enums[key].deprecated_message), TTR("This enumeration may be changed or removed in future versions.")); } if (cd.enums[key].is_experimental) { @@ -1693,7 +1705,7 @@ void EditorHelp::_update_doc() { } } has_prev_text = true; - EXPERIMENTAL_DOC_MSG(DTR_DOC(cd.enums[key].experimental_message), TTR("This enumeration may be changed or removed in future versions.")); + EXPERIMENTAL_DOC_MSG(HANDLE_DOC(cd.enums[key].experimental_message), TTR("This enumeration may be changed or removed in future versions.")); } if (!descr.is_empty()) { @@ -1718,7 +1730,7 @@ void EditorHelp::_update_doc() { bool prev_is_multiline = true; // Use a large margin for the first item. for (const DocData::ConstantDoc &enum_value : E.value) { - const String descr = DTR_DOC(enum_value.description).strip_edges(); + const String descr = HANDLE_DOC(enum_value.description); const bool is_multiline = descr.find_char('\n') > 0; class_desc->add_newline(); @@ -1766,7 +1778,7 @@ void EditorHelp::_update_doc() { if (enum_value.is_deprecated) { has_prev_text = true; - DEPRECATED_DOC_MSG(DTR_DOC(enum_value.deprecated_message), TTR("This constant may be changed or removed in future versions.")); + DEPRECATED_DOC_MSG(HANDLE_DOC(enum_value.deprecated_message), TTR("This constant may be changed or removed in future versions.")); } if (enum_value.is_experimental) { @@ -1777,7 +1789,7 @@ void EditorHelp::_update_doc() { } } has_prev_text = true; - EXPERIMENTAL_DOC_MSG(DTR_DOC(enum_value.experimental_message), TTR("This constant may be changed or removed in future versions.")); + EXPERIMENTAL_DOC_MSG(HANDLE_DOC(enum_value.experimental_message), TTR("This constant may be changed or removed in future versions.")); } if (!descr.is_empty()) { @@ -1817,7 +1829,7 @@ void EditorHelp::_update_doc() { bool prev_is_multiline = true; // Use a large margin for the first item. for (const DocData::ConstantDoc &constant : constants) { - const String descr = DTR_DOC(constant.description).strip_edges(); + const String descr = HANDLE_DOC(constant.description); const bool is_multiline = descr.find_char('\n') > 0; class_desc->add_newline(); @@ -1871,7 +1883,7 @@ void EditorHelp::_update_doc() { if (constant.is_deprecated) { has_prev_text = true; - DEPRECATED_DOC_MSG(DTR_DOC(constant.deprecated_message), TTR("This constant may be changed or removed in future versions.")); + DEPRECATED_DOC_MSG(HANDLE_DOC(constant.deprecated_message), TTR("This constant may be changed or removed in future versions.")); } if (constant.is_experimental) { @@ -1882,7 +1894,7 @@ void EditorHelp::_update_doc() { } } has_prev_text = true; - EXPERIMENTAL_DOC_MSG(DTR_DOC(constant.experimental_message), TTR("This constant may be changed or removed in future versions.")); + EXPERIMENTAL_DOC_MSG(HANDLE_DOC(constant.experimental_message), TTR("This constant may be changed or removed in future versions.")); } if (!descr.is_empty()) { @@ -2000,7 +2012,7 @@ void EditorHelp::_update_doc() { _push_normal_font(); class_desc->push_color(theme_cache.comment_color); - const String descr = DTR_DOC(annotation.description).strip_edges(); + const String descr = HANDLE_DOC(annotation.description); if (!descr.is_empty()) { _add_text(descr); } else { @@ -2185,7 +2197,7 @@ void EditorHelp::_update_doc() { if (prop.is_deprecated) { has_prev_text = true; - DEPRECATED_DOC_MSG(DTR_DOC(prop.deprecated_message), TTR("This property may be changed or removed in future versions.")); + DEPRECATED_DOC_MSG(HANDLE_DOC(prop.deprecated_message), TTR("This property may be changed or removed in future versions.")); } if (prop.is_experimental) { @@ -2194,10 +2206,10 @@ void EditorHelp::_update_doc() { class_desc->add_newline(); } has_prev_text = true; - EXPERIMENTAL_DOC_MSG(DTR_DOC(prop.experimental_message), TTR("This property may be changed or removed in future versions.")); + EXPERIMENTAL_DOC_MSG(HANDLE_DOC(prop.experimental_message), TTR("This property may be changed or removed in future versions.")); } - const String descr = DTR_DOC(prop.description).strip_edges(); + const String descr = HANDLE_DOC(prop.description); if (!descr.is_empty()) { if (has_prev_text) { class_desc->add_newline(); @@ -2251,7 +2263,7 @@ void EditorHelp::_update_doc() { // Free the scroll. scroll_locked = false; -#undef DTR_DOC +#undef HANDLE_DOC } void EditorHelp::_request_help(const String &p_string) { @@ -2331,7 +2343,7 @@ void EditorHelp::_help_callback(const String &p_topic) { } } -static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control *p_owner_node, const String &p_class) { +static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, const Control *p_owner_node, const String &p_class) { const DocTools *doc = EditorHelp::get_doc_data(); bool is_native = false; @@ -2452,7 +2464,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control const String tag = bbcode.substr(brk_pos + 1, brk_end - brk_pos - 1); if (tag.begins_with("/")) { - bool tag_ok = tag_stack.size() && tag_stack.front()->get() == tag.substr(1, tag.length()); + bool tag_ok = tag_stack.size() && tag_stack.front()->get() == tag.substr(1); if (!tag_ok) { p_rt->add_text("["); @@ -2467,8 +2479,8 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control } } else if (tag.begins_with("method ") || tag.begins_with("constructor ") || tag.begins_with("operator ") || tag.begins_with("member ") || tag.begins_with("signal ") || tag.begins_with("enum ") || tag.begins_with("constant ") || tag.begins_with("annotation ") || tag.begins_with("theme_item ")) { const int tag_end = tag.find_char(' '); - const String link_tag = tag.substr(0, tag_end); - const String link_target = tag.substr(tag_end + 1, tag.length()).lstrip(" "); + const String link_tag = tag.left(tag_end); + const String link_target = tag.substr(tag_end + 1).lstrip(" "); Color target_color = link_color; RichTextLabel::MetaUnderline underline_mode = RichTextLabel::META_UNDERLINE_ON_HOVER; @@ -2520,7 +2532,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control pos = brk_end + 1; } else if (tag.begins_with("param ")) { const int tag_end = tag.find_char(' '); - const String param_name = tag.substr(tag_end + 1, tag.length()).lstrip(" "); + const String param_name = tag.substr(tag_end + 1).lstrip(" "); // Use monospace font with translucent background color to make code easier to distinguish from other text. p_rt->push_font(doc_code_font); @@ -2741,7 +2753,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag.begins_with("url=")) { - String url = tag.substr(4, tag.length()); + String url = tag.substr(4); p_rt->push_meta(url); pos = brk_end + 1; @@ -2751,13 +2763,13 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control int height = 0; bool size_in_percent = false; if (tag.length() > 4) { - Vector subtags = tag.substr(4, tag.length()).split(" "); + Vector subtags = tag.substr(4).split(" "); HashMap bbcode_options; for (int i = 0; i < subtags.size(); i++) { const String &expr = subtags[i]; int value_pos = expr.find_char('='); if (value_pos > -1) { - bbcode_options[expr.substr(0, value_pos)] = expr.substr(value_pos + 1).unquote(); + bbcode_options[expr.left(value_pos)] = expr.substr(value_pos + 1).unquote(); } } HashMap::Iterator width_option = bbcode_options.find("width"); @@ -2787,14 +2799,14 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control pos = end; tag_stack.push_front("img"); } else if (tag.begins_with("color=")) { - String col = tag.substr(6, tag.length()); + String col = tag.substr(6); Color color = Color::from_string(col, Color()); p_rt->push_color(color); pos = brk_end + 1; tag_stack.push_front("color"); } else if (tag.begins_with("font=")) { - String font_path = tag.substr(5, tag.length()); + String font_path = tag.substr(5); Ref font = ResourceLoader::load(font_path, "Font"); if (font.is_valid()) { p_rt->push_font(font); @@ -3120,68 +3132,50 @@ DocTools *EditorHelp::get_doc_data() { /// EditorHelpBit /// -void EditorHelpBit::_go_to_help(const String &p_what) { - EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT); - ScriptEditor::get_singleton()->goto_help(p_what); - emit_signal(SNAME("request_hide")); -} +#define HANDLE_DOC(m_string) ((is_native ? DTR(m_string) : (m_string)).strip_edges()) -void EditorHelpBit::_meta_clicked(const String &p_select) { - if (p_select.begins_with("$")) { // enum - String select = p_select.substr(1, p_select.length()); - String class_name; - int rfind = select.rfind("."); - if (rfind != -1) { - class_name = select.substr(0, rfind); - select = select.substr(rfind + 1); - } else { - class_name = "@GlobalScope"; - } - _go_to_help("class_enum:" + class_name + ":" + select); - return; - } else if (p_select.begins_with("#")) { - _go_to_help("class_name:" + p_select.substr(1, p_select.length())); - return; - } else if (p_select.begins_with("@")) { - String m = p_select.substr(1, p_select.length()); - - if (m.contains(".")) { - _go_to_help("class_method:" + m.get_slice(".", 0) + ":" + m.get_slice(".", 0)); // Must go somewhere else. - } - } -} - -String EditorHelpBit::get_class_description(const StringName &p_class_name) const { +EditorHelpBit::HelpData EditorHelpBit::_get_class_help_data(const StringName &p_class_name) { if (doc_class_cache.has(p_class_name)) { return doc_class_cache[p_class_name]; } - String description; + HelpData result; const HashMap::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); if (E) { // Non-native class shouldn't be cached, nor translated. const bool is_native = !E->value.is_script_doc; - description = is_native ? DTR(E->value.brief_description) : E->value.brief_description; + + result.description = HANDLE_DOC(E->value.brief_description); + if (E->value.is_deprecated) { + if (E->value.deprecated_message.is_empty()) { + result.deprecated_message = TTR("This class may be changed or removed in future versions."); + } else { + result.deprecated_message = HANDLE_DOC(E->value.deprecated_message); + } + } + if (E->value.is_experimental) { + if (E->value.experimental_message.is_empty()) { + result.experimental_message = TTR("This class may be changed or removed in future versions."); + } else { + result.experimental_message = HANDLE_DOC(E->value.experimental_message); + } + } if (is_native) { - doc_class_cache[p_class_name] = description; + doc_class_cache[p_class_name] = result; } } - return description; + return result; } -String EditorHelpBit::get_property_description(const StringName &p_class_name, const StringName &p_property_name) const { - if (!custom_description.is_empty()) { - return custom_description; - } - +EditorHelpBit::HelpData EditorHelpBit::_get_property_help_data(const StringName &p_class_name, const StringName &p_property_name) { if (doc_property_cache.has(p_class_name) && doc_property_cache[p_class_name].has(p_property_name)) { return doc_property_cache[p_class_name][p_property_name]; } - String description; + HelpData result; const DocTools *dd = EditorHelp::get_doc_data(); const HashMap::ConstIterator E = dd->class_list.find(p_class_name); @@ -3190,7 +3184,22 @@ String EditorHelpBit::get_property_description(const StringName &p_class_name, c const bool is_native = !E->value.is_script_doc; for (const DocData::PropertyDoc &property : E->value.properties) { - String description_current = is_native ? DTR(property.description) : property.description; + HelpData current; + current.description = HANDLE_DOC(property.description); + if (property.is_deprecated) { + if (property.deprecated_message.is_empty()) { + current.deprecated_message = TTR("This property may be changed or removed in future versions."); + } else { + current.deprecated_message = HANDLE_DOC(property.deprecated_message); + } + } + if (property.is_experimental) { + if (property.experimental_message.is_empty()) { + current.experimental_message = TTR("This property may be changed or removed in future versions."); + } else { + current.experimental_message = HANDLE_DOC(property.experimental_message); + } + } String enum_class_name; String enum_name; @@ -3215,18 +3224,19 @@ String EditorHelpBit::get_property_description(const StringName &p_class_name, c if (constant.enumeration == enum_name && !constant.name.ends_with("_MAX")) { // Prettify the enum value display, so that "_" becomes "Item". const String item_name = EditorPropertyNameProcessor::get_singleton()->process_name(constant.name, EditorPropertyNameProcessor::STYLE_CAPITALIZED).trim_prefix(enum_prefix); - String item_descr = (is_native ? DTR(constant.description) : constant.description).strip_edges(); + String item_descr = HANDLE_DOC(constant.description); if (item_descr.is_empty()) { - item_descr = ("[i]" + DTR("No description available.") + "[/i]"); + item_descr = "[color=][i]" + TTR("No description available.") + "[/i][/color]"; } - description_current += vformat("\n[b]%s:[/b] %s", item_name, item_descr); + current.description += vformat("\n[b]%s:[/b] %s", item_name, item_descr); } } + current.description = current.description.lstrip("\n"); } } if (property.name == p_property_name) { - description = description_current; + result = current; if (!is_native) { break; @@ -3234,20 +3244,20 @@ String EditorHelpBit::get_property_description(const StringName &p_class_name, c } if (is_native) { - doc_property_cache[p_class_name][property.name] = description_current; + doc_property_cache[p_class_name][property.name] = current; } } } - return description; + return result; } -String EditorHelpBit::get_method_description(const StringName &p_class_name, const StringName &p_method_name) const { +EditorHelpBit::HelpData EditorHelpBit::_get_method_help_data(const StringName &p_class_name, const StringName &p_method_name) { if (doc_method_cache.has(p_class_name) && doc_method_cache[p_class_name].has(p_method_name)) { return doc_method_cache[p_class_name][p_method_name]; } - String description; + HelpData result; const HashMap::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); if (E) { @@ -3255,10 +3265,30 @@ String EditorHelpBit::get_method_description(const StringName &p_class_name, con const bool is_native = !E->value.is_script_doc; for (const DocData::MethodDoc &method : E->value.methods) { - String description_current = is_native ? DTR(method.description) : method.description; + HelpData current; + current.description = HANDLE_DOC(method.description); + if (method.is_deprecated) { + if (method.deprecated_message.is_empty()) { + current.deprecated_message = TTR("This method may be changed or removed in future versions."); + } else { + current.deprecated_message = HANDLE_DOC(method.deprecated_message); + } + } + if (method.is_experimental) { + if (method.experimental_message.is_empty()) { + current.experimental_message = TTR("This method may be changed or removed in future versions."); + } else { + current.experimental_message = HANDLE_DOC(method.experimental_message); + } + } + current.doc_type = { method.return_type, method.return_enum, method.return_is_bitfield }; + for (const DocData::ArgumentDoc &argument : method.arguments) { + const DocType argument_type = { argument.type, argument.enumeration, argument.is_bitfield }; + current.arguments.push_back({ argument.name, argument_type, argument.default_value }); + } if (method.name == p_method_name) { - description = description_current; + result = current; if (!is_native) { break; @@ -3266,20 +3296,20 @@ String EditorHelpBit::get_method_description(const StringName &p_class_name, con } if (is_native) { - doc_method_cache[p_class_name][method.name] = description_current; + doc_method_cache[p_class_name][method.name] = current; } } } - return description; + return result; } -String EditorHelpBit::get_signal_description(const StringName &p_class_name, const StringName &p_signal_name) const { +EditorHelpBit::HelpData EditorHelpBit::_get_signal_help_data(const StringName &p_class_name, const StringName &p_signal_name) { if (doc_signal_cache.has(p_class_name) && doc_signal_cache[p_class_name].has(p_signal_name)) { return doc_signal_cache[p_class_name][p_signal_name]; } - String description; + HelpData result; const HashMap::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); if (E) { @@ -3287,10 +3317,29 @@ String EditorHelpBit::get_signal_description(const StringName &p_class_name, con const bool is_native = !E->value.is_script_doc; for (const DocData::MethodDoc &signal : E->value.signals) { - String description_current = is_native ? DTR(signal.description) : signal.description; + HelpData current; + current.description = HANDLE_DOC(signal.description); + if (signal.is_deprecated) { + if (signal.deprecated_message.is_empty()) { + current.deprecated_message = TTR("This signal may be changed or removed in future versions."); + } else { + current.deprecated_message = HANDLE_DOC(signal.deprecated_message); + } + } + if (signal.is_experimental) { + if (signal.experimental_message.is_empty()) { + current.experimental_message = TTR("This signal may be changed or removed in future versions."); + } else { + current.experimental_message = HANDLE_DOC(signal.experimental_message); + } + } + for (const DocData::ArgumentDoc &argument : signal.arguments) { + const DocType argument_type = { argument.type, argument.enumeration, argument.is_bitfield }; + current.arguments.push_back({ argument.name, argument_type, argument.default_value }); + } if (signal.name == p_signal_name) { - description = description_current; + result = current; if (!is_native) { break; @@ -3298,20 +3347,20 @@ String EditorHelpBit::get_signal_description(const StringName &p_class_name, con } if (is_native) { - doc_signal_cache[p_class_name][signal.name] = description_current; + doc_signal_cache[p_class_name][signal.name] = current; } } } - return description; + return result; } -String EditorHelpBit::get_theme_item_description(const StringName &p_class_name, const StringName &p_theme_item_name) const { +EditorHelpBit::HelpData EditorHelpBit::_get_theme_item_help_data(const StringName &p_class_name, const StringName &p_theme_item_name) { if (doc_theme_item_cache.has(p_class_name) && doc_theme_item_cache[p_class_name].has(p_theme_item_name)) { return doc_theme_item_cache[p_class_name][p_theme_item_name]; } - String description; + HelpData result; bool found = false; const DocTools *dd = EditorHelp::get_doc_data(); @@ -3321,142 +3370,477 @@ String EditorHelpBit::get_theme_item_description(const StringName &p_class_name, const bool is_native = !E->value.is_script_doc; for (const DocData::ThemeItemDoc &theme_item : E->value.theme_properties) { - String description_current = is_native ? DTR(theme_item.description) : theme_item.description; + HelpData current; + current.description = HANDLE_DOC(theme_item.description); if (theme_item.name == p_theme_item_name) { - description = description_current; + result = current; found = true; - if (!is_native) { break; } } if (is_native) { - doc_theme_item_cache[p_class_name][theme_item.name] = description_current; + doc_theme_item_cache[p_class_name][theme_item.name] = current; } } if (found || E->value.inherits.is_empty()) { break; } + // Check for inherited theme items. E = dd->class_list.find(E->value.inherits); } - return description; + return result; +} + +#undef HANDLE_DOC + +void EditorHelpBit::_add_type_to_title(const DocType &p_doc_type) { + _add_type_to_rt(p_doc_type.type, p_doc_type.enumeration, p_doc_type.is_bitfield, title, this, symbol_class_name); +} + +void EditorHelpBit::_update_labels() { + const Ref 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 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 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("", comment_color.to_html()), content, this, symbol_class_name); + } + + if (is_inside_tree()) { + update_content_height(); + } +} + +void EditorHelpBit::_go_to_help(const String &p_what) { + EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT); + ScriptEditor::get_singleton()->goto_help(p_what); + emit_signal(SNAME("request_hide")); +} + +void EditorHelpBit::_meta_clicked(const String &p_select) { + if (p_select.begins_with("$")) { // Enum. + const String link = p_select.substr(1); + + String enum_class_name; + String enum_name; + if (CoreConstants::is_global_enum(link)) { + enum_class_name = "@GlobalScope"; + enum_name = link; + } else { + const int dot_pos = link.rfind("."); + if (dot_pos >= 0) { + enum_class_name = link.left(dot_pos); + enum_name = link.substr(dot_pos + 1); + } else { + enum_class_name = symbol_class_name; + enum_name = link; + } + } + + _go_to_help("class_enum:" + enum_class_name + ":" + enum_name); + } else if (p_select.begins_with("#")) { // Class. + _go_to_help("class_name:" + p_select.substr(1)); + } else if (p_select.begins_with("@")) { // Member. + const int tag_end = p_select.find_char(' '); + const String tag = p_select.substr(1, tag_end - 1); + const String link = p_select.substr(tag_end + 1).lstrip(" "); + + String topic; + if (tag == "method") { + topic = "class_method"; + } else if (tag == "constructor") { + topic = "class_method"; + } else if (tag == "operator") { + topic = "class_method"; + } else if (tag == "member") { + topic = "class_property"; + } else if (tag == "enum") { + topic = "class_enum"; + } else if (tag == "signal") { + topic = "class_signal"; + } else if (tag == "constant") { + topic = "class_constant"; + } else if (tag == "annotation") { + topic = "class_annotation"; + } else if (tag == "theme_item") { + topic = "class_theme_item"; + } else { + return; + } + + if (link.contains(".")) { + const int class_end = link.find_char('.'); + _go_to_help(topic + ":" + link.left(class_end) + ":" + link.substr(class_end + 1)); + } else { + _go_to_help(topic + ":" + symbol_class_name + ":" + link); + } + } else if (p_select.begins_with("http:") || p_select.begins_with("https:")) { + OS::get_singleton()->shell_open(p_select); + } else if (p_select.begins_with("^")) { // Copy button. + DisplayServer::get_singleton()->clipboard_set(p_select.substr(1)); + } } void EditorHelpBit::_bind_methods() { - ClassDB::bind_method(D_METHOD("set_text", "text"), &EditorHelpBit::set_text); ADD_SIGNAL(MethodInfo("request_hide")); } void EditorHelpBit::_notification(int p_what) { switch (p_what) { - case NOTIFICATION_THEME_CHANGED: { - rich_text->add_theme_color_override("selection_color", get_theme_color(SNAME("selection_color"), SNAME("EditorHelp"))); - rich_text->clear(); - _add_text_to_rt(text, rich_text, this, doc_class_name); - rich_text->reset_size(); // Force recalculating size after parsing bbcode. - } break; + case NOTIFICATION_THEME_CHANGED: + _update_labels(); + break; } } -void EditorHelpBit::set_text(const String &p_text) { - text = p_text; - rich_text->clear(); - _add_text_to_rt(text, rich_text, this, doc_class_name); -} +void EditorHelpBit::parse_symbol(const String &p_symbol) { + const PackedStringArray slices = p_symbol.split("|", true, 2); + ERR_FAIL_COND_MSG(slices.size() < 3, "Invalid doc id. The expected format is 'item_type|class_name|item_name'."); -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)); -} - -/// EditorHelpTooltip /// - -void EditorHelpTooltip::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_POSTINITIALIZE: { - if (!tooltip_text.is_empty()) { - parse_tooltip(tooltip_text); - } - } break; - } -} - -// `p_text` is expected to be something like these: -// - `class|Control||`; -// - `property|Control|size|`; -// - `signal|Control|gui_input|(event: InputEvent)`. -void EditorHelpTooltip::parse_tooltip(const String &p_text) { - tooltip_text = p_text; - - PackedStringArray slices = p_text.split("|", true, 3); - ERR_FAIL_COND_MSG(slices.size() < 4, "Invalid tooltip formatting. The expect string should be formatted as 'type|class|property|args'."); - - const String &type = slices[0]; + const String &item_type = slices[0]; const String &class_name = slices[1]; - const String &property_name = slices[2]; - const String &property_args = slices[3]; + const String &item_name = slices[2]; - doc_class_name = class_name; + String visible_type; + String name = item_name; - String formatted_text; - - // 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; + 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=][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'."); } - String title; - String description; + symbol_class_name = class_name; + symbol_type = item_type; + symbol_visible_type = visible_type; + symbol_name = name; - if (type == "class") { - title = class_name; - description = get_class_description(class_name); - formatted_text = TTR("Class:"); + if (help_data.description.is_empty()) { + help_data.description = "[color=][i]" + TTR("No description available.") + "[/i][/color]"; + } + + if (is_inside_tree()) { + _update_labels(); + } +} + +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(); + } +} + +void EditorHelpBit::prepend_description(const String &p_text) { + if (help_data.description.is_empty()) { + help_data.description = p_text; } else { - title = property_name; + help_data.description = p_text + "\n" + help_data.description; + } - 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 (is_inside_tree()) { + _update_labels(); + } +} + +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; + + if (is_inside_tree()) { + update_content_height(); + } +} + +void EditorHelpBit::update_content_height() { + float content_height = content->get_content_height(); + const Ref 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))); +} + +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); + } +} + +/// EditorHelpBitTooltip /// + +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; + } +} + +// Forwards non-mouse input to the parent viewport. +void EditorHelpBitTooltip::_input_from_window(const Ref &p_event) { + if (p_event->is_action_pressed(SNAME("ui_cancel"), false, true)) { + hide(); // Will be deleted on its timer. + } else { + const Ref mouse_event = p_event; + if (mouse_event.is_null()) { + get_parent_viewport()->push_input(p_event); } } - - // 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); } -EditorHelpTooltip::EditorHelpTooltip(const String &p_text, const String &p_custom_description) { - tooltip_text = p_text; - custom_description = p_custom_description; +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(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(); +} - get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 0)); +// 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 { + vr = window->get_usable_parent_rect(); + } + + 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; + } + + set_flag(Window::FLAG_NO_FOCUS, true); + popup(r); +} + +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(this), &Node::queue_free)); + add_child(timer); + + 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) diff --git a/editor/editor_help.h b/editor/editor_help.h index f8686b964a0c..078b42b439d0 100644 --- a/editor/editor_help.h +++ b/editor/editor_help.h @@ -35,9 +35,9 @@ #include "editor/code_editor.h" #include "editor/doc_tools.h" #include "editor/editor_plugin.h" -#include "scene/gui/margin_container.h" #include "scene/gui/menu_button.h" #include "scene/gui/panel_container.h" +#include "scene/gui/popup.h" #include "scene/gui/rich_text_label.h" #include "scene/gui/split_container.h" #include "scene/gui/tab_container.h" @@ -251,53 +251,91 @@ public: ~EditorHelp(); }; -class EditorHelpBit : public MarginContainer { - GDCLASS(EditorHelpBit, MarginContainer); +class EditorHelpBit : public VBoxContainer { + GDCLASS(EditorHelpBit, VBoxContainer); - inline static HashMap doc_class_cache; - inline static HashMap> doc_property_cache; - inline static HashMap> doc_method_cache; - inline static HashMap> doc_signal_cache; - inline static HashMap> doc_theme_item_cache; + struct DocType { + String type; + String enumeration; + bool is_bitfield = false; + }; - RichTextLabel *rich_text = nullptr; + 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 arguments; // For methods and signals. + }; + + inline static HashMap doc_class_cache; + inline static HashMap> doc_property_cache; + inline static HashMap> doc_method_cache; + inline static HashMap> doc_signal_cache; + inline static HashMap> doc_theme_item_cache; + + RichTextLabel *title = nullptr; + RichTextLabel *content = nullptr; + + String symbol_class_name; + String symbol_type; + String symbol_visible_type; + String symbol_name; + + HelpData help_data; + + float content_min_height = 0.0; + float content_max_height = 0.0; + + static HelpData _get_class_help_data(const StringName &p_class_name); + static HelpData _get_property_help_data(const StringName &p_class_name, const StringName &p_property_name); + static HelpData _get_method_help_data(const StringName &p_class_name, const StringName &p_method_name); + static HelpData _get_signal_help_data(const StringName &p_class_name, const StringName &p_signal_name); + static HelpData _get_theme_item_help_data(const StringName &p_class_name, const StringName &p_theme_item_name); + + void _add_type_to_title(const DocType &p_doc_type); + void _update_labels(); void _go_to_help(const String &p_what); void _meta_clicked(const String &p_select); - String text; - protected: - String doc_class_name; - String custom_description; - static void _bind_methods(); void _notification(int p_what); public: - String get_class_description(const StringName &p_class_name) const; - String get_property_description(const StringName &p_class_name, const StringName &p_property_name) const; - String get_method_description(const StringName &p_class_name, const StringName &p_method_name) const; - String get_signal_description(const StringName &p_class_name, const StringName &p_signal_name) const; - String get_theme_item_description(const StringName &p_class_name, const StringName &p_theme_item_name) const; + void parse_symbol(const String &p_symbol); + void set_custom_text(const String &p_type, const String &p_name, const String &p_description); + void prepend_description(const String &p_text); - RichTextLabel *get_rich_text() { return rich_text; } - void set_text(const String &p_text); + void set_content_height_limits(float p_min, float p_max); + void update_content_height(); - EditorHelpBit(); + EditorHelpBit(const String &p_symbol = String()); }; -class EditorHelpTooltip : public EditorHelpBit { - GDCLASS(EditorHelpTooltip, EditorHelpBit); +// Standard tooltips do not allow you to hover over them. +// This class is intended as a temporary workaround. +class EditorHelpBitTooltip : public PopupPanel { + GDCLASS(EditorHelpBitTooltip, PopupPanel); - String tooltip_text; + Timer *timer = nullptr; protected: void _notification(int p_what); + virtual void _input_from_window(const Ref &p_event) override; public: - void parse_tooltip(const String &p_text); + static void show_tooltip(EditorHelpBit *p_help_bit, Control *p_target); - EditorHelpTooltip(const String &p_text = String(), const String &p_custom_description = String()); + void popup_under_cursor(); + + EditorHelpBitTooltip(Control *p_target); }; #if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED) diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index aed1462eb613..50cc89c61832 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -44,6 +44,7 @@ #include "editor/plugins/script_editor_plugin.h" #include "editor/themes/editor_scale.h" #include "editor/themes/editor_theme_manager.h" +#include "scene/gui/margin_container.h" #include "scene/gui/spin_box.h" #include "scene/gui/texture_rect.h" #include "scene/property_utils.h" @@ -916,34 +917,35 @@ void EditorProperty::_update_pin_flags() { } Control *EditorProperty::make_custom_tooltip(const String &p_text) const { - EditorHelpBit *tooltip = nullptr; - - if (has_doc_tooltip) { - String custom_description; - - const EditorInspector *inspector = get_parent_inspector(); - if (inspector) { - custom_description = inspector->get_custom_property_description(p_text); - } - tooltip = memnew(EditorHelpTooltip(p_text, custom_description)); - } - + String custom_warning; 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"; - } - tooltip->set_text(prev_text + "[b][color=" + get_theme_color(SNAME("warning_color")).to_html(false) + "]" + warn + "[/color][/b]"); - } + custom_warning = object->call("_get_property_warning", property); } - return tooltip; + if (has_doc_tooltip || !custom_warning.is_empty()) { + EditorHelpBit *help_bit = memnew(EditorHelpBit); + + if (has_doc_tooltip) { + help_bit->parse_symbol(p_text); + + 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); + } + } + } + + 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(this)); + return memnew(Control); // Make the standard tooltip invisible. + } + + return nullptr; } void EditorProperty::menu_option(int p_option) { @@ -1178,7 +1180,14 @@ void EditorInspectorCategory::_notification(int p_what) { } Control *EditorInspectorCategory::make_custom_tooltip(const String &p_text) const { - return doc_class_name.is_empty() ? nullptr : memnew(EditorHelpTooltip(p_text)); + // If it's not a doc tooltip, fallback to the default one. + if (doc_class_name.is_empty()) { + return nullptr; + } + + EditorHelpBit *help_bit = memnew(EditorHelpBit(p_text)); + EditorHelpBitTooltip::show_tooltip(help_bit, const_cast(this)); + return memnew(Control); // Make the standard tooltip invisible. } Size2 EditorInspectorCategory::get_minimum_size() const { @@ -2887,8 +2896,8 @@ void EditorInspector::update_tree() { category->doc_class_name = doc_name; if (use_doc_hints) { - // `|` separator used in `EditorHelpTooltip` for formatting. - category->set_tooltip_text("class|" + doc_name + "||"); + // `|` separators used in `EditorHelpBit`. + category->set_tooltip_text("class|" + doc_name + "|"); } } @@ -3368,15 +3377,15 @@ void EditorInspector::update_tree() { ep->connect("object_id_selected", callable_mp(this, &EditorInspector::_object_id_selected), CONNECT_DEFERRED); if (use_doc_hints) { - // `|` separator used in `EditorHelpTooltip` for formatting. + // `|` separators used in `EditorHelpBit`. if (theme_item_name.is_empty()) { if (p.usage & PROPERTY_USAGE_INTERNAL) { - ep->set_tooltip_text("internal_property|" + classname + "|" + property_prefix + p.name + "|"); + ep->set_tooltip_text("internal_property|" + classname + "|" + property_prefix + p.name); } else { - ep->set_tooltip_text("property|" + classname + "|" + property_prefix + p.name + "|"); + ep->set_tooltip_text("property|" + classname + "|" + property_prefix + p.name); } } else { - ep->set_tooltip_text("theme_item|" + classname + "|" + theme_item_name + "|"); + ep->set_tooltip_text("theme_item|" + classname + "|" + theme_item_name); } ep->has_doc_tooltip = true; } diff --git a/editor/editor_inspector.h b/editor/editor_inspector.h index eff4f9caa543..cf3bf89e09b3 100644 --- a/editor/editor_inspector.h +++ b/editor/editor_inspector.h @@ -41,6 +41,7 @@ class ConfirmationDialog; class EditorInspector; class EditorValidationPanel; class LineEdit; +class MarginContainer; class OptionButton; class PanelContainer; class PopupMenu; diff --git a/editor/editor_properties_array_dict.cpp b/editor/editor_properties_array_dict.cpp index c59c221e027a..da072744b8ae 100644 --- a/editor/editor_properties_array_dict.cpp +++ b/editor/editor_properties_array_dict.cpp @@ -40,6 +40,7 @@ #include "editor/inspector_dock.h" #include "editor/themes/editor_scale.h" #include "scene/gui/button.h" +#include "scene/gui/margin_container.h" #include "scene/resources/packed_scene.h" bool EditorPropertyArrayObject::_set(const StringName &p_name, const Variant &p_value) { diff --git a/editor/editor_properties_array_dict.h b/editor/editor_properties_array_dict.h index dae0fc52a644..b00f85c93c51 100644 --- a/editor/editor_properties_array_dict.h +++ b/editor/editor_properties_array_dict.h @@ -37,6 +37,7 @@ class Button; class EditorSpinSlider; +class MarginContainer; class EditorPropertyArrayObject : public RefCounted { GDCLASS(EditorPropertyArrayObject, RefCounted); diff --git a/editor/plugins/theme_editor_plugin.cpp b/editor/plugins/theme_editor_plugin.cpp index 0db752e771e4..166ed05748c7 100644 --- a/editor/plugins/theme_editor_plugin.cpp +++ b/editor/plugins/theme_editor_plugin.cpp @@ -2267,7 +2267,9 @@ ThemeTypeDialog::ThemeTypeDialog() { /////////////////////// Control *ThemeItemLabel::make_custom_tooltip(const String &p_text) const { - return memnew(EditorHelpTooltip(p_text)); + EditorHelpBit *help_bit = memnew(EditorHelpBit(p_text)); + EditorHelpBitTooltip::show_tooltip(help_bit, const_cast(this)); + return memnew(Control); // Make the standard tooltip invisible. } VBoxContainer *ThemeTypeEditor::_create_item_list(Theme::DataType p_data_type) { @@ -2436,8 +2438,8 @@ HBoxContainer *ThemeTypeEditor::_create_property_control(Theme::DataType p_data_ item_name->set_h_size_flags(SIZE_EXPAND_FILL); item_name->set_clip_text(true); item_name->set_text(p_item_name); - // `|` separators used in `EditorHelpTooltip` for formatting. - item_name->set_tooltip_text("theme_item|" + edited_type + "|" + p_item_name + "|"); + // `|` separators used in `EditorHelpBit`. + item_name->set_tooltip_text("theme_item|" + edited_type + "|" + p_item_name); item_name->set_mouse_filter(Control::MOUSE_FILTER_STOP); item_name_container->add_child(item_name); diff --git a/editor/plugins/theme_editor_plugin.h b/editor/plugins/theme_editor_plugin.h index ba3446807ede..0bc02789aa1e 100644 --- a/editor/plugins/theme_editor_plugin.h +++ b/editor/plugins/theme_editor_plugin.h @@ -321,7 +321,7 @@ public: ThemeTypeDialog(); }; -// Custom `Label` needed to use `EditorHelpTooltip` to display theme item documentation. +// Custom `Label` needed to use `EditorHelpBit` to display theme item documentation. class ThemeItemLabel : public Label { virtual Control *make_custom_tooltip(const String &p_text) const; }; diff --git a/editor/property_selector.cpp b/editor/property_selector.cpp index ac175d01a6c3..d7af751f956d 100644 --- a/editor/property_selector.cpp +++ b/editor/property_selector.cpp @@ -87,7 +87,7 @@ void PropertySelector::_update_search() { } search_options->clear(); - help_bit->set_text(""); + help_bit->set_custom_text(String(), String(), String()); TreeItem *root = search_options->create_item(); @@ -353,7 +353,7 @@ void PropertySelector::_confirmed() { } void PropertySelector::_item_selected() { - help_bit->set_text(""); + help_bit->set_custom_text(String(), String(), String()); TreeItem *item = search_options->get_selected(); if (!item) { @@ -372,25 +372,21 @@ void PropertySelector::_item_selected() { String text; while (!class_type.is_empty()) { - text = properties ? help_bit->get_property_description(class_type, name) : help_bit->get_method_description(class_type, name); - if (!text.is_empty()) { - break; + if (properties) { + if (ClassDB::has_property(class_type, name, true)) { + help_bit->parse_symbol("property|" + class_type + "|" + name); + break; + } + } else { + if (ClassDB::has_method(class_type, name, true)) { + help_bit->parse_symbol("method|" + class_type + "|" + name); + break; + } } // It may be from a parent class, keep looking. class_type = ClassDB::get_parent_class(class_type); } - - if (!text.is_empty()) { - // Display both property name and description, since the help bit may be displayed - // far away from the location (especially if the dialog was resized to be taller). - help_bit->set_text(vformat("[b]%s[/b]: %s", name, text)); - help_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); - } else { - // Use nested `vformat()` as translators shouldn't interfere with BBCode tags. - help_bit->set_text(vformat(TTR("No description available for %s."), vformat("[b]%s[/b]", name))); - help_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5)); - } } void PropertySelector::_hide_requested() { @@ -569,8 +565,7 @@ PropertySelector::PropertySelector() { search_options->set_hide_folding(true); help_bit = memnew(EditorHelpBit); - vbc->add_margin_child(TTR("Description:"), help_bit); - help_bit->get_rich_text()->set_fit_content(false); - help_bit->get_rich_text()->set_custom_minimum_size(Size2(help_bit->get_rich_text()->get_minimum_size().x, 135 * EDSCALE)); + help_bit->set_content_height_limits(80 * EDSCALE, 80 * EDSCALE); help_bit->connect("request_hide", callable_mp(this, &PropertySelector::_hide_requested)); + vbc->add_margin_child(TTR("Description:"), help_bit); } diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp index 734fa415a394..71277cf68a4c 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -2158,6 +2158,28 @@ void EditorThemeManager::_populate_editor_styles(const Ref &p_theme p_theme->set_constant("text_highlight_v_padding", "EditorHelp", 2 * EDSCALE); } + // EditorHelpBitTitle. + { + Ref 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 style = p_config.tree_panel_style->duplicate(); + style->set_corner_radius(CORNER_TOP_LEFT, 0); + style->set_corner_radius(CORNER_TOP_RIGHT, 0); + + p_theme->set_type_variation("EditorHelpBitContent", "RichTextLabel"); + p_theme->set_stylebox("normal", "EditorHelpBitContent", style); + } + // Asset Library. p_theme->set_stylebox("bg", "AssetLib", p_config.base_empty_style); p_theme->set_stylebox("panel", "AssetLib", p_config.content_panel_style);