summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/object/class_db.cpp27
-rw-r--r--core/object/script_language.cpp8
-rw-r--r--core/object/script_language.h2
-rw-r--r--doc/classes/EditorSettings.xml3
-rw-r--r--doc/classes/Object.xml2
-rw-r--r--editor/connections_dialog.cpp17
-rw-r--r--editor/connections_dialog.h2
-rw-r--r--editor/create_dialog.cpp16
-rw-r--r--editor/editor_build_profile.cpp22
-rw-r--r--editor/editor_build_profile.h1
-rw-r--r--editor/editor_feature_profile.cpp23
-rw-r--r--editor/editor_feature_profile.h1
-rw-r--r--editor/editor_help.cpp860
-rw-r--r--editor/editor_help.h92
-rw-r--r--editor/editor_inspector.cpp65
-rw-r--r--editor/editor_inspector.h1
-rw-r--r--editor/editor_properties_array_dict.cpp1
-rw-r--r--editor/editor_properties_array_dict.h1
-rw-r--r--editor/editor_quick_open.cpp61
-rw-r--r--editor/editor_quick_open.h2
-rw-r--r--editor/editor_settings.cpp1
-rw-r--r--editor/filesystem_dock.cpp49
-rw-r--r--editor/filesystem_dock.h3
-rw-r--r--editor/icons/PreviewRotate.svg1
-rw-r--r--editor/import/3d/scene_import_settings.cpp72
-rw-r--r--editor/import/3d/scene_import_settings.h17
-rw-r--r--editor/plugins/gizmos/geometry_instance_3d_gizmo_plugin.cpp79
-rw-r--r--editor/plugins/gizmos/geometry_instance_3d_gizmo_plugin.h49
-rw-r--r--editor/plugins/node_3d_editor_plugin.cpp2
-rw-r--r--editor/plugins/theme_editor_plugin.cpp8
-rw-r--r--editor/plugins/theme_editor_plugin.h2
-rw-r--r--editor/property_selector.cpp33
-rw-r--r--editor/themes/editor_theme_manager.cpp22
-rw-r--r--methods.py4
-rw-r--r--modules/gdscript/gdscript_compiler.cpp6
-rw-r--r--modules/gdscript/gdscript_compiler.h6
-rw-r--r--modules/mono/csharp_script.cpp51
-rw-r--r--scene/2d/audio_stream_player_2d.cpp1
-rw-r--r--scene/3d/audio_stream_player_3d.cpp1
-rw-r--r--scene/3d/node_3d.cpp9
-rw-r--r--scene/3d/node_3d.h1
-rw-r--r--scene/3d/visual_instance_3d.cpp1
-rw-r--r--scene/audio/audio_stream_player.cpp1
-rw-r--r--scene/gui/dialogs.cpp12
-rw-r--r--scene/gui/graph_frame.cpp2
-rw-r--r--servers/rendering/renderer_rd/effects/copy_effects.cpp10
-rw-r--r--servers/rendering/renderer_rd/effects/copy_effects.h3
-rw-r--r--servers/rendering/renderer_rd/shaders/effects/copy_to_fb.glsl5
-rw-r--r--servers/rendering/renderer_rd/storage_rd/texture_storage.cpp5
-rw-r--r--tests/core/math/test_transform_3d.h29
50 files changed, 1230 insertions, 462 deletions
diff --git a/core/object/class_db.cpp b/core/object/class_db.cpp
index 80a2703c2f..7ef1ce74ed 100644
--- a/core/object/class_db.cpp
+++ b/core/object/class_db.cpp
@@ -182,8 +182,20 @@ public:
// Construct a placeholder.
Object *obj = native_parent->creation_func();
+
+ // ClassDB::set_object_extension_instance() won't be called for placeholders.
+ // We need need to make sure that all the things it would have done (even if
+ // done in a different way to support placeholders) will also be done here.
+
obj->_extension = ClassDB::get_placeholder_extension(ti->name);
obj->_extension_instance = memnew(PlaceholderExtensionInstance(ti->name));
+
+#ifdef TOOLS_ENABLED
+ if (obj->_extension->track_instance) {
+ obj->_extension->track_instance(obj->_extension->tracking_userdata, obj);
+ }
+#endif
+
return obj;
}
@@ -506,14 +518,7 @@ Object *ClassDB::_instantiate_internal(const StringName &p_class, bool p_require
extension = get_placeholder_extension(ti->name);
}
#endif
- Object *obj = (Object *)extension->create_instance(extension->class_userdata);
-
-#ifdef TOOLS_ENABLED
- if (extension->track_instance) {
- extension->track_instance(extension->tracking_userdata, obj);
- }
-#endif
- return obj;
+ return (Object *)extension->create_instance(extension->class_userdata);
} else {
#ifdef TOOLS_ENABLED
if (!p_require_real_class && ti->is_runtime && Engine::get_singleton()->is_editor_hint()) {
@@ -638,6 +643,12 @@ void ClassDB::set_object_extension_instance(Object *p_object, const StringName &
p_object->_extension = ti->gdextension;
p_object->_extension_instance = p_instance;
+
+#ifdef TOOLS_ENABLED
+ if (p_object->_extension->track_instance) {
+ p_object->_extension->track_instance(p_object->_extension->tracking_userdata, p_object);
+ }
+#endif
}
bool ClassDB::can_instantiate(const StringName &p_class) {
diff --git a/core/object/script_language.cpp b/core/object/script_language.cpp
index 660e13e819..bd3199ca0a 100644
--- a/core/object/script_language.cpp
+++ b/core/object/script_language.cpp
@@ -51,7 +51,7 @@ void Script::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_POSTINITIALIZE: {
if (EngineDebugger::is_active()) {
- EngineDebugger::get_script_debugger()->set_break_language(get_language());
+ callable_mp(this, &Script::_set_debugger_break_language).call_deferred();
}
} break;
}
@@ -103,6 +103,12 @@ Dictionary Script::_get_script_constant_map() {
return ret;
}
+void Script::_set_debugger_break_language() {
+ if (EngineDebugger::is_active()) {
+ EngineDebugger::get_script_debugger()->set_break_language(get_language());
+ }
+}
+
int Script::get_script_method_argument_count(const StringName &p_method, bool *r_is_valid) const {
MethodInfo mi = get_method_info(p_method);
diff --git a/core/object/script_language.h b/core/object/script_language.h
index c6c6f3de9f..223f114150 100644
--- a/core/object/script_language.h
+++ b/core/object/script_language.h
@@ -124,6 +124,8 @@ protected:
TypedArray<Dictionary> _get_script_signal_list();
Dictionary _get_script_constant_map();
+ void _set_debugger_break_language();
+
public:
virtual void reload_from_file() override;
diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml
index 20ee65403c..93a7b09fce 100644
--- a/doc/classes/EditorSettings.xml
+++ b/doc/classes/EditorSettings.xml
@@ -384,6 +384,9 @@
<member name="editors/3d/selection_box_color" type="Color" setter="" getter="">
The color to use for the selection box that surrounds selected nodes in the 3D editor viewport. The color's alpha channel influences the selection box's opacity.
</member>
+ <member name="editors/3d_gizmos/gizmo_colors/aabb" type="Color" setter="" getter="">
+ The color to use for the AABB gizmo that displays the [GeometryInstance3D]'s custom [AABB].
+ </member>
<member name="editors/3d_gizmos/gizmo_colors/instantiated" type="Color" setter="" getter="">
The color override to use for 3D editor gizmos if the [Node3D] in question is part of an instantiated scene file (from the perspective of the current scene).
</member>
diff --git a/doc/classes/Object.xml b/doc/classes/Object.xml
index 961cb2e684..68d2d6411c 100644
--- a/doc/classes/Object.xml
+++ b/doc/classes/Object.xml
@@ -905,7 +905,7 @@
[b]Note:[/b] Metadata that has a name starting with an underscore ([code]_[/code]) is considered editor-only. Editor-only metadata is not displayed in the Inspector and should not be edited, although it can still be found by this method.
</description>
</method>
- <method name="remove_user_signal" experimental="">
+ <method name="remove_user_signal">
<return type="void" />
<param index="0" name="signal" type="StringName" />
<description>
diff --git a/editor/connections_dialog.cpp b/editor/connections_dialog.cpp
index de5f9ecf89..f57e9cb5f8 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<ConnectionsDockTree *>(this));
+ return memnew(Control); // Make the standard tooltip invisible.
}
struct _ConnectionsDockMethodInfoSort {
@@ -1458,8 +1465,8 @@ void ConnectionsDock::update_tree() {
section_item = tree->create_item(root);
section_item->set_text(0, class_name);
- // `|` separators used in `EditorHelpTooltip` for formatting.
- section_item->set_tooltip_text(0, "class|" + doc_class_name + "||");
+ // `|` separators used in `EditorHelpBit`.
+ section_item->set_tooltip_text(0, "class|" + doc_class_name + "|");
section_item->set_icon(0, class_icon);
section_item->set_selectable(0, false);
section_item->set_editable(0, false);
@@ -1490,8 +1497,8 @@ void ConnectionsDock::update_tree() {
sinfo["args"] = argnames;
signal_item->set_metadata(0, sinfo);
signal_item->set_icon(0, get_editor_theme_icon(SNAME("Signal")));
- // `|` separators used in `EditorHelpTooltip` for formatting.
- signal_item->set_tooltip_text(0, "signal|" + doc_class_name + "|" + String(signal_name) + "|" + signame.trim_prefix(mi.name));
+ // `|` separators used in `EditorHelpBit`.
+ signal_item->set_tooltip_text(0, "signal|" + doc_class_name + "|" + String(signal_name));
// List existing connections.
List<Object::Connection> existing_connections;
diff --git a/editor/connections_dialog.h b/editor/connections_dialog.h
index f644d3335a..78f1b76e23 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 f7914d3aaa..b00f059b36 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 c6ecaba230..c1db674cbe 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 649784afc3..a947365c7f 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 541bcd5e02..86b7b3eb2f 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 25ee1c9ba4..2fa6ae9813 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 1f7505633b..5cc09b7104 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);
+ if (p_select.begins_with("$")) { // Enum.
+ const String link = p_select.substr(1);
+
+ String enum_class_name;
+ String enum_name;
+ if (CoreConstants::is_global_enum(link)) {
+ enum_class_name = "@GlobalScope";
+ enum_name = link;
} else {
- class_name = "@GlobalScope";
+ const int dot_pos = link.rfind(".");
+ if (dot_pos >= 0) {
+ enum_class_name = link.left(dot_pos);
+ enum_name = link.substr(dot_pos + 1);
+ } else {
+ enum_class_name = edited_class;
+ enum_name = link;
+ }
}
- emit_signal(SNAME("go_to_help"), "class_enum:" + class_name + ":" + select);
- return;
- } else if (p_select.begins_with("#")) {
- emit_signal(SNAME("go_to_help"), "class_name:" + p_select.substr(1, p_select.length()));
- return;
- } else if (p_select.begins_with("@")) {
- int tag_end = p_select.find_char(' ');
- String tag = p_select.substr(1, tag_end - 1);
- String link = p_select.substr(tag_end + 1, p_select.length()).lstrip(" ");
+ emit_signal(SNAME("go_to_help"), "class_enum:" + enum_class_name + ":" + enum_name);
+ } else if (p_select.begins_with("#")) { // Class.
+ emit_signal(SNAME("go_to_help"), "class_name:" + p_select.substr(1));
+ } else if (p_select.begins_with("@")) { // Member.
+ const int tag_end = p_select.find_char(' ');
+ const String tag = p_select.substr(1, tag_end - 1);
+ const String link = p_select.substr(tag_end + 1).lstrip(" ");
String topic;
- HashMap<String, int> *table = nullptr;
+ const HashMap<String, int> *table = nullptr;
if (tag == "method") {
topic = "class_method";
@@ -311,14 +317,14 @@ void EditorHelp::_class_desc_select(const String &p_select) {
}
if (link.contains(".")) {
- int class_end = link.find_char('.');
- emit_signal(SNAME("go_to_help"), topic + ":" + link.substr(0, class_end) + ":" + link.substr(class_end + 1, link.length()));
+ const int class_end = link.find_char('.');
+ emit_signal(SNAME("go_to_help"), topic + ":" + link.left(class_end) + ":" + link.substr(class_end + 1));
}
}
- } else if (p_select.begins_with("http")) {
+ } else if (p_select.begins_with("http:") || p_select.begins_with("https:")) {
OS::get_singleton()->shell_open(p_select);
- } else if (p_select.begins_with("^")) {
- DisplayServer::get_singleton()->clipboard_set(p_select.trim_prefix("^"));
+ } else if (p_select.begins_with("^")) { // Copy button.
+ DisplayServer::get_singleton()->clipboard_set(p_select.substr(1));
}
}
@@ -341,13 +347,15 @@ void EditorHelp::_class_desc_resized(bool p_force_update_theme) {
}
}
-void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is_bitfield) {
+static void _add_type_to_rt(const String &p_type, const String &p_enum, bool p_is_bitfield, RichTextLabel *p_rt, const Control *p_owner_node, const String &p_class) {
+ const Color type_color = p_owner_node->get_theme_color(SNAME("type_color"), SNAME("EditorHelp"));
+
if (p_type.is_empty() || p_type == "void") {
- class_desc->push_color(Color(theme_cache.type_color, 0.5));
- class_desc->push_hint(TTR("No return value."));
- class_desc->add_text("void");
- class_desc->pop(); // hint
- class_desc->pop(); // color
+ p_rt->push_color(Color(type_color, 0.5));
+ p_rt->push_hint(TTR("No return value."));
+ p_rt->add_text("void");
+ p_rt->pop(); // hint
+ p_rt->pop(); // color
return;
}
@@ -359,12 +367,12 @@ void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is
String display_t; // For display purposes.
if (is_enum_type) {
link_t = p_enum; // The link for enums is always the full enum description
- display_t = _contextualize_class_specifier(p_enum, edited_class);
+ display_t = _contextualize_class_specifier(p_enum, p_class);
} else {
- display_t = _contextualize_class_specifier(p_type, edited_class);
+ display_t = _contextualize_class_specifier(p_type, p_class);
}
- class_desc->push_color(theme_cache.type_color);
+ p_rt->push_color(type_color);
bool add_array = false;
if (can_ref) {
if (link_t.ends_with("[]")) {
@@ -372,37 +380,41 @@ void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is
link_t = link_t.trim_suffix("[]");
display_t = display_t.trim_suffix("[]");
- class_desc->push_meta("#Array", RichTextLabel::META_UNDERLINE_ON_HOVER); // class
- class_desc->add_text("Array");
- class_desc->pop(); // meta
- class_desc->add_text("[");
+ p_rt->push_meta("#Array", RichTextLabel::META_UNDERLINE_ON_HOVER); // class
+ p_rt->add_text("Array");
+ p_rt->pop(); // meta
+ p_rt->add_text("[");
} else if (is_bitfield) {
- class_desc->push_color(Color(theme_cache.type_color, 0.5));
- class_desc->push_hint(TTR("This value is an integer composed as a bitmask of the following flags."));
- class_desc->add_text("BitField");
- class_desc->pop(); // hint
- class_desc->add_text("[");
- class_desc->pop(); // color
+ p_rt->push_color(Color(type_color, 0.5));
+ p_rt->push_hint(TTR("This value is an integer composed as a bitmask of the following flags."));
+ p_rt->add_text("BitField");
+ p_rt->pop(); // hint
+ p_rt->add_text("[");
+ p_rt->pop(); // color
}
if (is_enum_type) {
- class_desc->push_meta("$" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // enum
+ p_rt->push_meta("$" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // enum
} else {
- class_desc->push_meta("#" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // class
+ p_rt->push_meta("#" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // class
}
}
- class_desc->add_text(display_t);
+ p_rt->add_text(display_t);
if (can_ref) {
- class_desc->pop(); // meta
+ p_rt->pop(); // meta
if (add_array) {
- class_desc->add_text("]");
+ p_rt->add_text("]");
} else if (is_bitfield) {
- class_desc->push_color(Color(theme_cache.type_color, 0.5));
- class_desc->add_text("]");
- class_desc->pop(); // color
+ p_rt->push_color(Color(type_color, 0.5));
+ p_rt->add_text("]");
+ p_rt->pop(); // color
}
}
- class_desc->pop(); // color
+ p_rt->pop(); // color
+}
+
+void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is_bitfield) {
+ _add_type_to_rt(p_type, p_enum, p_is_bitfield, class_desc, this, edited_class);
}
void EditorHelp::_add_type_icon(const String &p_type, int p_size, const String &p_fallback) {
@@ -717,10 +729,10 @@ void EditorHelp::_update_method_list(MethodType p_method_type, const Vector<DocD
String group_prefix;
for (int i = 0; i < m.size(); i++) {
- const String new_prefix = m[i].name.substr(0, 3);
+ const String new_prefix = m[i].name.left(3);
bool is_new_group = false;
- if (i < m.size() - 1 && new_prefix == m[i + 1].name.substr(0, 3) && new_prefix != group_prefix) {
+ if (i < m.size() - 1 && new_prefix == m[i + 1].name.left(3) && new_prefix != group_prefix) {
is_new_group = i > 0;
group_prefix = new_prefix;
} else if (!group_prefix.is_empty() && new_prefix != group_prefix) {
@@ -748,7 +760,7 @@ void EditorHelp::_update_method_list(MethodType p_method_type, const Vector<DocD
}
void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc, MethodType p_method_type, const Vector<DocData::MethodDoc> &p_methods) {
-#define DTR_DOC(m_string) (p_classdoc.is_script_doc ? (m_string) : DTR(m_string))
+#define HANDLE_DOC(m_string) ((p_classdoc.is_script_doc ? (m_string) : DTR(m_string)).strip_edges())
class_desc->add_newline();
class_desc->add_newline();
@@ -807,7 +819,7 @@ void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc
TTRC("This constructor may be changed or removed in future versions."),
TTRC("This operator may be changed or removed in future versions."),
};
- DEPRECATED_DOC_MSG(DTR_DOC(method.deprecated_message), TTRGET(messages_by_type[p_method_type]));
+ DEPRECATED_DOC_MSG(HANDLE_DOC(method.deprecated_message), TTRGET(messages_by_type[p_method_type]));
}
if (method.is_experimental) {
@@ -822,7 +834,7 @@ void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc
TTRC("This constructor may be changed or removed in future versions."),
TTRC("This operator may be changed or removed in future versions."),
};
- EXPERIMENTAL_DOC_MSG(DTR_DOC(method.experimental_message), TTRGET(messages_by_type[p_method_type]));
+ EXPERIMENTAL_DOC_MSG(HANDLE_DOC(method.experimental_message), TTRGET(messages_by_type[p_method_type]));
}
if (!method.errors_returned.is_empty()) {
@@ -856,7 +868,7 @@ void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc
class_desc->pop(); // list
}
- const String descr = DTR_DOC(method.description).strip_edges();
+ const String descr = HANDLE_DOC(method.description);
const bool is_documented = method.is_deprecated || method.is_experimental || !descr.is_empty();
if (!descr.is_empty()) {
if (has_prev_text) {
@@ -903,7 +915,7 @@ void EditorHelp::_update_method_descriptions(const DocData::ClassDoc &p_classdoc
}
}
-#undef DTR_DOC
+#undef HANDLE_DOC
}
void EditorHelp::_update_doc() {
@@ -922,7 +934,7 @@ void EditorHelp::_update_doc() {
DocData::ClassDoc cd = doc->class_list[edited_class]; // Make a copy, so we can sort without worrying.
-#define DTR_DOC(m_string) (cd.is_script_doc ? (m_string) : DTR(m_string))
+#define HANDLE_DOC(m_string) ((cd.is_script_doc ? (m_string) : DTR(m_string)).strip_edges())
// Class name
@@ -940,12 +952,12 @@ void EditorHelp::_update_doc() {
if (cd.is_deprecated) {
class_desc->add_newline();
- DEPRECATED_DOC_MSG(DTR_DOC(cd.deprecated_message), TTR("This class may be changed or removed in future versions."));
+ DEPRECATED_DOC_MSG(HANDLE_DOC(cd.deprecated_message), TTR("This class may be changed or removed in future versions."));
}
if (cd.is_experimental) {
class_desc->add_newline();
- EXPERIMENTAL_DOC_MSG(DTR_DOC(cd.experimental_message), TTR("This class may be changed or removed in future versions."));
+ EXPERIMENTAL_DOC_MSG(HANDLE_DOC(cd.experimental_message), TTR("This class may be changed or removed in future versions."));
}
// Inheritance tree
@@ -1003,7 +1015,7 @@ void EditorHelp::_update_doc() {
bool has_description = false;
// Brief description
- const String brief_class_descr = DTR_DOC(cd.brief_description).strip_edges();
+ const String brief_class_descr = HANDLE_DOC(cd.brief_description);
if (!brief_class_descr.is_empty()) {
has_description = true;
@@ -1022,7 +1034,7 @@ void EditorHelp::_update_doc() {
}
// Class description
- const String class_descr = DTR_DOC(cd.description).strip_edges();
+ const String class_descr = HANDLE_DOC(cd.description);
if (!class_descr.is_empty()) {
has_description = true;
@@ -1106,9 +1118,9 @@ void EditorHelp::_update_doc() {
class_desc->push_color(theme_cache.symbol_color);
for (const DocData::TutorialDoc &tutorial : cd.tutorials) {
- const String link = DTR_DOC(tutorial.link).strip_edges();
+ const String link = HANDLE_DOC(tutorial.link);
- String link_text = DTR_DOC(tutorial.title).strip_edges();
+ String link_text = HANDLE_DOC(tutorial.title);
if (link_text.is_empty()) {
const int sep_pos = link.find("//");
if (sep_pos >= 0) {
@@ -1441,7 +1453,7 @@ void EditorHelp::_update_doc() {
_push_normal_font();
class_desc->push_color(theme_cache.comment_color);
- const String descr = DTR_DOC(theme_item.description).strip_edges();
+ const String descr = HANDLE_DOC(theme_item.description);
if (!descr.is_empty()) {
_add_text(descr);
} else {
@@ -1538,13 +1550,13 @@ void EditorHelp::_update_doc() {
_push_normal_font();
class_desc->push_color(theme_cache.comment_color);
- const String descr = DTR_DOC(signal.description).strip_edges();
+ const String descr = HANDLE_DOC(signal.description);
const bool is_multiline = descr.find_char('\n') > 0;
bool has_prev_text = false;
if (signal.is_deprecated) {
has_prev_text = true;
- DEPRECATED_DOC_MSG(DTR_DOC(signal.deprecated_message), TTR("This signal may be changed or removed in future versions."));
+ DEPRECATED_DOC_MSG(HANDLE_DOC(signal.deprecated_message), TTR("This signal may be changed or removed in future versions."));
}
if (signal.is_experimental) {
@@ -1555,7 +1567,7 @@ void EditorHelp::_update_doc() {
}
}
has_prev_text = true;
- EXPERIMENTAL_DOC_MSG(DTR_DOC(signal.experimental_message), TTR("This signal may be changed or removed in future versions."));
+ EXPERIMENTAL_DOC_MSG(HANDLE_DOC(signal.experimental_message), TTR("This signal may be changed or removed in future versions."));
}
if (!descr.is_empty()) {
@@ -1669,7 +1681,7 @@ void EditorHelp::_update_doc() {
// Enum description.
if (key != "@unnamed_enums" && cd.enums.has(key)) {
- const String descr = DTR_DOC(cd.enums[key].description).strip_edges();
+ const String descr = HANDLE_DOC(cd.enums[key].description);
const bool is_multiline = descr.find_char('\n') > 0;
if (cd.enums[key].is_deprecated || cd.enums[key].is_experimental || !descr.is_empty()) {
class_desc->add_newline();
@@ -1682,7 +1694,7 @@ void EditorHelp::_update_doc() {
if (cd.enums[key].is_deprecated) {
has_prev_text = true;
- DEPRECATED_DOC_MSG(DTR_DOC(cd.enums[key].deprecated_message), TTR("This enumeration may be changed or removed in future versions."));
+ DEPRECATED_DOC_MSG(HANDLE_DOC(cd.enums[key].deprecated_message), TTR("This enumeration may be changed or removed in future versions."));
}
if (cd.enums[key].is_experimental) {
@@ -1693,7 +1705,7 @@ void EditorHelp::_update_doc() {
}
}
has_prev_text = true;
- EXPERIMENTAL_DOC_MSG(DTR_DOC(cd.enums[key].experimental_message), TTR("This enumeration may be changed or removed in future versions."));
+ EXPERIMENTAL_DOC_MSG(HANDLE_DOC(cd.enums[key].experimental_message), TTR("This enumeration may be changed or removed in future versions."));
}
if (!descr.is_empty()) {
@@ -1718,7 +1730,7 @@ void EditorHelp::_update_doc() {
bool prev_is_multiline = true; // Use a large margin for the first item.
for (const DocData::ConstantDoc &enum_value : E.value) {
- const String descr = DTR_DOC(enum_value.description).strip_edges();
+ const String descr = HANDLE_DOC(enum_value.description);
const bool is_multiline = descr.find_char('\n') > 0;
class_desc->add_newline();
@@ -1766,7 +1778,7 @@ void EditorHelp::_update_doc() {
if (enum_value.is_deprecated) {
has_prev_text = true;
- DEPRECATED_DOC_MSG(DTR_DOC(enum_value.deprecated_message), TTR("This constant may be changed or removed in future versions."));
+ DEPRECATED_DOC_MSG(HANDLE_DOC(enum_value.deprecated_message), TTR("This constant may be changed or removed in future versions."));
}
if (enum_value.is_experimental) {
@@ -1777,7 +1789,7 @@ void EditorHelp::_update_doc() {
}
}
has_prev_text = true;
- EXPERIMENTAL_DOC_MSG(DTR_DOC(enum_value.experimental_message), TTR("This constant may be changed or removed in future versions."));
+ EXPERIMENTAL_DOC_MSG(HANDLE_DOC(enum_value.experimental_message), TTR("This constant may be changed or removed in future versions."));
}
if (!descr.is_empty()) {
@@ -1817,7 +1829,7 @@ void EditorHelp::_update_doc() {
bool prev_is_multiline = true; // Use a large margin for the first item.
for (const DocData::ConstantDoc &constant : constants) {
- const String descr = DTR_DOC(constant.description).strip_edges();
+ const String descr = HANDLE_DOC(constant.description);
const bool is_multiline = descr.find_char('\n') > 0;
class_desc->add_newline();
@@ -1871,7 +1883,7 @@ void EditorHelp::_update_doc() {
if (constant.is_deprecated) {
has_prev_text = true;
- DEPRECATED_DOC_MSG(DTR_DOC(constant.deprecated_message), TTR("This constant may be changed or removed in future versions."));
+ DEPRECATED_DOC_MSG(HANDLE_DOC(constant.deprecated_message), TTR("This constant may be changed or removed in future versions."));
}
if (constant.is_experimental) {
@@ -1882,7 +1894,7 @@ void EditorHelp::_update_doc() {
}
}
has_prev_text = true;
- EXPERIMENTAL_DOC_MSG(DTR_DOC(constant.experimental_message), TTR("This constant may be changed or removed in future versions."));
+ EXPERIMENTAL_DOC_MSG(HANDLE_DOC(constant.experimental_message), TTR("This constant may be changed or removed in future versions."));
}
if (!descr.is_empty()) {
@@ -2000,7 +2012,7 @@ void EditorHelp::_update_doc() {
_push_normal_font();
class_desc->push_color(theme_cache.comment_color);
- const String descr = DTR_DOC(annotation.description).strip_edges();
+ const String descr = HANDLE_DOC(annotation.description);
if (!descr.is_empty()) {
_add_text(descr);
} else {
@@ -2185,7 +2197,7 @@ void EditorHelp::_update_doc() {
if (prop.is_deprecated) {
has_prev_text = true;
- DEPRECATED_DOC_MSG(DTR_DOC(prop.deprecated_message), TTR("This property may be changed or removed in future versions."));
+ DEPRECATED_DOC_MSG(HANDLE_DOC(prop.deprecated_message), TTR("This property may be changed or removed in future versions."));
}
if (prop.is_experimental) {
@@ -2194,10 +2206,10 @@ void EditorHelp::_update_doc() {
class_desc->add_newline();
}
has_prev_text = true;
- EXPERIMENTAL_DOC_MSG(DTR_DOC(prop.experimental_message), TTR("This property may be changed or removed in future versions."));
+ EXPERIMENTAL_DOC_MSG(HANDLE_DOC(prop.experimental_message), TTR("This property may be changed or removed in future versions."));
}
- const String descr = DTR_DOC(prop.description).strip_edges();
+ const String descr = HANDLE_DOC(prop.description);
if (!descr.is_empty()) {
if (has_prev_text) {
class_desc->add_newline();
@@ -2251,7 +2263,7 @@ void EditorHelp::_update_doc() {
// Free the scroll.
scroll_locked = false;
-#undef DTR_DOC
+#undef HANDLE_DOC
}
void EditorHelp::_request_help(const String &p_string) {
@@ -2331,7 +2343,7 @@ void EditorHelp::_help_callback(const String &p_topic) {
}
}
-static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control *p_owner_node, const String &p_class) {
+static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, const Control *p_owner_node, const String &p_class) {
const DocTools *doc = EditorHelp::get_doc_data();
bool is_native = false;
@@ -2452,7 +2464,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control
const String tag = bbcode.substr(brk_pos + 1, brk_end - brk_pos - 1);
if (tag.begins_with("/")) {
- bool tag_ok = tag_stack.size() && tag_stack.front()->get() == tag.substr(1, tag.length());
+ bool tag_ok = tag_stack.size() && tag_stack.front()->get() == tag.substr(1);
if (!tag_ok) {
p_rt->add_text("[");
@@ -2467,8 +2479,8 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control
}
} else if (tag.begins_with("method ") || tag.begins_with("constructor ") || tag.begins_with("operator ") || tag.begins_with("member ") || tag.begins_with("signal ") || tag.begins_with("enum ") || tag.begins_with("constant ") || tag.begins_with("annotation ") || tag.begins_with("theme_item ")) {
const int tag_end = tag.find_char(' ');
- const String link_tag = tag.substr(0, tag_end);
- const String link_target = tag.substr(tag_end + 1, tag.length()).lstrip(" ");
+ const String link_tag = tag.left(tag_end);
+ const String link_target = tag.substr(tag_end + 1).lstrip(" ");
Color target_color = link_color;
RichTextLabel::MetaUnderline underline_mode = RichTextLabel::META_UNDERLINE_ON_HOVER;
@@ -2520,7 +2532,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control
pos = brk_end + 1;
} else if (tag.begins_with("param ")) {
const int tag_end = tag.find_char(' ');
- const String param_name = tag.substr(tag_end + 1, tag.length()).lstrip(" ");
+ const String param_name = tag.substr(tag_end + 1).lstrip(" ");
// Use monospace font with translucent background color to make code easier to distinguish from other text.
p_rt->push_font(doc_code_font);
@@ -2741,7 +2753,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control
pos = brk_end + 1;
tag_stack.push_front(tag);
} else if (tag.begins_with("url=")) {
- String url = tag.substr(4, tag.length());
+ String url = tag.substr(4);
p_rt->push_meta(url);
pos = brk_end + 1;
@@ -2751,13 +2763,13 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control
int height = 0;
bool size_in_percent = false;
if (tag.length() > 4) {
- Vector<String> subtags = tag.substr(4, tag.length()).split(" ");
+ Vector<String> subtags = tag.substr(4).split(" ");
HashMap<String, String> bbcode_options;
for (int i = 0; i < subtags.size(); i++) {
const String &expr = subtags[i];
int value_pos = expr.find_char('=');
if (value_pos > -1) {
- bbcode_options[expr.substr(0, value_pos)] = expr.substr(value_pos + 1).unquote();
+ bbcode_options[expr.left(value_pos)] = expr.substr(value_pos + 1).unquote();
}
}
HashMap<String, String>::Iterator width_option = bbcode_options.find("width");
@@ -2787,14 +2799,14 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control
pos = end;
tag_stack.push_front("img");
} else if (tag.begins_with("color=")) {
- String col = tag.substr(6, tag.length());
+ String col = tag.substr(6);
Color color = Color::from_string(col, Color());
p_rt->push_color(color);
pos = brk_end + 1;
tag_stack.push_front("color");
} else if (tag.begins_with("font=")) {
- String font_path = tag.substr(5, tag.length());
+ String font_path = tag.substr(5);
Ref<Font> font = ResourceLoader::load(font_path, "Font");
if (font.is_valid()) {
p_rt->push_font(font);
@@ -3120,68 +3132,50 @@ DocTools *EditorHelp::get_doc_data() {
/// EditorHelpBit ///
-void EditorHelpBit::_go_to_help(const String &p_what) {
- EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT);
- ScriptEditor::get_singleton()->goto_help(p_what);
- emit_signal(SNAME("request_hide"));
-}
+#define HANDLE_DOC(m_string) ((is_native ? DTR(m_string) : (m_string)).strip_edges())
-void EditorHelpBit::_meta_clicked(const String &p_select) {
- if (p_select.begins_with("$")) { // enum
- String select = p_select.substr(1, p_select.length());
- String class_name;
- int rfind = select.rfind(".");
- if (rfind != -1) {
- class_name = select.substr(0, rfind);
- select = select.substr(rfind + 1);
- } else {
- class_name = "@GlobalScope";
- }
- _go_to_help("class_enum:" + class_name + ":" + select);
- return;
- } else if (p_select.begins_with("#")) {
- _go_to_help("class_name:" + p_select.substr(1, p_select.length()));
- return;
- } else if (p_select.begins_with("@")) {
- String m = p_select.substr(1, p_select.length());
-
- if (m.contains(".")) {
- _go_to_help("class_method:" + m.get_slice(".", 0) + ":" + m.get_slice(".", 0)); // Must go somewhere else.
- }
- }
-}
-
-String EditorHelpBit::get_class_description(const StringName &p_class_name) const {
+EditorHelpBit::HelpData EditorHelpBit::_get_class_help_data(const StringName &p_class_name) {
if (doc_class_cache.has(p_class_name)) {
return doc_class_cache[p_class_name];
}
- String description;
+ HelpData result;
const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
if (E) {
// Non-native class shouldn't be cached, nor translated.
const bool is_native = !E->value.is_script_doc;
- description = is_native ? DTR(E->value.brief_description) : E->value.brief_description;
+
+ result.description = HANDLE_DOC(E->value.brief_description);
+ if (E->value.is_deprecated) {
+ if (E->value.deprecated_message.is_empty()) {
+ result.deprecated_message = TTR("This class may be changed or removed in future versions.");
+ } else {
+ result.deprecated_message = HANDLE_DOC(E->value.deprecated_message);
+ }
+ }
+ if (E->value.is_experimental) {
+ if (E->value.experimental_message.is_empty()) {
+ result.experimental_message = TTR("This class may be changed or removed in future versions.");
+ } else {
+ result.experimental_message = HANDLE_DOC(E->value.experimental_message);
+ }
+ }
if (is_native) {
- doc_class_cache[p_class_name] = description;
+ doc_class_cache[p_class_name] = result;
}
}
- return description;
+ return result;
}
-String EditorHelpBit::get_property_description(const StringName &p_class_name, const StringName &p_property_name) const {
- if (!custom_description.is_empty()) {
- return custom_description;
- }
-
+EditorHelpBit::HelpData EditorHelpBit::_get_property_help_data(const StringName &p_class_name, const StringName &p_property_name) {
if (doc_property_cache.has(p_class_name) && doc_property_cache[p_class_name].has(p_property_name)) {
return doc_property_cache[p_class_name][p_property_name];
}
- String description;
+ HelpData result;
const DocTools *dd = EditorHelp::get_doc_data();
const HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name);
@@ -3190,7 +3184,22 @@ String EditorHelpBit::get_property_description(const StringName &p_class_name, c
const bool is_native = !E->value.is_script_doc;
for (const DocData::PropertyDoc &property : E->value.properties) {
- String description_current = is_native ? DTR(property.description) : property.description;
+ HelpData current;
+ current.description = HANDLE_DOC(property.description);
+ if (property.is_deprecated) {
+ if (property.deprecated_message.is_empty()) {
+ current.deprecated_message = TTR("This property may be changed or removed in future versions.");
+ } else {
+ current.deprecated_message = HANDLE_DOC(property.deprecated_message);
+ }
+ }
+ if (property.is_experimental) {
+ if (property.experimental_message.is_empty()) {
+ current.experimental_message = TTR("This property may be changed or removed in future versions.");
+ } else {
+ current.experimental_message = HANDLE_DOC(property.experimental_message);
+ }
+ }
String enum_class_name;
String enum_name;
@@ -3215,18 +3224,19 @@ String EditorHelpBit::get_property_description(const StringName &p_class_name, c
if (constant.enumeration == enum_name && !constant.name.ends_with("_MAX")) {
// Prettify the enum value display, so that "<ENUM_NAME>_<ITEM>" becomes "Item".
const String item_name = EditorPropertyNameProcessor::get_singleton()->process_name(constant.name, EditorPropertyNameProcessor::STYLE_CAPITALIZED).trim_prefix(enum_prefix);
- String item_descr = (is_native ? DTR(constant.description) : constant.description).strip_edges();
+ String item_descr = HANDLE_DOC(constant.description);
if (item_descr.is_empty()) {
- item_descr = ("[i]" + DTR("No description available.") + "[/i]");
+ item_descr = "[color=<EditorHelpBitCommentColor>][i]" + TTR("No description available.") + "[/i][/color]";
}
- description_current += vformat("\n[b]%s:[/b] %s", item_name, item_descr);
+ current.description += vformat("\n[b]%s:[/b] %s", item_name, item_descr);
}
}
+ current.description = current.description.lstrip("\n");
}
}
if (property.name == p_property_name) {
- description = description_current;
+ result = current;
if (!is_native) {
break;
@@ -3234,20 +3244,20 @@ String EditorHelpBit::get_property_description(const StringName &p_class_name, c
}
if (is_native) {
- doc_property_cache[p_class_name][property.name] = description_current;
+ doc_property_cache[p_class_name][property.name] = current;
}
}
}
- return description;
+ return result;
}
-String EditorHelpBit::get_method_description(const StringName &p_class_name, const StringName &p_method_name) const {
+EditorHelpBit::HelpData EditorHelpBit::_get_method_help_data(const StringName &p_class_name, const StringName &p_method_name) {
if (doc_method_cache.has(p_class_name) && doc_method_cache[p_class_name].has(p_method_name)) {
return doc_method_cache[p_class_name][p_method_name];
}
- String description;
+ HelpData result;
const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
if (E) {
@@ -3255,10 +3265,30 @@ String EditorHelpBit::get_method_description(const StringName &p_class_name, con
const bool is_native = !E->value.is_script_doc;
for (const DocData::MethodDoc &method : E->value.methods) {
- String description_current = is_native ? DTR(method.description) : method.description;
+ HelpData current;
+ current.description = HANDLE_DOC(method.description);
+ if (method.is_deprecated) {
+ if (method.deprecated_message.is_empty()) {
+ current.deprecated_message = TTR("This method may be changed or removed in future versions.");
+ } else {
+ current.deprecated_message = HANDLE_DOC(method.deprecated_message);
+ }
+ }
+ if (method.is_experimental) {
+ if (method.experimental_message.is_empty()) {
+ current.experimental_message = TTR("This method may be changed or removed in future versions.");
+ } else {
+ current.experimental_message = HANDLE_DOC(method.experimental_message);
+ }
+ }
+ current.doc_type = { method.return_type, method.return_enum, method.return_is_bitfield };
+ for (const DocData::ArgumentDoc &argument : method.arguments) {
+ const DocType argument_type = { argument.type, argument.enumeration, argument.is_bitfield };
+ current.arguments.push_back({ argument.name, argument_type, argument.default_value });
+ }
if (method.name == p_method_name) {
- description = description_current;
+ result = current;
if (!is_native) {
break;
@@ -3266,20 +3296,20 @@ String EditorHelpBit::get_method_description(const StringName &p_class_name, con
}
if (is_native) {
- doc_method_cache[p_class_name][method.name] = description_current;
+ doc_method_cache[p_class_name][method.name] = current;
}
}
}
- return description;
+ return result;
}
-String EditorHelpBit::get_signal_description(const StringName &p_class_name, const StringName &p_signal_name) const {
+EditorHelpBit::HelpData EditorHelpBit::_get_signal_help_data(const StringName &p_class_name, const StringName &p_signal_name) {
if (doc_signal_cache.has(p_class_name) && doc_signal_cache[p_class_name].has(p_signal_name)) {
return doc_signal_cache[p_class_name][p_signal_name];
}
- String description;
+ HelpData result;
const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name);
if (E) {
@@ -3287,10 +3317,29 @@ String EditorHelpBit::get_signal_description(const StringName &p_class_name, con
const bool is_native = !E->value.is_script_doc;
for (const DocData::MethodDoc &signal : E->value.signals) {
- String description_current = is_native ? DTR(signal.description) : signal.description;
+ HelpData current;
+ current.description = HANDLE_DOC(signal.description);
+ if (signal.is_deprecated) {
+ if (signal.deprecated_message.is_empty()) {
+ current.deprecated_message = TTR("This signal may be changed or removed in future versions.");
+ } else {
+ current.deprecated_message = HANDLE_DOC(signal.deprecated_message);
+ }
+ }
+ if (signal.is_experimental) {
+ if (signal.experimental_message.is_empty()) {
+ current.experimental_message = TTR("This signal may be changed or removed in future versions.");
+ } else {
+ current.experimental_message = HANDLE_DOC(signal.experimental_message);
+ }
+ }
+ for (const DocData::ArgumentDoc &argument : signal.arguments) {
+ const DocType argument_type = { argument.type, argument.enumeration, argument.is_bitfield };
+ current.arguments.push_back({ argument.name, argument_type, argument.default_value });
+ }
if (signal.name == p_signal_name) {
- description = description_current;
+ result = current;
if (!is_native) {
break;
@@ -3298,20 +3347,20 @@ String EditorHelpBit::get_signal_description(const StringName &p_class_name, con
}
if (is_native) {
- doc_signal_cache[p_class_name][signal.name] = description_current;
+ doc_signal_cache[p_class_name][signal.name] = current;
}
}
}
- return description;
+ return result;
}
-String EditorHelpBit::get_theme_item_description(const StringName &p_class_name, const StringName &p_theme_item_name) const {
+EditorHelpBit::HelpData EditorHelpBit::_get_theme_item_help_data(const StringName &p_class_name, const StringName &p_theme_item_name) {
if (doc_theme_item_cache.has(p_class_name) && doc_theme_item_cache[p_class_name].has(p_theme_item_name)) {
return doc_theme_item_cache[p_class_name][p_theme_item_name];
}
- String description;
+ HelpData result;
bool found = false;
const DocTools *dd = EditorHelp::get_doc_data();
@@ -3321,142 +3370,477 @@ String EditorHelpBit::get_theme_item_description(const StringName &p_class_name,
const bool is_native = !E->value.is_script_doc;
for (const DocData::ThemeItemDoc &theme_item : E->value.theme_properties) {
- String description_current = is_native ? DTR(theme_item.description) : theme_item.description;
+ HelpData current;
+ current.description = HANDLE_DOC(theme_item.description);
if (theme_item.name == p_theme_item_name) {
- description = description_current;
+ result = current;
found = true;
-
if (!is_native) {
break;
}
}
if (is_native) {
- doc_theme_item_cache[p_class_name][theme_item.name] = description_current;
+ doc_theme_item_cache[p_class_name][theme_item.name] = current;
}
}
if (found || E->value.inherits.is_empty()) {
break;
}
+
// Check for inherited theme items.
E = dd->class_list.find(E->value.inherits);
}
- return description;
+ return result;
+}
+
+#undef HANDLE_DOC
+
+void EditorHelpBit::_add_type_to_title(const DocType &p_doc_type) {
+ _add_type_to_rt(p_doc_type.type, p_doc_type.enumeration, p_doc_type.is_bitfield, title, this, symbol_class_name);
+}
+
+void EditorHelpBit::_update_labels() {
+ const Ref<Font> doc_bold_font = get_theme_font(SNAME("doc_bold"), EditorStringName(EditorFonts));
+
+ if (!symbol_visible_type.is_empty() || !symbol_name.is_empty()) {
+ title->clear();
+
+ title->push_font(doc_bold_font);
+
+ if (!symbol_visible_type.is_empty()) {
+ title->push_color(get_theme_color(SNAME("title_color"), SNAME("EditorHelp")));
+ title->add_text(symbol_visible_type);
+ title->pop(); // color
+ }
+
+ if (!symbol_visible_type.is_empty() && !symbol_name.is_empty()) {
+ title->add_text(" ");
+ }
+
+ if (!symbol_name.is_empty()) {
+ title->push_underline();
+ title->add_text(symbol_name);
+ title->pop(); // underline
+ }
+
+ title->pop(); // font
+
+ if (symbol_type == "method" || symbol_type == "signal") {
+ const Color symbol_color = get_theme_color(SNAME("symbol_color"), SNAME("EditorHelp"));
+ const Color value_color = get_theme_color(SNAME("value_color"), SNAME("EditorHelp"));
+
+ title->push_font(get_theme_font(SNAME("doc_source"), EditorStringName(EditorFonts)));
+ title->push_font_size(get_theme_font_size(SNAME("doc_source_size"), EditorStringName(EditorFonts)) * 0.9);
+
+ title->push_color(symbol_color);
+ title->add_text("(");
+ title->pop(); // color
+
+ for (int i = 0; i < help_data.arguments.size(); i++) {
+ const ArgumentData &argument = help_data.arguments[i];
+
+ if (i > 0) {
+ title->push_color(symbol_color);
+ title->add_text(", ");
+ title->pop(); // color
+ }
+
+ title->add_text(argument.name);
+
+ title->push_color(symbol_color);
+ title->add_text(": ");
+ title->pop(); // color
+
+ _add_type_to_title(argument.doc_type);
+
+ if (!argument.default_value.is_empty()) {
+ title->push_color(symbol_color);
+ title->add_text(" = ");
+ title->pop(); // color
+
+ title->push_color(value_color);
+ title->add_text(argument.default_value);
+ title->pop(); // color
+ }
+ }
+
+ title->push_color(symbol_color);
+ title->add_text(")");
+ title->pop(); // color
+
+ if (symbol_type == "method") {
+ title->push_color(symbol_color);
+ title->add_text(" -> ");
+ title->pop(); // color
+
+ _add_type_to_title(help_data.doc_type);
+ }
+
+ title->pop(); // font_size
+ title->pop(); // font
+ }
+
+ title->show();
+ } else {
+ title->hide();
+ }
+
+ content->clear();
+
+ bool has_prev_text = false;
+
+ if (!help_data.deprecated_message.is_empty()) {
+ has_prev_text = true;
+
+ Ref<Texture2D> error_icon = get_editor_theme_icon(SNAME("StatusError"));
+ content->add_image(error_icon, error_icon->get_width(), error_icon->get_height());
+ content->add_text(" ");
+ content->push_color(get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
+ content->push_font(doc_bold_font);
+ content->add_text(TTR("Deprecated:"));
+ content->pop(); // font
+ content->pop(); // color
+ content->add_text(" ");
+ _add_text_to_rt(help_data.deprecated_message, content, this, symbol_class_name);
+ }
+
+ if (!help_data.experimental_message.is_empty()) {
+ if (has_prev_text) {
+ content->add_newline();
+ content->add_newline();
+ }
+ has_prev_text = true;
+
+ Ref<Texture2D> warning_icon = get_editor_theme_icon(SNAME("NodeWarning"));
+ content->add_image(warning_icon, warning_icon->get_width(), warning_icon->get_height());
+ content->add_text(" ");
+ content->push_color(get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
+ content->push_font(doc_bold_font);
+ content->add_text(TTR("Experimental:"));
+ content->pop(); // font
+ content->pop(); // color
+ content->add_text(" ");
+ _add_text_to_rt(help_data.experimental_message, content, this, symbol_class_name);
+ }
+
+ if (!help_data.description.is_empty()) {
+ if (has_prev_text) {
+ content->add_newline();
+ content->add_newline();
+ }
+ has_prev_text = true;
+
+ const Color comment_color = get_theme_color(SNAME("comment_color"), SNAME("EditorHelp"));
+ _add_text_to_rt(help_data.description.replace("<EditorHelpBitCommentColor>", comment_color.to_html()), content, this, symbol_class_name);
+ }
+
+ if (is_inside_tree()) {
+ update_content_height();
+ }
+}
+
+void EditorHelpBit::_go_to_help(const String &p_what) {
+ EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT);
+ ScriptEditor::get_singleton()->goto_help(p_what);
+ emit_signal(SNAME("request_hide"));
+}
+
+void EditorHelpBit::_meta_clicked(const String &p_select) {
+ if (p_select.begins_with("$")) { // Enum.
+ const String link = p_select.substr(1);
+
+ String enum_class_name;
+ String enum_name;
+ if (CoreConstants::is_global_enum(link)) {
+ enum_class_name = "@GlobalScope";
+ enum_name = link;
+ } else {
+ const int dot_pos = link.rfind(".");
+ if (dot_pos >= 0) {
+ enum_class_name = link.left(dot_pos);
+ enum_name = link.substr(dot_pos + 1);
+ } else {
+ enum_class_name = symbol_class_name;
+ enum_name = link;
+ }
+ }
+
+ _go_to_help("class_enum:" + enum_class_name + ":" + enum_name);
+ } else if (p_select.begins_with("#")) { // Class.
+ _go_to_help("class_name:" + p_select.substr(1));
+ } else if (p_select.begins_with("@")) { // Member.
+ const int tag_end = p_select.find_char(' ');
+ const String tag = p_select.substr(1, tag_end - 1);
+ const String link = p_select.substr(tag_end + 1).lstrip(" ");
+
+ String topic;
+ if (tag == "method") {
+ topic = "class_method";
+ } else if (tag == "constructor") {
+ topic = "class_method";
+ } else if (tag == "operator") {
+ topic = "class_method";
+ } else if (tag == "member") {
+ topic = "class_property";
+ } else if (tag == "enum") {
+ topic = "class_enum";
+ } else if (tag == "signal") {
+ topic = "class_signal";
+ } else if (tag == "constant") {
+ topic = "class_constant";
+ } else if (tag == "annotation") {
+ topic = "class_annotation";
+ } else if (tag == "theme_item") {
+ topic = "class_theme_item";
+ } else {
+ return;
+ }
+
+ if (link.contains(".")) {
+ const int class_end = link.find_char('.');
+ _go_to_help(topic + ":" + link.left(class_end) + ":" + link.substr(class_end + 1));
+ } else {
+ _go_to_help(topic + ":" + symbol_class_name + ":" + link);
+ }
+ } else if (p_select.begins_with("http:") || p_select.begins_with("https:")) {
+ OS::get_singleton()->shell_open(p_select);
+ } else if (p_select.begins_with("^")) { // Copy button.
+ DisplayServer::get_singleton()->clipboard_set(p_select.substr(1));
+ }
}
void EditorHelpBit::_bind_methods() {
- ClassDB::bind_method(D_METHOD("set_text", "text"), &EditorHelpBit::set_text);
ADD_SIGNAL(MethodInfo("request_hide"));
}
void EditorHelpBit::_notification(int p_what) {
switch (p_what) {
- case NOTIFICATION_THEME_CHANGED: {
- rich_text->add_theme_color_override("selection_color", get_theme_color(SNAME("selection_color"), SNAME("EditorHelp")));
- rich_text->clear();
- _add_text_to_rt(text, rich_text, this, doc_class_name);
- rich_text->reset_size(); // Force recalculating size after parsing bbcode.
- } break;
+ case NOTIFICATION_THEME_CHANGED:
+ _update_labels();
+ break;
}
}
-void EditorHelpBit::set_text(const String &p_text) {
- text = p_text;
- rich_text->clear();
- _add_text_to_rt(text, rich_text, this, doc_class_name);
+void EditorHelpBit::parse_symbol(const String &p_symbol) {
+ const PackedStringArray slices = p_symbol.split("|", true, 2);
+ ERR_FAIL_COND_MSG(slices.size() < 3, "Invalid doc id. The expected format is 'item_type|class_name|item_name'.");
+
+ const String &item_type = slices[0];
+ const String &class_name = slices[1];
+ const String &item_name = slices[2];
+
+ String visible_type;
+ String name = item_name;
+
+ if (item_type == "class") {
+ visible_type = TTR("Class:");
+ name = class_name;
+ help_data = _get_class_help_data(class_name);
+ } else if (item_type == "property") {
+ if (name.begins_with("metadata/")) {
+ visible_type = TTR("Metadata:");
+ name = name.trim_prefix("metadata/");
+ } else if (class_name == "ProjectSettings" || class_name == "EditorSettings") {
+ visible_type = TTR("Setting:");
+ } else {
+ visible_type = TTR("Property:");
+ }
+ help_data = _get_property_help_data(class_name, item_name);
+ } else if (item_type == "internal_property") {
+ visible_type = TTR("Internal Property:");
+ help_data = HelpData();
+ help_data.description = "[color=<EditorHelpBitCommentColor>][i]" + TTR("This property can only be set in the Inspector.") + "[/i][/color]";
+ } else if (item_type == "method") {
+ visible_type = TTR("Method:");
+ help_data = _get_method_help_data(class_name, item_name);
+ } else if (item_type == "signal") {
+ visible_type = TTR("Signal:");
+ help_data = _get_signal_help_data(class_name, item_name);
+ } else if (item_type == "theme_item") {
+ visible_type = TTR("Theme Property:");
+ help_data = _get_theme_item_help_data(class_name, item_name);
+ } else {
+ ERR_FAIL_MSG("Invalid tooltip type '" + item_type + "'. Valid types are 'class', 'property', 'internal_property', 'method', 'signal', and 'theme_item'.");
+ }
+
+ symbol_class_name = class_name;
+ symbol_type = item_type;
+ symbol_visible_type = visible_type;
+ symbol_name = name;
+
+ if (help_data.description.is_empty()) {
+ help_data.description = "[color=<EditorHelpBitCommentColor>][i]" + TTR("No description available.") + "[/i][/color]";
+ }
+
+ if (is_inside_tree()) {
+ _update_labels();
+ }
}
-EditorHelpBit::EditorHelpBit() {
- rich_text = memnew(RichTextLabel);
- add_child(rich_text);
- rich_text->connect("meta_clicked", callable_mp(this, &EditorHelpBit::_meta_clicked));
- rich_text->set_fit_content(true);
- set_custom_minimum_size(Size2(0, 50 * EDSCALE));
+void EditorHelpBit::set_custom_text(const String &p_type, const String &p_name, const String &p_description) {
+ symbol_class_name = String();
+ symbol_type = String();
+ symbol_visible_type = p_type;
+ symbol_name = p_name;
+
+ help_data = HelpData();
+ help_data.description = p_description;
+
+ if (is_inside_tree()) {
+ _update_labels();
+ }
}
-/// EditorHelpTooltip ///
+void EditorHelpBit::prepend_description(const String &p_text) {
+ if (help_data.description.is_empty()) {
+ help_data.description = p_text;
+ } else {
+ help_data.description = p_text + "\n" + help_data.description;
+ }
-void EditorHelpTooltip::_notification(int p_what) {
- switch (p_what) {
- case NOTIFICATION_POSTINITIALIZE: {
- if (!tooltip_text.is_empty()) {
- parse_tooltip(tooltip_text);
- }
- } break;
+ if (is_inside_tree()) {
+ _update_labels();
}
}
-// `p_text` is expected to be something like these:
-// - `class|Control||`;
-// - `property|Control|size|`;
-// - `signal|Control|gui_input|(event: InputEvent)`.
-void EditorHelpTooltip::parse_tooltip(const String &p_text) {
- tooltip_text = p_text;
+void EditorHelpBit::set_content_height_limits(float p_min, float p_max) {
+ ERR_FAIL_COND(p_min > p_max);
+ content_min_height = p_min;
+ content_max_height = p_max;
- PackedStringArray slices = p_text.split("|", true, 3);
- ERR_FAIL_COND_MSG(slices.size() < 4, "Invalid tooltip formatting. The expect string should be formatted as 'type|class|property|args'.");
+ if (is_inside_tree()) {
+ update_content_height();
+ }
+}
- const String &type = slices[0];
- const String &class_name = slices[1];
- const String &property_name = slices[2];
- const String &property_args = slices[3];
+void EditorHelpBit::update_content_height() {
+ float content_height = content->get_content_height();
+ const Ref<StyleBox> style = content->get_theme_stylebox("normal");
+ if (style.is_valid()) {
+ content_height += style->get_content_margin(SIDE_TOP) + style->get_content_margin(SIDE_BOTTOM);
+ }
+ content->set_custom_minimum_size(Size2(content->get_custom_minimum_size().x, CLAMP(content_height, content_min_height, content_max_height)));
+}
- doc_class_name = class_name;
+EditorHelpBit::EditorHelpBit(const String &p_symbol) {
+ add_theme_constant_override("separation", 0);
+
+ title = memnew(RichTextLabel);
+ title->set_theme_type_variation("EditorHelpBitTitle");
+ title->set_fit_content(true);
+ title->set_selection_enabled(true);
+ //title->set_context_menu_enabled(true); // TODO: Fix opening context menu hides tooltip.
+ title->connect("meta_clicked", callable_mp(this, &EditorHelpBit::_meta_clicked));
+ title->hide();
+ add_child(title);
+
+ content_min_height = 48 * EDSCALE;
+ content_max_height = 360 * EDSCALE;
+
+ content = memnew(RichTextLabel);
+ content->set_theme_type_variation("EditorHelpBitContent");
+ content->set_custom_minimum_size(Size2(512 * EDSCALE, content_min_height));
+ content->set_selection_enabled(true);
+ //content->set_context_menu_enabled(true); // TODO: Fix opening context menu hides tooltip.
+ content->connect("meta_clicked", callable_mp(this, &EditorHelpBit::_meta_clicked));
+ add_child(content);
+
+ if (!p_symbol.is_empty()) {
+ parse_symbol(p_symbol);
+ }
+}
- String formatted_text;
+/// EditorHelpBitTooltip ///
- // Exclude internal properties, they are not documented.
- if (type == "internal_property") {
- formatted_text = "[i]" + TTR("This property can only be set in the Inspector.") + "[/i]";
- set_text(formatted_text);
- return;
+void EditorHelpBitTooltip::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_WM_MOUSE_ENTER:
+ timer->stop();
+ break;
+ case NOTIFICATION_WM_MOUSE_EXIT:
+ timer->start();
+ break;
}
+}
- String title;
- String description;
+// Forwards non-mouse input to the parent viewport.
+void EditorHelpBitTooltip::_input_from_window(const Ref<InputEvent> &p_event) {
+ if (p_event->is_action_pressed(SNAME("ui_cancel"), false, true)) {
+ hide(); // Will be deleted on its timer.
+ } else {
+ const Ref<InputEventMouse> mouse_event = p_event;
+ if (mouse_event.is_null()) {
+ get_parent_viewport()->push_input(p_event);
+ }
+ }
+}
+
+void EditorHelpBitTooltip::show_tooltip(EditorHelpBit *p_help_bit, Control *p_target) {
+ ERR_FAIL_NULL(p_help_bit);
+ EditorHelpBitTooltip *tooltip = memnew(EditorHelpBitTooltip(p_target));
+ p_help_bit->connect("request_hide", callable_mp(static_cast<Window *>(tooltip), &Window::hide)); // Will be deleted on its timer.
+ tooltip->add_child(p_help_bit);
+ p_target->get_viewport()->add_child(tooltip);
+ p_help_bit->update_content_height();
+ tooltip->popup_under_cursor();
+}
- if (type == "class") {
- title = class_name;
- description = get_class_description(class_name);
- formatted_text = TTR("Class:");
+// Copy-paste from `Viewport::_gui_show_tooltip()`.
+void EditorHelpBitTooltip::popup_under_cursor() {
+ Point2 mouse_pos = get_mouse_position();
+ Point2 tooltip_offset = GLOBAL_GET("display/mouse_cursor/tooltip_position_offset");
+ Rect2 r(mouse_pos + tooltip_offset, get_contents_minimum_size());
+ r.size = r.size.min(get_max_size());
+
+ Window *window = get_parent_visible_window();
+ Rect2i vr;
+ if (is_embedded()) {
+ vr = get_embedder()->get_visible_rect();
} else {
- title = property_name;
+ vr = window->get_usable_parent_rect();
+ }
- if (type == "property") {
- description = get_property_description(class_name, property_name);
- if (property_name.begins_with("metadata/")) {
- formatted_text = TTR("Metadata:");
- } else {
- formatted_text = TTR("Property:");
- }
- } else if (type == "method") {
- description = get_method_description(class_name, property_name);
- formatted_text = TTR("Method:");
- } else if (type == "signal") {
- description = get_signal_description(class_name, property_name);
- formatted_text = TTR("Signal:");
- } else if (type == "theme_item") {
- description = get_theme_item_description(class_name, property_name);
- formatted_text = TTR("Theme Property:");
- } else {
- ERR_FAIL_MSG("Invalid tooltip type '" + type + "'. Valid types are 'class', 'property', 'method', 'signal', and 'theme_item'.");
+ if (r.size.x + r.position.x > vr.size.x + vr.position.x) {
+ // Place it in the opposite direction. If it fails, just hug the border.
+ r.position.x = mouse_pos.x - r.size.x - tooltip_offset.x;
+
+ if (r.position.x < vr.position.x) {
+ r.position.x = vr.position.x + vr.size.x - r.size.x;
+ }
+ } else if (r.position.x < vr.position.x) {
+ r.position.x = vr.position.x;
+ }
+
+ if (r.size.y + r.position.y > vr.size.y + vr.position.y) {
+ // Same as above.
+ r.position.y = mouse_pos.y - r.size.y - tooltip_offset.y;
+
+ if (r.position.y < vr.position.y) {
+ r.position.y = vr.position.y + vr.size.y - r.size.y;
}
+ } else if (r.position.y < vr.position.y) {
+ r.position.y = vr.position.y;
}
- // Metadata special handling replaces "Property:" with "Metadata": above.
- formatted_text += " [u][b]" + title.trim_prefix("metadata/") + "[/b][/u]" + property_args.replace("[", "[lb]") + "\n";
- formatted_text += description.is_empty() ? "[i]" + TTR("No description available.") + "[/i]" : description;
- set_text(formatted_text);
+ set_flag(Window::FLAG_NO_FOCUS, true);
+ popup(r);
}
-EditorHelpTooltip::EditorHelpTooltip(const String &p_text, const String &p_custom_description) {
- tooltip_text = p_text;
- custom_description = p_custom_description;
+EditorHelpBitTooltip::EditorHelpBitTooltip(Control *p_target) {
+ set_theme_type_variation("TooltipPanel");
+
+ timer = memnew(Timer);
+ timer->set_wait_time(0.2);
+ timer->connect("timeout", callable_mp(static_cast<Node *>(this), &Node::queue_free));
+ add_child(timer);
- get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 0));
+ ERR_FAIL_NULL(p_target);
+ p_target->connect("mouse_entered", callable_mp(timer, &Timer::stop));
+ p_target->connect("mouse_exited", callable_mp(timer, &Timer::start).bind(-1));
}
#if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED)
diff --git a/editor/editor_help.h b/editor/editor_help.h
index f8686b964a..078b42b439 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<StringName, String> doc_class_cache;
- inline static HashMap<StringName, HashMap<StringName, String>> doc_property_cache;
- inline static HashMap<StringName, HashMap<StringName, String>> doc_method_cache;
- inline static HashMap<StringName, HashMap<StringName, String>> doc_signal_cache;
- inline static HashMap<StringName, HashMap<StringName, String>> doc_theme_item_cache;
+ struct DocType {
+ String type;
+ String enumeration;
+ bool is_bitfield = false;
+ };
+
+ struct ArgumentData {
+ String name;
+ DocType doc_type;
+ String default_value;
+ };
+
+ struct HelpData {
+ String description;
+ String deprecated_message;
+ String experimental_message;
+ DocType doc_type; // For method return type.
+ Vector<ArgumentData> arguments; // For methods and signals.
+ };
+
+ inline static HashMap<StringName, HelpData> doc_class_cache;
+ inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_property_cache;
+ inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_method_cache;
+ inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_signal_cache;
+ inline static HashMap<StringName, HashMap<StringName, HelpData>> doc_theme_item_cache;
+
+ RichTextLabel *title = nullptr;
+ RichTextLabel *content = nullptr;
- RichTextLabel *rich_text = nullptr;
+ String symbol_class_name;
+ String symbol_type;
+ String symbol_visible_type;
+ String symbol_name;
+
+ HelpData help_data;
+
+ float content_min_height = 0.0;
+ float content_max_height = 0.0;
+
+ static HelpData _get_class_help_data(const StringName &p_class_name);
+ static HelpData _get_property_help_data(const StringName &p_class_name, const StringName &p_property_name);
+ static HelpData _get_method_help_data(const StringName &p_class_name, const StringName &p_method_name);
+ static HelpData _get_signal_help_data(const StringName &p_class_name, const StringName &p_signal_name);
+ static HelpData _get_theme_item_help_data(const StringName &p_class_name, const StringName &p_theme_item_name);
+
+ void _add_type_to_title(const DocType &p_doc_type);
+ void _update_labels();
void _go_to_help(const String &p_what);
void _meta_clicked(const String &p_select);
- String text;
-
protected:
- String doc_class_name;
- String custom_description;
-
static void _bind_methods();
void _notification(int p_what);
public:
- String get_class_description(const StringName &p_class_name) const;
- String get_property_description(const StringName &p_class_name, const StringName &p_property_name) const;
- String get_method_description(const StringName &p_class_name, const StringName &p_method_name) const;
- String get_signal_description(const StringName &p_class_name, const StringName &p_signal_name) const;
- String get_theme_item_description(const StringName &p_class_name, const StringName &p_theme_item_name) const;
+ void parse_symbol(const String &p_symbol);
+ void set_custom_text(const String &p_type, const String &p_name, const String &p_description);
+ void prepend_description(const String &p_text);
- RichTextLabel *get_rich_text() { return rich_text; }
- void set_text(const String &p_text);
+ void set_content_height_limits(float p_min, float p_max);
+ void update_content_height();
- EditorHelpBit();
+ EditorHelpBit(const String &p_symbol = String());
};
-class EditorHelpTooltip : public EditorHelpBit {
- GDCLASS(EditorHelpTooltip, EditorHelpBit);
+// Standard tooltips do not allow you to hover over them.
+// This class is intended as a temporary workaround.
+class EditorHelpBitTooltip : public PopupPanel {
+ GDCLASS(EditorHelpBitTooltip, PopupPanel);
- String tooltip_text;
+ Timer *timer = nullptr;
protected:
void _notification(int p_what);
+ virtual void _input_from_window(const Ref<InputEvent> &p_event) override;
public:
- void parse_tooltip(const String &p_text);
+ static void show_tooltip(EditorHelpBit *p_help_bit, Control *p_target);
+
+ void popup_under_cursor();
- EditorHelpTooltip(const String &p_text = String(), const String &p_custom_description = String());
+ EditorHelpBitTooltip(Control *p_target);
};
#if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED)
diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp
index aed1462eb6..50cc89c618 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;
+ String custom_warning;
+ if (object->has_method("_get_property_warning")) {
+ custom_warning = object->call("_get_property_warning", property);
+ }
- if (has_doc_tooltip) {
- String custom_description;
+ if (has_doc_tooltip || !custom_warning.is_empty()) {
+ EditorHelpBit *help_bit = memnew(EditorHelpBit);
- const EditorInspector *inspector = get_parent_inspector();
- if (inspector) {
- custom_description = inspector->get_custom_property_description(p_text);
- }
- tooltip = memnew(EditorHelpTooltip(p_text, custom_description));
- }
+ if (has_doc_tooltip) {
+ help_bit->parse_symbol(p_text);
- if (object->has_method("_get_property_warning")) {
- String warn = object->call("_get_property_warning", property);
- if (!warn.is_empty()) {
- String prev_text;
- if (tooltip == nullptr) {
- tooltip = memnew(EditorHelpBit());
- tooltip->set_text(p_text);
- tooltip->get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 0));
- } else {
- prev_text = tooltip->get_rich_text()->get_text() + "\n";
+ const EditorInspector *inspector = get_parent_inspector();
+ if (inspector) {
+ const String custom_description = inspector->get_custom_property_description(p_text);
+ if (!custom_description.is_empty()) {
+ help_bit->prepend_description(custom_description);
+ }
}
- tooltip->set_text(prev_text + "[b][color=" + get_theme_color(SNAME("warning_color")).to_html(false) + "]" + warn + "[/color][/b]");
}
+
+ if (!custom_warning.is_empty()) {
+ help_bit->prepend_description("[b][color=" + get_theme_color(SNAME("warning_color")).to_html(false) + "]" + custom_warning + "[/color][/b]");
+ }
+
+ EditorHelpBitTooltip::show_tooltip(help_bit, const_cast<EditorProperty *>(this));
+ return memnew(Control); // Make the standard tooltip invisible.
}
- return tooltip;
+ return nullptr;
}
void EditorProperty::menu_option(int p_option) {
@@ -1178,7 +1180,14 @@ void EditorInspectorCategory::_notification(int p_what) {
}
Control *EditorInspectorCategory::make_custom_tooltip(const String &p_text) const {
- return doc_class_name.is_empty() ? nullptr : memnew(EditorHelpTooltip(p_text));
+ // If it's not a doc tooltip, fallback to the default one.
+ if (doc_class_name.is_empty()) {
+ return nullptr;
+ }
+
+ EditorHelpBit *help_bit = memnew(EditorHelpBit(p_text));
+ EditorHelpBitTooltip::show_tooltip(help_bit, const_cast<EditorInspectorCategory *>(this));
+ return memnew(Control); // Make the standard tooltip invisible.
}
Size2 EditorInspectorCategory::get_minimum_size() const {
@@ -2887,8 +2896,8 @@ void EditorInspector::update_tree() {
category->doc_class_name = doc_name;
if (use_doc_hints) {
- // `|` separator used in `EditorHelpTooltip` for formatting.
- category->set_tooltip_text("class|" + doc_name + "||");
+ // `|` separators used in `EditorHelpBit`.
+ category->set_tooltip_text("class|" + doc_name + "|");
}
}
@@ -3368,15 +3377,15 @@ void EditorInspector::update_tree() {
ep->connect("object_id_selected", callable_mp(this, &EditorInspector::_object_id_selected), CONNECT_DEFERRED);
if (use_doc_hints) {
- // `|` separator used in `EditorHelpTooltip` for formatting.
+ // `|` separators used in `EditorHelpBit`.
if (theme_item_name.is_empty()) {
if (p.usage & PROPERTY_USAGE_INTERNAL) {
- ep->set_tooltip_text("internal_property|" + classname + "|" + property_prefix + p.name + "|");
+ ep->set_tooltip_text("internal_property|" + classname + "|" + property_prefix + p.name);
} else {
- ep->set_tooltip_text("property|" + classname + "|" + property_prefix + p.name + "|");
+ ep->set_tooltip_text("property|" + classname + "|" + property_prefix + p.name);
}
} else {
- ep->set_tooltip_text("theme_item|" + classname + "|" + theme_item_name + "|");
+ ep->set_tooltip_text("theme_item|" + classname + "|" + theme_item_name);
}
ep->has_doc_tooltip = true;
}
diff --git a/editor/editor_inspector.h b/editor/editor_inspector.h
index eff4f9caa5..cf3bf89e09 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 c59c221e02..da072744b8 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 dae0fc52a6..b00f85c93c 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/editor_quick_open.cpp b/editor/editor_quick_open.cpp
index 6fd1fd687b..f8df0f9fa8 100644
--- a/editor/editor_quick_open.cpp
+++ b/editor/editor_quick_open.cpp
@@ -91,17 +91,21 @@ void EditorQuickOpen::_build_search_cache(EditorFileSystemDirectory *p_efsd) {
}
void EditorQuickOpen::_update_search() {
- const String search_text = search_box->get_text();
- const bool empty_search = search_text.is_empty();
+ const PackedStringArray search_tokens = search_box->get_text().to_lower().replace("/", " ").split(" ", false);
+ const bool empty_search = search_tokens.is_empty();
// Filter possible candidates.
Vector<Entry> entries;
for (int i = 0; i < files.size(); i++) {
- if (empty_search || search_text.is_subsequence_ofn(files[i])) {
- Entry r;
- r.path = files[i];
- r.score = empty_search ? 0 : _score_path(search_text, files[i].to_lower());
+ Entry r;
+ r.path = files[i];
+ if (empty_search) {
entries.push_back(r);
+ } else {
+ r.score = _score_search_result(search_tokens, r.path.to_lower());
+ if (r.score > 0) {
+ entries.push_back(r);
+ }
}
}
@@ -135,23 +139,42 @@ void EditorQuickOpen::_update_search() {
}
}
-float EditorQuickOpen::_score_path(const String &p_search, const String &p_path) {
- float score = 0.9f + .1f * (p_search.length() / (float)p_path.length());
+float EditorQuickOpen::_score_search_result(const PackedStringArray &p_search_tokens, const String &p_path) {
+ float score = 0.0f;
+ int prev_min_match_idx = -1;
- // Exact match.
- if (p_search == p_path) {
- return 1.2f;
- }
+ for (const String &s : p_search_tokens) {
+ int min_match_idx = p_path.find(s);
+
+ if (min_match_idx == -1) {
+ return 0.0f;
+ }
+
+ float token_score = s.length();
+
+ int max_match_idx = p_path.rfind(s);
+
+ // Prioritize the actual file name over folder.
+ if (max_match_idx > p_path.rfind("/")) {
+ token_score *= 2.0f;
+ }
+
+ // Prioritize matches at the front of the path token.
+ if (min_match_idx == 0 || p_path.find("/" + s) != -1) {
+ token_score += 1.0f;
+ }
+
+ score += token_score;
+
+ // Prioritize tokens which appear in order.
+ if (prev_min_match_idx != -1 && max_match_idx > prev_min_match_idx) {
+ score += 1.0f;
+ }
- // Positive bias for matches close to the beginning of the file name.
- String file = p_path.get_file();
- int pos = file.findn(p_search);
- if (pos != -1) {
- return score * (1.0f - 0.1f * (float(pos) / file.length()));
+ prev_min_match_idx = min_match_idx;
}
- // Similarity
- return p_path.to_lower().similarity(p_search.to_lower());
+ return score;
}
void EditorQuickOpen::_confirmed() {
diff --git a/editor/editor_quick_open.h b/editor/editor_quick_open.h
index ec8ce0175e..bbc689040a 100644
--- a/editor/editor_quick_open.h
+++ b/editor/editor_quick_open.h
@@ -63,7 +63,7 @@ class EditorQuickOpen : public ConfirmationDialog {
void _update_search();
void _build_search_cache(EditorFileSystemDirectory *p_efsd);
- float _score_path(const String &p_search, const String &p_path);
+ float _score_search_result(const PackedStringArray &p_search_tokens, const String &p_path);
void _confirmed();
virtual void cancel_pressed() override;
diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp
index 452715a577..ca6be130f9 100644
--- a/editor/editor_settings.cpp
+++ b/editor/editor_settings.cpp
@@ -690,6 +690,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
EDITOR_SETTING_USAGE(Variant::COLOR, PROPERTY_HINT_NONE, "editors/3d_gizmos/gizmo_colors/instantiated", Color(0.7, 0.7, 0.7, 0.6), "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED)
EDITOR_SETTING_USAGE(Variant::COLOR, PROPERTY_HINT_NONE, "editors/3d_gizmos/gizmo_colors/joint", Color(0.5, 0.8, 1), "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED)
EDITOR_SETTING_USAGE(Variant::COLOR, PROPERTY_HINT_NONE, "editors/3d_gizmos/gizmo_colors/shape", Color(0.5, 0.7, 1), "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED)
+ EDITOR_SETTING_USAGE(Variant::COLOR, PROPERTY_HINT_NONE, "editors/3d_gizmos/gizmo_colors/aabb", Color(0.28, 0.8, 0.82), "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED)
// If a line is a multiple of this, it uses the primary grid color.
// Use a power of 2 value by default as it's more common to use powers of 2 in level design.
diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp
index 34fd0363b1..b7deb6afa6 100644
--- a/editor/filesystem_dock.cpp
+++ b/editor/filesystem_dock.cpp
@@ -265,7 +265,7 @@ bool FileSystemDock::_create_tree(TreeItem *p_parent, EditorFileSystemDirectory
} else {
subdirectory_item->set_collapsed(uncollapsed_paths.find(lpath) < 0);
}
- if (searched_string.length() > 0 && dname.to_lower().find(searched_string) >= 0) {
+ if (!searched_tokens.is_empty() && _matches_all_search_tokens(dname)) {
parent_should_expand = true;
}
@@ -291,8 +291,8 @@ bool FileSystemDock::_create_tree(TreeItem *p_parent, EditorFileSystemDirectory
}
String file_name = p_dir->get_file(i);
- if (searched_string.length() > 0) {
- if (file_name.to_lower().find(searched_string) < 0) {
+ if (!searched_tokens.is_empty()) {
+ if (!_matches_all_search_tokens(file_name)) {
// The searched string is not in the file name, we skip it.
continue;
} else {
@@ -350,7 +350,7 @@ bool FileSystemDock::_create_tree(TreeItem *p_parent, EditorFileSystemDirectory
}
}
- if (searched_string.length() > 0) {
+ if (!searched_tokens.is_empty()) {
if (parent_should_expand) {
subdirectory_item->set_collapsed(false);
} else if (dname != "res://") {
@@ -458,7 +458,7 @@ void FileSystemDock::_update_tree(const Vector<String> &p_uncollapsed_paths, boo
color = Color(1, 1, 1);
}
- if (searched_string.length() == 0 || text.to_lower().find(searched_string) >= 0) {
+ if (searched_tokens.is_empty() || _matches_all_search_tokens(text)) {
TreeItem *ti = tree->create_item(favorites_item);
ti->set_text(0, text);
ti->set_icon(0, icon);
@@ -855,7 +855,7 @@ void FileSystemDock::_search(EditorFileSystemDirectory *p_path, List<FileInfo> *
for (int i = 0; i < p_path->get_file_count(); i++) {
String file = p_path->get_file(i);
- if (file.to_lower().contains(searched_string)) {
+ if (_matches_all_search_tokens(file)) {
FileInfo fi;
fi.name = file;
fi.type = p_path->get_file_type(i);
@@ -982,14 +982,14 @@ void FileSystemDock::_update_file_list(bool p_keep_selection) {
if (favorite == "res://") {
text = "/";
icon = folder_icon;
- if (searched_string.length() == 0 || text.to_lower().find(searched_string) >= 0) {
+ if (searched_tokens.is_empty() || _matches_all_search_tokens(text)) {
files->add_item(text, icon, true);
files->set_item_metadata(-1, favorite);
}
} else if (favorite.ends_with("/")) {
text = favorite.substr(0, favorite.length() - 1).get_file();
icon = folder_icon;
- if (searched_string.length() == 0 || text.to_lower().find(searched_string) >= 0) {
+ if (searched_tokens.is_empty() || _matches_all_search_tokens(text)) {
files->add_item(text, icon, true);
files->set_item_metadata(-1, favorite);
}
@@ -1011,7 +1011,7 @@ void FileSystemDock::_update_file_list(bool p_keep_selection) {
fi.modified_time = 0;
}
- if (searched_string.length() == 0 || fi.name.to_lower().find(searched_string) >= 0) {
+ if (searched_tokens.is_empty() || _matches_all_search_tokens(fi.name)) {
file_list.push_back(fi);
}
}
@@ -1034,7 +1034,7 @@ void FileSystemDock::_update_file_list(bool p_keep_selection) {
return;
}
- if (searched_string.length() > 0) {
+ if (!searched_tokens.is_empty()) {
// Display the search results.
// Limit the number of results displayed to avoid an infinite loop.
_search(EditorFileSystem::get_singleton()->get_filesystem(), &file_list, 10000);
@@ -1270,7 +1270,7 @@ void FileSystemDock::_file_list_activate_file(int p_idx) {
}
void FileSystemDock::_preview_invalidated(const String &p_path) {
- if (file_list_display_mode == FILE_LIST_DISPLAY_THUMBNAILS && p_path.get_base_dir() == current_path && searched_string.length() == 0 && file_list_vb->is_visible_in_tree()) {
+ if (file_list_display_mode == FILE_LIST_DISPLAY_THUMBNAILS && p_path.get_base_dir() == current_path && searched_tokens.is_empty() && file_list_vb->is_visible_in_tree()) {
for (int i = 0; i < files->get_item_count(); i++) {
if (files->get_item_metadata(i) == p_path) {
// Re-request preview.
@@ -2612,12 +2612,13 @@ void FileSystemDock::_resource_created() {
}
void FileSystemDock::_search_changed(const String &p_text, const Control *p_from) {
- if (searched_string.length() == 0) {
+ if (searched_tokens.is_empty()) {
// Register the uncollapsed paths before they change.
uncollapsed_paths_before_search = get_uncollapsed_paths();
}
- searched_string = p_text.to_lower();
+ const String searched_string = p_text.to_lower();
+ searched_tokens = searched_string.split(" ", false);
if (p_from == tree_search_box) {
file_list_search_box->set_text(searched_string);
@@ -2628,16 +2629,29 @@ void FileSystemDock::_search_changed(const String &p_text, const Control *p_from
bool unfold_path = (p_text.is_empty() && !current_path.is_empty());
switch (display_mode) {
case DISPLAY_MODE_TREE_ONLY: {
- _update_tree(searched_string.length() == 0 ? uncollapsed_paths_before_search : Vector<String>(), false, false, unfold_path);
+ _update_tree(searched_tokens.is_empty() ? uncollapsed_paths_before_search : Vector<String>(), false, false, unfold_path);
} break;
case DISPLAY_MODE_HSPLIT:
case DISPLAY_MODE_VSPLIT: {
_update_file_list(false);
- _update_tree(searched_string.length() == 0 ? uncollapsed_paths_before_search : Vector<String>(), false, false, unfold_path);
+ _update_tree(searched_tokens.is_empty() ? uncollapsed_paths_before_search : Vector<String>(), false, false, unfold_path);
} break;
}
}
+bool FileSystemDock::_matches_all_search_tokens(const String &p_text) {
+ if (searched_tokens.is_empty()) {
+ return false;
+ }
+ const String s = p_text.to_lower();
+ for (const String &t : searched_tokens) {
+ if (!s.contains(t)) {
+ return false;
+ }
+ }
+ return true;
+}
+
void FileSystemDock::_rescan() {
_set_scanning_mode();
EditorFileSystem::get_singleton()->scan();
@@ -3365,7 +3379,7 @@ void FileSystemDock::_file_list_item_clicked(int p_item, const Vector2 &p_pos, M
// Popup.
if (!paths.is_empty()) {
file_list_popup->clear();
- _file_and_folders_fill_popup(file_list_popup, paths, searched_string.length() == 0);
+ _file_and_folders_fill_popup(file_list_popup, paths, searched_tokens.is_empty());
file_list_popup->set_position(files->get_screen_position() + p_pos);
file_list_popup->reset_size();
file_list_popup->popup();
@@ -3378,7 +3392,7 @@ void FileSystemDock::_file_list_empty_clicked(const Vector2 &p_pos, MouseButton
}
// Right click on empty space for file list.
- if (searched_string.length() > 0) {
+ if (!searched_tokens.is_empty()) {
return;
}
@@ -4125,7 +4139,6 @@ FileSystemDock::FileSystemDock() {
new_resource_dialog->set_base_type("Resource");
new_resource_dialog->connect("create", callable_mp(this, &FileSystemDock::_resource_created));
- searched_string = String();
uncollapsed_paths_before_search = Vector<String>();
tree_update_id = 0;
diff --git a/editor/filesystem_dock.h b/editor/filesystem_dock.h
index f514de15e5..058886c91a 100644
--- a/editor/filesystem_dock.h
+++ b/editor/filesystem_dock.h
@@ -172,7 +172,7 @@ private:
LineEdit *file_list_search_box = nullptr;
MenuButton *file_list_button_sort = nullptr;
- String searched_string;
+ PackedStringArray searched_tokens;
Vector<String> uncollapsed_paths_before_search;
TextureRect *search_icon = nullptr;
@@ -311,6 +311,7 @@ private:
void _split_dragged(int p_offset);
void _search_changed(const String &p_text, const Control *p_from);
+ bool _matches_all_search_tokens(const String &p_text);
MenuButton *_create_file_menu_button();
void _file_sort_popup(int p_id);
diff --git a/editor/icons/PreviewRotate.svg b/editor/icons/PreviewRotate.svg
new file mode 100644
index 0000000000..9e0da46169
--- /dev/null
+++ b/editor/icons/PreviewRotate.svg
@@ -0,0 +1 @@
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="#000" stroke-width="2" opacity=".8" stroke-linejoin="round"><circle cx="8" cy="8" r="2"/><path d="M8 1a7 7 0 00-4.982 11.998H1.911v2h4a1 1 0 00.97-1.242l-1-4-1.94.486.28 1.121a5 5 0 117.223.168l1.416 1.416A7 7 0 008 1z"/></g><g fill="#f9f9f9"><circle cx="8" cy="8" r="2"/><path d="M8 1a7 7 0 00-4.982 11.998H1.911v2h4a1 1 0 00.97-1.242l-1-4-1.94.486.28 1.121a5 5 0 117.223.168l1.416 1.416A7 7 0 008 1z"/></g></svg>
diff --git a/editor/import/3d/scene_import_settings.cpp b/editor/import/3d/scene_import_settings.cpp
index 620ebce44b..325525be1b 100644
--- a/editor/import/3d/scene_import_settings.cpp
+++ b/editor/import/3d/scene_import_settings.cpp
@@ -1117,6 +1117,20 @@ void SceneImportSettingsDialog::_cleanup() {
set_process(false);
}
+void SceneImportSettingsDialog::_on_light_1_switch_pressed() {
+ light1->set_visible(light_1_switch->is_pressed());
+}
+
+void SceneImportSettingsDialog::_on_light_2_switch_pressed() {
+ light2->set_visible(light_2_switch->is_pressed());
+}
+
+void SceneImportSettingsDialog::_on_light_rotate_switch_pressed() {
+ bool light_top_level = !light_rotate_switch->is_pressed();
+ light1->set_as_top_level_keep_local(light_top_level);
+ light2->set_as_top_level_keep_local(light_top_level);
+}
+
void SceneImportSettingsDialog::_viewport_input(const Ref<InputEvent> &p_input) {
float *rot_x = &cam_rot_x;
float *rot_y = &cam_rot_y;
@@ -1232,6 +1246,13 @@ void SceneImportSettingsDialog::_re_import() {
EditorFileSystem::get_singleton()->reimport_file_with_custom_parameters(base_path, editing_animation ? "animation_library" : "scene", main_settings);
}
+void SceneImportSettingsDialog::_update_theme_item_cache() {
+ ConfirmationDialog::_update_theme_item_cache();
+ theme_cache.light_1_icon = get_editor_theme_icon(SNAME("MaterialPreviewLight1"));
+ theme_cache.light_2_icon = get_editor_theme_icon(SNAME("MaterialPreviewLight2"));
+ theme_cache.rotate_icon = get_editor_theme_icon(SNAME("PreviewRotate"));
+}
+
void SceneImportSettingsDialog::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_READY: {
@@ -1251,6 +1272,10 @@ void SceneImportSettingsDialog::_notification(int p_what) {
animation_play_button->set_icon(get_editor_theme_icon(SNAME("MainPlay")));
}
animation_stop_button->set_icon(get_editor_theme_icon(SNAME("Stop")));
+
+ light_1_switch->set_icon(theme_cache.light_1_icon);
+ light_2_switch->set_icon(theme_cache.light_2_icon);
+ light_rotate_switch->set_icon(theme_cache.rotate_icon);
} break;
case NOTIFICATION_PROCESS: {
@@ -1644,6 +1669,40 @@ SceneImportSettingsDialog::SceneImportSettingsDialog() {
base_viewport->set_use_own_world_3d(true);
+ HBoxContainer *viewport_hbox = memnew(HBoxContainer);
+ vp_container->add_child(viewport_hbox);
+ viewport_hbox->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT, Control::PRESET_MODE_MINSIZE, 2);
+
+ viewport_hbox->add_spacer();
+
+ VBoxContainer *vb_light = memnew(VBoxContainer);
+ vb_light->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+ viewport_hbox->add_child(vb_light);
+
+ light_rotate_switch = memnew(Button);
+ light_rotate_switch->set_theme_type_variation("PreviewLightButton");
+ light_rotate_switch->set_toggle_mode(true);
+ light_rotate_switch->set_pressed(true);
+ light_rotate_switch->set_tooltip_text(TTR("Rotate Lights With Model"));
+ light_rotate_switch->connect("pressed", callable_mp(this, &SceneImportSettingsDialog::_on_light_rotate_switch_pressed));
+ vb_light->add_child(light_rotate_switch);
+
+ light_1_switch = memnew(Button);
+ light_1_switch->set_theme_type_variation("PreviewLightButton");
+ light_1_switch->set_toggle_mode(true);
+ light_1_switch->set_pressed(true);
+ light_1_switch->set_tooltip_text(TTR("Primary Light"));
+ light_1_switch->connect("pressed", callable_mp(this, &SceneImportSettingsDialog::_on_light_1_switch_pressed));
+ vb_light->add_child(light_1_switch);
+
+ light_2_switch = memnew(Button);
+ light_2_switch->set_theme_type_variation("PreviewLightButton");
+ light_2_switch->set_toggle_mode(true);
+ light_2_switch->set_pressed(true);
+ light_2_switch->set_tooltip_text(TTR("Secondary Light"));
+ light_2_switch->connect("pressed", callable_mp(this, &SceneImportSettingsDialog::_on_light_2_switch_pressed));
+ vb_light->add_child(light_2_switch);
+
camera = memnew(Camera3D);
base_viewport->add_child(camera);
camera->make_current();
@@ -1675,10 +1734,15 @@ SceneImportSettingsDialog::SceneImportSettingsDialog() {
environment->set_sky_custom_fov(50.0);
camera->set_environment(environment);
- light = memnew(DirectionalLight3D);
- light->set_transform(Transform3D().looking_at(Vector3(-1, -2, -0.6), Vector3(0, 1, 0)));
- base_viewport->add_child(light);
- light->set_shadow(true);
+ light1 = memnew(DirectionalLight3D);
+ light1->set_transform(Transform3D(Basis::looking_at(Vector3(-1, -1, -1))));
+ light1->set_shadow(true);
+ camera->add_child(light1);
+
+ light2 = memnew(DirectionalLight3D);
+ light2->set_transform(Transform3D(Basis::looking_at(Vector3(0, 1, 0), Vector3(0, 0, 1))));
+ light2->set_color(Color(0.5f, 0.5f, 0.5f));
+ camera->add_child(light2);
{
Ref<StandardMaterial3D> selection_mat;
diff --git a/editor/import/3d/scene_import_settings.h b/editor/import/3d/scene_import_settings.h
index 17d6616fc0..c2a5151432 100644
--- a/editor/import/3d/scene_import_settings.h
+++ b/editor/import/3d/scene_import_settings.h
@@ -85,7 +85,18 @@ class SceneImportSettingsDialog : public ConfirmationDialog {
bool first_aabb = false;
AABB contents_aabb;
- DirectionalLight3D *light = nullptr;
+ Button *light_1_switch = nullptr;
+ Button *light_2_switch = nullptr;
+ Button *light_rotate_switch = nullptr;
+
+ struct ThemeCache {
+ Ref<Texture2D> light_1_icon;
+ Ref<Texture2D> light_2_icon;
+ Ref<Texture2D> rotate_icon;
+ } theme_cache;
+
+ DirectionalLight3D *light1 = nullptr;
+ DirectionalLight3D *light2 = nullptr;
Ref<ArrayMesh> selection_mesh;
MeshInstance3D *node_selected = nullptr;
@@ -180,6 +191,9 @@ class SceneImportSettingsDialog : public ConfirmationDialog {
void _mesh_tree_selected();
void _scene_tree_selected();
void _cleanup();
+ void _on_light_1_switch_pressed();
+ void _on_light_2_switch_pressed();
+ void _on_light_rotate_switch_pressed();
void _viewport_input(const Ref<InputEvent> &p_input);
@@ -222,6 +236,7 @@ class SceneImportSettingsDialog : public ConfirmationDialog {
Timer *update_view_timer = nullptr;
protected:
+ virtual void _update_theme_item_cache() override;
void _notification(int p_what);
public:
diff --git a/editor/plugins/gizmos/geometry_instance_3d_gizmo_plugin.cpp b/editor/plugins/gizmos/geometry_instance_3d_gizmo_plugin.cpp
new file mode 100644
index 0000000000..16be707d08
--- /dev/null
+++ b/editor/plugins/gizmos/geometry_instance_3d_gizmo_plugin.cpp
@@ -0,0 +1,79 @@
+/**************************************************************************/
+/* geometry_instance_3d_gizmo_plugin.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "geometry_instance_3d_gizmo_plugin.h"
+
+#include "editor/editor_node.h"
+#include "editor/editor_settings.h"
+#include "editor/plugins/node_3d_editor_plugin.h"
+#include "scene/3d/visual_instance_3d.h"
+
+GeometryInstance3DGizmoPlugin::GeometryInstance3DGizmoPlugin() {
+}
+
+bool GeometryInstance3DGizmoPlugin::has_gizmo(Node3D *p_spatial) {
+ return Object::cast_to<GeometryInstance3D>(p_spatial) != nullptr;
+}
+
+String GeometryInstance3DGizmoPlugin::get_gizmo_name() const {
+ return "MeshInstance3DCustomAABB";
+}
+
+int GeometryInstance3DGizmoPlugin::get_priority() const {
+ return -1;
+}
+
+void GeometryInstance3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) {
+ GeometryInstance3D *geometry = Object::cast_to<GeometryInstance3D>(p_gizmo->get_node_3d());
+
+ p_gizmo->clear();
+
+ if (p_gizmo->is_selected()) {
+ AABB aabb = geometry->get_custom_aabb();
+
+ Vector<Vector3> lines;
+ for (int i = 0; i < 12; i++) {
+ Vector3 a;
+ Vector3 b;
+ aabb.get_edge(i, a, b);
+
+ lines.push_back(a);
+ lines.push_back(b);
+ }
+
+ Ref<StandardMaterial3D> mat = memnew(StandardMaterial3D);
+ mat->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
+ mat->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
+ const Color selection_box_color = EDITOR_GET("editors/3d_gizmos/gizmo_colors/aabb");
+ mat->set_albedo(selection_box_color);
+ mat->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
+ p_gizmo->add_lines(lines, mat);
+ }
+}
diff --git a/editor/plugins/gizmos/geometry_instance_3d_gizmo_plugin.h b/editor/plugins/gizmos/geometry_instance_3d_gizmo_plugin.h
new file mode 100644
index 0000000000..2482e277ea
--- /dev/null
+++ b/editor/plugins/gizmos/geometry_instance_3d_gizmo_plugin.h
@@ -0,0 +1,49 @@
+/**************************************************************************/
+/* geometry_instance_3d_gizmo_plugin.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef GEOMETRY_INSTANCE_3D_GIZMO_PLUGIN_H
+#define GEOMETRY_INSTANCE_3D_GIZMO_PLUGIN_H
+
+#include "editor/plugins/node_3d_editor_gizmos.h"
+
+class GeometryInstance3DGizmoPlugin : public EditorNode3DGizmoPlugin {
+ GDCLASS(GeometryInstance3DGizmoPlugin, EditorNode3DGizmoPlugin);
+
+public:
+ virtual bool has_gizmo(Node3D *p_spatial) override;
+ virtual String get_gizmo_name() const override;
+ virtual int get_priority() const override;
+
+ virtual void redraw(EditorNode3DGizmo *p_gizmo) override;
+
+ GeometryInstance3DGizmoPlugin();
+};
+
+#endif // GEOMETRY_INSTANCE_3D_GIZMO_PLUGIN_H
diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp
index aea3ea9700..be14132185 100644
--- a/editor/plugins/node_3d_editor_plugin.cpp
+++ b/editor/plugins/node_3d_editor_plugin.cpp
@@ -53,6 +53,7 @@
#include "editor/plugins/gizmos/cpu_particles_3d_gizmo_plugin.h"
#include "editor/plugins/gizmos/decal_gizmo_plugin.h"
#include "editor/plugins/gizmos/fog_volume_gizmo_plugin.h"
+#include "editor/plugins/gizmos/geometry_instance_3d_gizmo_plugin.h"
#include "editor/plugins/gizmos/gpu_particles_3d_gizmo_plugin.h"
#include "editor/plugins/gizmos/gpu_particles_collision_3d_gizmo_plugin.h"
#include "editor/plugins/gizmos/joint_3d_gizmo_plugin.h"
@@ -8096,6 +8097,7 @@ void Node3DEditor::_register_all_gizmos() {
add_gizmo_plugin(Ref<SoftBody3DGizmoPlugin>(memnew(SoftBody3DGizmoPlugin)));
add_gizmo_plugin(Ref<SpriteBase3DGizmoPlugin>(memnew(SpriteBase3DGizmoPlugin)));
add_gizmo_plugin(Ref<Label3DGizmoPlugin>(memnew(Label3DGizmoPlugin)));
+ add_gizmo_plugin(Ref<GeometryInstance3DGizmoPlugin>(memnew(GeometryInstance3DGizmoPlugin)));
add_gizmo_plugin(Ref<Marker3DGizmoPlugin>(memnew(Marker3DGizmoPlugin)));
add_gizmo_plugin(Ref<RayCast3DGizmoPlugin>(memnew(RayCast3DGizmoPlugin)));
add_gizmo_plugin(Ref<ShapeCast3DGizmoPlugin>(memnew(ShapeCast3DGizmoPlugin)));
diff --git a/editor/plugins/theme_editor_plugin.cpp b/editor/plugins/theme_editor_plugin.cpp
index 0db752e771..166ed05748 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<ThemeItemLabel *>(this));
+ return memnew(Control); // Make the standard tooltip invisible.
}
VBoxContainer *ThemeTypeEditor::_create_item_list(Theme::DataType p_data_type) {
@@ -2436,8 +2438,8 @@ HBoxContainer *ThemeTypeEditor::_create_property_control(Theme::DataType p_data_
item_name->set_h_size_flags(SIZE_EXPAND_FILL);
item_name->set_clip_text(true);
item_name->set_text(p_item_name);
- // `|` separators used in `EditorHelpTooltip` for formatting.
- item_name->set_tooltip_text("theme_item|" + edited_type + "|" + p_item_name + "|");
+ // `|` separators used in `EditorHelpBit`.
+ item_name->set_tooltip_text("theme_item|" + edited_type + "|" + p_item_name);
item_name->set_mouse_filter(Control::MOUSE_FILTER_STOP);
item_name_container->add_child(item_name);
diff --git a/editor/plugins/theme_editor_plugin.h b/editor/plugins/theme_editor_plugin.h
index ba3446807e..0bc02789aa 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 ac175d01a6..d7af751f95 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 f74be7d978..6151cb4b74 100644
--- a/editor/themes/editor_theme_manager.cpp
+++ b/editor/themes/editor_theme_manager.cpp
@@ -2166,6 +2166,28 @@ void EditorThemeManager::_populate_editor_styles(const Ref<EditorTheme> &p_theme
p_theme->set_constant("text_highlight_v_padding", "EditorHelp", 2 * EDSCALE);
}
+ // EditorHelpBitTitle.
+ {
+ Ref<StyleBoxFlat> style = p_config.tree_panel_style->duplicate();
+ style->set_bg_color(p_config.dark_theme ? style->get_bg_color().lightened(0.04) : style->get_bg_color().darkened(0.04));
+ style->set_border_color(p_config.dark_theme ? style->get_border_color().lightened(0.04) : style->get_border_color().darkened(0.04));
+ style->set_corner_radius(CORNER_BOTTOM_LEFT, 0);
+ style->set_corner_radius(CORNER_BOTTOM_RIGHT, 0);
+
+ p_theme->set_type_variation("EditorHelpBitTitle", "RichTextLabel");
+ p_theme->set_stylebox("normal", "EditorHelpBitTitle", style);
+ }
+
+ // EditorHelpBitContent.
+ {
+ Ref<StyleBoxFlat> style = p_config.tree_panel_style->duplicate();
+ style->set_corner_radius(CORNER_TOP_LEFT, 0);
+ style->set_corner_radius(CORNER_TOP_RIGHT, 0);
+
+ p_theme->set_type_variation("EditorHelpBitContent", "RichTextLabel");
+ p_theme->set_stylebox("normal", "EditorHelpBitContent", style);
+ }
+
// Asset Library.
p_theme->set_stylebox("bg", "AssetLib", p_config.base_empty_style);
p_theme->set_stylebox("panel", "AssetLib", p_config.content_panel_style);
diff --git a/methods.py b/methods.py
index 4d1f4c1cda..8498310bf5 100644
--- a/methods.py
+++ b/methods.py
@@ -990,6 +990,10 @@ def using_emcc(env):
def show_progress(env):
+ if env["ninja"]:
+ # Has its own progress/tracking tool that clashes with ours
+ return
+
import sys
from SCons.Script import Progress, Command, AlwaysBuild
diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp
index d706c1b101..c526d9c0a4 100644
--- a/modules/gdscript/gdscript_compiler.cpp
+++ b/modules/gdscript/gdscript_compiler.cpp
@@ -250,7 +250,7 @@ static bool _can_use_validate_call(const MethodBind *p_method, const Vector<GDSc
return true;
}
-GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &codegen, Error &r_error, const GDScriptParser::ExpressionNode *p_expression, bool p_root, bool p_initializer, const GDScriptCodeGenerator::Address &p_index_addr) {
+GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &codegen, Error &r_error, const GDScriptParser::ExpressionNode *p_expression, bool p_root, bool p_initializer) {
if (p_expression->is_constant && !(p_expression->get_datatype().is_meta_type && p_expression->get_datatype().kind == GDScriptParser::DataType::CLASS)) {
return codegen.add_constant(p_expression->reduced_value);
}
@@ -781,9 +781,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
bool named = subscript->is_attribute;
StringName name;
GDScriptCodeGenerator::Address index;
- if (p_index_addr.mode != GDScriptCodeGenerator::Address::NIL) {
- index = p_index_addr;
- } else if (subscript->is_attribute) {
+ if (subscript->is_attribute) {
if (subscript->base->type == GDScriptParser::Node::SELF && codegen.script) {
GDScriptParser::IdentifierNode *identifier = subscript->attribute;
HashMap<StringName, GDScript::MemberInfo>::Iterator MI = codegen.script->member_indices.find(identifier->name);
diff --git a/modules/gdscript/gdscript_compiler.h b/modules/gdscript/gdscript_compiler.h
index 0adbe1ed8e..637d61ca3b 100644
--- a/modules/gdscript/gdscript_compiler.h
+++ b/modules/gdscript/gdscript_compiler.h
@@ -149,13 +149,9 @@ class GDScriptCompiler {
void _set_error(const String &p_error, const GDScriptParser::Node *p_node);
- Error _create_binary_operator(CodeGen &codegen, const GDScriptParser::BinaryOpNode *on, Variant::Operator op, bool p_initializer = false, const GDScriptCodeGenerator::Address &p_index_addr = GDScriptCodeGenerator::Address());
- Error _create_binary_operator(CodeGen &codegen, const GDScriptParser::ExpressionNode *p_left_operand, const GDScriptParser::ExpressionNode *p_right_operand, Variant::Operator op, bool p_initializer = false, const GDScriptCodeGenerator::Address &p_index_addr = GDScriptCodeGenerator::Address());
-
GDScriptDataType _gdtype_from_datatype(const GDScriptParser::DataType &p_datatype, GDScript *p_owner, bool p_handle_metatype = true);
- GDScriptCodeGenerator::Address _parse_assign_right_expression(CodeGen &codegen, Error &r_error, const GDScriptParser::AssignmentNode *p_assignmentint, const GDScriptCodeGenerator::Address &p_index_addr = GDScriptCodeGenerator::Address());
- GDScriptCodeGenerator::Address _parse_expression(CodeGen &codegen, Error &r_error, const GDScriptParser::ExpressionNode *p_expression, bool p_root = false, bool p_initializer = false, const GDScriptCodeGenerator::Address &p_index_addr = GDScriptCodeGenerator::Address());
+ GDScriptCodeGenerator::Address _parse_expression(CodeGen &codegen, Error &r_error, const GDScriptParser::ExpressionNode *p_expression, bool p_root = false, bool p_initializer = false);
GDScriptCodeGenerator::Address _parse_match_pattern(CodeGen &codegen, Error &r_error, const GDScriptParser::PatternNode *p_pattern, const GDScriptCodeGenerator::Address &p_value_addr, const GDScriptCodeGenerator::Address &p_type_addr, const GDScriptCodeGenerator::Address &p_previous_test, bool p_is_first, bool p_is_nested);
List<GDScriptCodeGenerator::Address> _add_locals_in_block(CodeGen &codegen, const GDScriptParser::SuiteNode *p_block);
void _clear_addresses(CodeGen &codegen, const List<GDScriptCodeGenerator::Address> &p_addresses);
diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp
index a3464ccfc2..dcc18ebdd7 100644
--- a/modules/mono/csharp_script.cpp
+++ b/modules/mono/csharp_script.cpp
@@ -941,6 +941,31 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) {
to_reload_state.push_back(scr);
}
+ // Deserialize managed callables.
+ // This is done before reloading script's internal state, so potential callables invoked in properties work.
+ {
+ MutexLock lock(ManagedCallable::instances_mutex);
+
+ for (const KeyValue<ManagedCallable *, Array> &elem : ManagedCallable::instances_pending_reload) {
+ ManagedCallable *managed_callable = elem.key;
+ const Array &serialized_data = elem.value;
+
+ GCHandleIntPtr delegate = { nullptr };
+
+ bool success = GDMonoCache::managed_callbacks.DelegateUtils_TryDeserializeDelegateWithGCHandle(
+ &serialized_data, &delegate);
+
+ if (success) {
+ ERR_CONTINUE(delegate.value == nullptr);
+ managed_callable->delegate_handle = delegate;
+ } else if (OS::get_singleton()->is_stdout_verbose()) {
+ OS::get_singleton()->print("Failed to deserialize delegate\n");
+ }
+ }
+
+ ManagedCallable::instances_pending_reload.clear();
+ }
+
for (Ref<CSharpScript> &scr : to_reload_state) {
for (const ObjectID &obj_id : scr->pending_reload_instances) {
Object *obj = ObjectDB::get_instance(obj_id);
@@ -963,7 +988,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) {
properties[G.first] = G.second;
}
- // Restore serialized state and call OnAfterDeserialization
+ // Restore serialized state and call OnAfterDeserialize.
GDMonoCache::managed_callbacks.CSharpInstanceBridge_DeserializeState(
csi->get_gchandle_intptr(), &properties, &state_backup.event_signals);
}
@@ -973,30 +998,6 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) {
scr->pending_reload_state.clear();
}
- // Deserialize managed callables
- {
- MutexLock lock(ManagedCallable::instances_mutex);
-
- for (const KeyValue<ManagedCallable *, Array> &elem : ManagedCallable::instances_pending_reload) {
- ManagedCallable *managed_callable = elem.key;
- const Array &serialized_data = elem.value;
-
- GCHandleIntPtr delegate = { nullptr };
-
- bool success = GDMonoCache::managed_callbacks.DelegateUtils_TryDeserializeDelegateWithGCHandle(
- &serialized_data, &delegate);
-
- if (success) {
- ERR_CONTINUE(delegate.value == nullptr);
- managed_callable->delegate_handle = delegate;
- } else if (OS::get_singleton()->is_stdout_verbose()) {
- OS::get_singleton()->print("Failed to deserialize delegate\n");
- }
- }
-
- ManagedCallable::instances_pending_reload.clear();
- }
-
#ifdef TOOLS_ENABLED
// FIXME: Hack to refresh editor in order to display new properties and signals. See if there is a better alternative.
if (Engine::get_singleton()->is_editor_hint()) {
diff --git a/scene/2d/audio_stream_player_2d.cpp b/scene/2d/audio_stream_player_2d.cpp
index cbd3c244d9..4fc0fe0268 100644
--- a/scene/2d/audio_stream_player_2d.cpp
+++ b/scene/2d/audio_stream_player_2d.cpp
@@ -196,6 +196,7 @@ Ref<AudioStream> AudioStreamPlayer2D::get_stream() const {
}
void AudioStreamPlayer2D::set_volume_db(float p_volume) {
+ ERR_FAIL_COND_MSG(Math::is_nan(p_volume), "Volume can't be set to NaN.");
internal->volume_db = p_volume;
}
diff --git a/scene/3d/audio_stream_player_3d.cpp b/scene/3d/audio_stream_player_3d.cpp
index f1f9a04ea0..0cef56dbf2 100644
--- a/scene/3d/audio_stream_player_3d.cpp
+++ b/scene/3d/audio_stream_player_3d.cpp
@@ -496,6 +496,7 @@ Ref<AudioStream> AudioStreamPlayer3D::get_stream() const {
}
void AudioStreamPlayer3D::set_volume_db(float p_volume) {
+ ERR_FAIL_COND_MSG(Math::is_nan(p_volume), "Volume can't be set to NaN.");
internal->volume_db = p_volume;
}
diff --git a/scene/3d/node_3d.cpp b/scene/3d/node_3d.cpp
index b11a1d9506..3b788b2fd0 100644
--- a/scene/3d/node_3d.cpp
+++ b/scene/3d/node_3d.cpp
@@ -753,6 +753,15 @@ void Node3D::set_as_top_level(bool p_enabled) {
data.top_level = p_enabled;
}
+void Node3D::set_as_top_level_keep_local(bool p_enabled) {
+ ERR_THREAD_GUARD;
+ if (data.top_level == p_enabled) {
+ return;
+ }
+ data.top_level = p_enabled;
+ _propagate_transform_changed(this);
+}
+
bool Node3D::is_set_as_top_level() const {
ERR_READ_THREAD_GUARD_V(false);
return data.top_level;
diff --git a/scene/3d/node_3d.h b/scene/3d/node_3d.h
index 7f8c3e6e68..c1667221df 100644
--- a/scene/3d/node_3d.h
+++ b/scene/3d/node_3d.h
@@ -227,6 +227,7 @@ public:
void clear_gizmos();
void set_as_top_level(bool p_enabled);
+ void set_as_top_level_keep_local(bool p_enabled);
bool is_set_as_top_level() const;
void set_disable_scale(bool p_enabled);
diff --git a/scene/3d/visual_instance_3d.cpp b/scene/3d/visual_instance_3d.cpp
index 503c39ae3e..89f7ab2391 100644
--- a/scene/3d/visual_instance_3d.cpp
+++ b/scene/3d/visual_instance_3d.cpp
@@ -377,6 +377,7 @@ void GeometryInstance3D::set_custom_aabb(AABB p_aabb) {
}
custom_aabb = p_aabb;
RS::get_singleton()->instance_set_custom_aabb(get_instance(), custom_aabb);
+ update_gizmos();
}
AABB GeometryInstance3D::get_custom_aabb() const {
diff --git a/scene/audio/audio_stream_player.cpp b/scene/audio/audio_stream_player.cpp
index dadcfab69f..0c2bd64e84 100644
--- a/scene/audio/audio_stream_player.cpp
+++ b/scene/audio/audio_stream_player.cpp
@@ -59,6 +59,7 @@ Ref<AudioStream> AudioStreamPlayer::get_stream() const {
}
void AudioStreamPlayer::set_volume_db(float p_volume) {
+ ERR_FAIL_COND_MSG(Math::is_nan(p_volume), "Volume can't be set to NaN.");
internal->volume_db = p_volume;
Vector<AudioFrame> volume_vector = _get_volume_vector();
diff --git a/scene/gui/dialogs.cpp b/scene/gui/dialogs.cpp
index e481b78715..4d2080dda2 100644
--- a/scene/gui/dialogs.cpp
+++ b/scene/gui/dialogs.cpp
@@ -264,12 +264,6 @@ Size2 AcceptDialog::_get_contents_minimum_size() const {
content_minsize = child_minsize.max(content_minsize);
}
- // Then we take the background panel as it provides the offsets,
- // which are always added to the minimum size.
- if (theme_cache.panel_style.is_valid()) {
- content_minsize += theme_cache.panel_style->get_minimum_size();
- }
-
// Then we add buttons. Horizontally we're interested in whichever
// value is the biggest. Vertically buttons add to the overall size.
Size2 buttons_minsize = buttons_hbox->get_combined_minimum_size();
@@ -278,6 +272,12 @@ Size2 AcceptDialog::_get_contents_minimum_size() const {
// Plus there is a separation size added on top.
content_minsize.y += theme_cache.buttons_separation;
+ // Then we take the background panel as it provides the offsets,
+ // which are always added to the minimum size.
+ if (theme_cache.panel_style.is_valid()) {
+ content_minsize += theme_cache.panel_style->get_minimum_size();
+ }
+
return content_minsize;
}
diff --git a/scene/gui/graph_frame.cpp b/scene/gui/graph_frame.cpp
index 288b8ba66d..ca9f7e6fcf 100644
--- a/scene/gui/graph_frame.cpp
+++ b/scene/gui/graph_frame.cpp
@@ -118,7 +118,7 @@ void GraphFrame::_notification(int p_what) {
sb_panel_flat->set_border_color(selected ? original_border_color : tint_color.lightened(0.3));
draw_style_box(sb_panel_flat, body_rect);
} else if (sb_panel_texture.is_valid()) {
- sb_panel_texture = sb_panel_flat->duplicate();
+ sb_panel_texture = sb_panel_texture->duplicate();
sb_panel_texture->set_modulate(tint_color);
draw_style_box(sb_panel_texture, body_rect);
}
diff --git a/servers/rendering/renderer_rd/effects/copy_effects.cpp b/servers/rendering/renderer_rd/effects/copy_effects.cpp
index 1568867663..a363b03dd8 100644
--- a/servers/rendering/renderer_rd/effects/copy_effects.cpp
+++ b/servers/rendering/renderer_rd/effects/copy_effects.cpp
@@ -533,7 +533,7 @@ void CopyEffects::copy_to_atlas_fb(RID p_source_rd_texture, RID p_dest_framebuff
RD::get_singleton()->draw_list_draw(draw_list, true);
}
-void CopyEffects::copy_to_fb_rect(RID p_source_rd_texture, RID p_dest_framebuffer, const Rect2i &p_rect, bool p_flip_y, bool p_force_luminance, bool p_alpha_to_zero, bool p_srgb, RID p_secondary, bool p_multiview, bool p_alpha_to_one, bool p_linear, bool p_normal) {
+void CopyEffects::copy_to_fb_rect(RID p_source_rd_texture, RID p_dest_framebuffer, const Rect2i &p_rect, bool p_flip_y, bool p_force_luminance, bool p_alpha_to_zero, bool p_srgb, RID p_secondary, bool p_multiview, bool p_alpha_to_one, bool p_linear, bool p_normal, const Rect2 &p_src_rect) {
UniformSetCacheRD *uniform_set_cache = UniformSetCacheRD::get_singleton();
ERR_FAIL_NULL(uniform_set_cache);
MaterialStorage *material_storage = MaterialStorage::get_singleton();
@@ -568,6 +568,14 @@ void CopyEffects::copy_to_fb_rect(RID p_source_rd_texture, RID p_dest_framebuffe
copy_to_fb.push_constant.flags |= COPY_TO_FB_FLAG_NORMAL;
}
+ if (p_src_rect != Rect2()) {
+ copy_to_fb.push_constant.section[0] = p_src_rect.position.x;
+ copy_to_fb.push_constant.section[1] = p_src_rect.position.y;
+ copy_to_fb.push_constant.section[2] = p_src_rect.size.x;
+ copy_to_fb.push_constant.section[3] = p_src_rect.size.y;
+ copy_to_fb.push_constant.flags |= COPY_TO_FB_FLAG_USE_SRC_SECTION;
+ }
+
// setup our uniforms
RID default_sampler = material_storage->sampler_rd_get_default(RS::CANVAS_ITEM_TEXTURE_FILTER_LINEAR, RS::CANVAS_ITEM_TEXTURE_REPEAT_DISABLED);
diff --git a/servers/rendering/renderer_rd/effects/copy_effects.h b/servers/rendering/renderer_rd/effects/copy_effects.h
index d18971a676..014f78e2b9 100644
--- a/servers/rendering/renderer_rd/effects/copy_effects.h
+++ b/servers/rendering/renderer_rd/effects/copy_effects.h
@@ -191,6 +191,7 @@ private:
COPY_TO_FB_FLAG_ALPHA_TO_ONE = (1 << 5),
COPY_TO_FB_FLAG_LINEAR = (1 << 6),
COPY_TO_FB_FLAG_NORMAL = (1 << 7),
+ COPY_TO_FB_FLAG_USE_SRC_SECTION = (1 << 8),
};
struct CopyToFbPushConstant {
@@ -329,7 +330,7 @@ public:
void copy_cubemap_to_panorama(RID p_source_cube, RID p_dest_panorama, const Size2i &p_panorama_size, float p_lod, bool p_is_array);
void copy_depth_to_rect(RID p_source_rd_texture, RID p_dest_framebuffer, const Rect2i &p_rect, bool p_flip_y = false);
void copy_depth_to_rect_and_linearize(RID p_source_rd_texture, RID p_dest_texture, const Rect2i &p_rect, bool p_flip_y, float p_z_near, float p_z_far);
- void copy_to_fb_rect(RID p_source_rd_texture, RID p_dest_framebuffer, const Rect2i &p_rect, bool p_flip_y = false, bool p_force_luminance = false, bool p_alpha_to_zero = false, bool p_srgb = false, RID p_secondary = RID(), bool p_multiview = false, bool alpha_to_one = false, bool p_linear = false, bool p_normal = false);
+ void copy_to_fb_rect(RID p_source_rd_texture, RID p_dest_framebuffer, const Rect2i &p_rect, bool p_flip_y = false, bool p_force_luminance = false, bool p_alpha_to_zero = false, bool p_srgb = false, RID p_secondary = RID(), bool p_multiview = false, bool alpha_to_one = false, bool p_linear = false, bool p_normal = false, const Rect2 &p_src_rect = Rect2());
void copy_to_atlas_fb(RID p_source_rd_texture, RID p_dest_framebuffer, const Rect2 &p_uv_rect, RD::DrawListID p_draw_list, bool p_flip_y = false, bool p_panorama = false);
void copy_to_drawlist(RD::DrawListID p_draw_list, RD::FramebufferFormatID p_fb_format, RID p_source_rd_texture, bool p_linear = false);
void copy_raster(RID p_source_texture, RID p_dest_framebuffer);
diff --git a/servers/rendering/renderer_rd/shaders/effects/copy_to_fb.glsl b/servers/rendering/renderer_rd/shaders/effects/copy_to_fb.glsl
index 7192e596eb..3f9d1cce79 100644
--- a/servers/rendering/renderer_rd/shaders/effects/copy_to_fb.glsl
+++ b/servers/rendering/renderer_rd/shaders/effects/copy_to_fb.glsl
@@ -21,6 +21,7 @@
#define FLAG_ALPHA_TO_ONE (1 << 5)
#define FLAG_LINEAR (1 << 6)
#define FLAG_NORMAL (1 << 7)
+#define FLAG_USE_SRC_SECTION (1 << 8)
#ifdef USE_MULTIVIEW
layout(location = 0) out vec3 uv_interp;
@@ -54,6 +55,10 @@ void main() {
if (bool(params.flags & FLAG_FLIP_Y)) {
uv_interp.y = 1.0 - uv_interp.y;
}
+
+ if (bool(params.flags & FLAG_USE_SRC_SECTION)) {
+ uv_interp = params.section.xy + uv_interp * params.section.zw;
+ }
}
#[fragment]
diff --git a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp
index da046bf6b1..af30a32866 100644
--- a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp
+++ b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp
@@ -3820,7 +3820,10 @@ void TextureStorage::render_target_copy_to_back_buffer(RID p_render_target, cons
if (RendererSceneRenderRD::get_singleton()->_render_buffers_can_be_storage()) {
copy_effects->copy_to_rect(rt->color, rt->backbuffer_mipmap0, region, false, false, false, !rt->use_hdr, true);
} else {
- copy_effects->copy_to_fb_rect(rt->color, rt->backbuffer_fb, region, false, false, false, false, RID(), false, true);
+ Rect2 src_rect = Rect2(region);
+ src_rect.position /= Size2(rt->size);
+ src_rect.size /= Size2(rt->size);
+ copy_effects->copy_to_fb_rect(rt->color, rt->backbuffer_fb, region, false, false, false, false, RID(), false, true, false, false, src_rect);
}
if (!p_gen_mipmaps) {
diff --git a/tests/core/math/test_transform_3d.h b/tests/core/math/test_transform_3d.h
index 551b20fe74..fba0fcb280 100644
--- a/tests/core/math/test_transform_3d.h
+++ b/tests/core/math/test_transform_3d.h
@@ -107,6 +107,35 @@ TEST_CASE("[Transform3D] Finite number checks") {
"Transform3D with two components infinite should not be finite.");
}
+TEST_CASE("[Transform3D] Rotate around global origin") {
+ // Start with the default orientation, but not centered on the origin.
+ // Rotating should rotate both our basis and the origin.
+ Transform3D transform = Transform3D();
+ transform.origin = Vector3(0, 0, 1);
+
+ Transform3D expected = Transform3D();
+ expected.origin = Vector3(0, 0, -1);
+ expected.basis[0] = Vector3(-1, 0, 0);
+ expected.basis[2] = Vector3(0, 0, -1);
+
+ const Transform3D rotated_transform = transform.rotated(Vector3(0, 1, 0), Math_PI);
+ CHECK_MESSAGE(rotated_transform.is_equal_approx(expected), "The rotated transform should have a new orientation and basis.");
+}
+
+TEST_CASE("[Transform3D] Rotate in-place (local rotation)") {
+ // Start with the default orientation.
+ // Local rotation should not change the origin, only the basis.
+ Transform3D transform = Transform3D();
+ transform.origin = Vector3(1, 2, 3);
+
+ Transform3D expected = Transform3D();
+ expected.origin = Vector3(1, 2, 3);
+ expected.basis[0] = Vector3(-1, 0, 0);
+ expected.basis[2] = Vector3(0, 0, -1);
+
+ const Transform3D rotated_transform = Transform3D(transform.rotated_local(Vector3(0, 1, 0), Math_PI));
+ CHECK_MESSAGE(rotated_transform.is_equal_approx(expected), "The rotated transform should have a new orientation but still be based on the same origin.");
+}
} // namespace TestTransform3D
#endif // TEST_TRANSFORM_3D_H