diff options
64 files changed, 741 insertions, 350 deletions
diff --git a/doc/classes/Animation.xml b/doc/classes/Animation.xml index 9529c60771..ceef16f158 100644 --- a/doc/classes/Animation.xml +++ b/doc/classes/Animation.xml @@ -692,6 +692,17 @@ Sets the update mode (see [enum UpdateMode]) of a value track. </description> </method> + <method name="value_track_interpolate" qualifiers="const"> + <return type="float"> + </return> + <argument index="0" name="track_idx" type="int"> + </argument> + <argument index="1" name="time_sec" type="float"> + </argument> + <description> + Returns the interpolated value at the given time (in seconds). The [code]track_idx[/code] must be the index of a value track. + </description> + </method> </methods> <members> <member name="length" type="float" setter="set_length" getter="get_length" default="1.0"> diff --git a/doc/classes/CanvasItem.xml b/doc/classes/CanvasItem.xml index 946c4f8988..3524a5d22b 100644 --- a/doc/classes/CanvasItem.xml +++ b/doc/classes/CanvasItem.xml @@ -525,13 +525,6 @@ Returns [code]true[/code] if local transform notifications are communicated to children. </description> </method> - <method name="is_set_as_toplevel" qualifiers="const"> - <return type="bool"> - </return> - <description> - Returns [code]true[/code] if the node is set as top-level. See [method set_as_toplevel]. - </description> - </method> <method name="is_transform_notification_enabled" qualifiers="const"> <return type="bool"> </return> @@ -564,15 +557,6 @@ Transformations issued by [code]event[/code]'s inputs are applied in local space instead of global space. </description> </method> - <method name="set_as_toplevel"> - <return type="void"> - </return> - <argument index="0" name="enable" type="bool"> - </argument> - <description> - If [code]enable[/code] is [code]true[/code], the node won't inherit its transform from parent canvas items. - </description> - </method> <method name="set_notify_local_transform"> <return type="void"> </return> @@ -622,6 +606,9 @@ <member name="show_behind_parent" type="bool" setter="set_draw_behind_parent" getter="is_draw_behind_parent_enabled" default="false"> If [code]true[/code], the object draws behind its parent. </member> + <member name="toplevel" type="bool" setter="set_as_toplevel" getter="is_set_as_toplevel"> + If [code]true[/code], the node will not inherit its transform from parent [CanvasItem]s. + </member> <member name="show_on_top" type="bool" setter="_set_on_top" getter="_is_on_top"> If [code]true[/code], the object draws on top of its parent. </member> diff --git a/doc/classes/EditorNode3DGizmoPlugin.xml b/doc/classes/EditorNode3DGizmoPlugin.xml index 8865939eb1..4dea1bb645 100644 --- a/doc/classes/EditorNode3DGizmoPlugin.xml +++ b/doc/classes/EditorNode3DGizmoPlugin.xml @@ -136,7 +136,7 @@ </description> </method> <method name="get_priority" qualifiers="virtual"> - <return type="String"> + <return type="int"> </return> <description> Override this method to set the gizmo's priority. Higher values correspond to higher priority. If a gizmo with higher priority conflicts with another gizmo, only the gizmo with higher priority will be used. diff --git a/doc/classes/File.xml b/doc/classes/File.xml index f1b9106d35..9a94231cc1 100644 --- a/doc/classes/File.xml +++ b/doc/classes/File.xml @@ -399,8 +399,7 @@ <argument index="0" name="line" type="String"> </argument> <description> - Stores the given [String] as a line in the file. - Text will be encoded as UTF-8. + Appends [code]line[/code] to the file followed by a line return character ([code]\n[/code]), encoding the text as UTF-8. </description> </method> <method name="store_pascal_string"> @@ -428,8 +427,7 @@ <argument index="0" name="string" type="String"> </argument> <description> - Stores the given [String] in the file. - Text will be encoded as UTF-8. + Appends [code]string[/code] to the file without a line return, encoding the text as UTF-8. </description> </method> <method name="store_var"> diff --git a/doc/classes/Node3D.xml b/doc/classes/Node3D.xml index 1ef875f606..daadde4fe4 100644 --- a/doc/classes/Node3D.xml +++ b/doc/classes/Node3D.xml @@ -83,13 +83,6 @@ Returns whether this node uses a scale of [code](1, 1, 1)[/code] or its local transformation scale. </description> </method> - <method name="is_set_as_toplevel" qualifiers="const"> - <return type="bool"> - </return> - <description> - Returns whether this node is set as Toplevel, that is whether it ignores its parent nodes transformations. - </description> - </method> <method name="is_transform_notification_enabled" qualifiers="const"> <return type="bool"> </return> @@ -195,15 +188,6 @@ Scales the local transformation by given 3D scale factors in object-local coordinate system. </description> </method> - <method name="set_as_toplevel"> - <return type="void"> - </return> - <argument index="0" name="enable" type="bool"> - </argument> - <description> - Makes the node ignore its parents transformations. Node transformations are only in global space. - </description> - </method> <method name="set_disable_scale"> <return type="void"> </return> @@ -316,6 +300,9 @@ <member name="scale" type="Vector3" setter="set_scale" getter="get_scale" default="Vector3( 1, 1, 1 )"> Scale part of the local transformation. </member> + <member name="toplevel" type="bool" setter="set_as_toplevel" getter="is_set_as_toplevel"> + If [code]true[/code], the node will not inherit its transformations from its parent. Node transformations are only in global space. + </member> <member name="transform" type="Transform" setter="set_transform" getter="get_transform" default="Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 )"> Local space [Transform] of this node, with respect to the parent node. </member> diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index ed8eddda07..d040341100 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -555,6 +555,9 @@ <member name="input_devices/pointing/emulate_touch_from_mouse" type="bool" setter="" getter="" default="false"> If [code]true[/code], sends touch input events when clicking or dragging the mouse. </member> + <member name="input_devices/pointing/ios/touch_delay" type="float" setter="" getter="" default="0.150"> + Default delay for touch events. This only affects iOS devices. + </member> <member name="layer_names/2d_physics/layer_1" type="String" setter="" getter="" default=""""> Optional name for the 2D physics layer 1. </member> diff --git a/doc/classes/String.xml b/doc/classes/String.xml index 40fff25fc4..4034a5ee07 100644 --- a/doc/classes/String.xml +++ b/doc/classes/String.xml @@ -342,7 +342,10 @@ <argument index="0" name="to" type="String"> </argument> <description> - Performs a case-sensitive comparison to another string. Returns [code]-1[/code] if less than, [code]+1[/code] if greater than, or [code]0[/code] if equal. + Performs a case-sensitive comparison to another string. Returns [code]-1[/code] if less than, [code]1[/code] if greater than, or [code]0[/code] if equal. "less than" or "greater than" are determined by the [url=https://en.wikipedia.org/wiki/List_of_Unicode_characters]Unicode code points[/code] of each string, which roughly matches the alphabetical order. + [b]Behavior with different string lengths:[/b] Returns [code]1[/code] if the "base" string is longer than the [code]to[/code] string or [code]-1[/code] if the "base" string is shorter than the [code]to[/code] string. Keep in mind this length is determined by the number of Unicode codepoints, [i]not[/i] the actual visible characters. + [b]Behavior with empty strings:[/b] Returns [code]-1[/code] if the "base" string is empty, [code]1[/code] if the [code]to[/code] string is empty or [code]0[/code] if both strings are empty. + To get a boolean result from a string comparison, use the [code]==[/code] operator instead. See also [method nocasecmp_to] and [method naturalnocasecmp_to]. </description> </method> <method name="count"> @@ -615,7 +618,7 @@ <return type="bool"> </return> <description> - Returns [code]true[/code] if this string contains a valid IP address. + Returns [code]true[/code] if this string contains only a well-formatted IPv4 or IPv6 address. This method considers [url=https://en.wikipedia.org/wiki/Reserved_IP_addresses]reserved IP addresses[/url] such as [code]0.0.0.0[/code] as valid. </description> </method> <method name="join"> @@ -712,7 +715,11 @@ <argument index="0" name="to" type="String"> </argument> <description> - Performs a case-insensitive natural order comparison to another string. Returns [code]-1[/code] if less than, [code]+1[/code] if greater than, or [code]0[/code] if equal. + Performs a case-insensitive [i]natural order[/i] comparison to another string. Returns [code]-1[/code] if less than, [code]1[/code] if greater than, or [code]0[/code] if equal. "less than" or "greater than" are determined by the [url=https://en.wikipedia.org/wiki/List_of_Unicode_characters]Unicode code points[/code] of each string, which roughly matches the alphabetical order. Internally, lowercase characters will be converted to uppercase during the comparison. + When used for sorting, natural order comparison will order suites of numbers as expected by most people. If you sort the numbers from 1 to 10 using natural order, you will get [code][1, 2, 3, ...][/code] instead of [code][1, 10, 2, 3, ...][/code]. + [b]Behavior with different string lengths:[/b] Returns [code]1[/code] if the "base" string is longer than the [code]to[/code] string or [code]-1[/code] if the "base" string is shorter than the [code]to[/code] string. Keep in mind this length is determined by the number of Unicode codepoints, [i]not[/i] the actual visible characters. + [b]Behavior with empty strings:[/b] Returns [code]-1[/code] if the "base" string is empty, [code]1[/code] if the [code]to[/code] string is empty or [code]0[/code] if both strings are empty. + To get a boolean result from a string comparison, use the [code]==[/code] operator instead. See also [method nocasecmp_to] and [method casecmp_to]. </description> </method> <method name="nocasecmp_to"> @@ -721,7 +728,10 @@ <argument index="0" name="to" type="String"> </argument> <description> - Performs a case-insensitive comparison to another string. Returns [code]-1[/code] if less than, [code]+1[/code] if greater than, or [code]0[/code] if equal. + Performs a case-insensitive comparison to another string. Returns [code]-1[/code] if less than, [code]1[/code] if greater than, or [code]0[/code] if equal. "less than" or "greater than" are determined by the [url=https://en.wikipedia.org/wiki/List_of_Unicode_characters]Unicode code points[/code] of each string, which roughly matches the alphabetical order. Internally, lowercase characters will be converted to uppercase during the comparison. + [b]Behavior with different string lengths:[/b] Returns [code]1[/code] if the "base" string is longer than the [code]to[/code] string or [code]-1[/code] if the "base" string is shorter than the [code]to[/code] string. Keep in mind this length is determined by the number of Unicode codepoints, [i]not[/i] the actual visible characters. + [b]Behavior with empty strings:[/b] Returns [code]-1[/code] if the "base" string is empty, [code]1[/code] if the [code]to[/code] string is empty or [code]0[/code] if both strings are empty. + To get a boolean result from a string comparison, use the [code]==[/code] operator instead. See also [method casecmp_to] and [method naturalnocasecmp_to]. </description> </method> <method name="ord_at"> diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp index 402ec5d80d..470d19e65a 100644 --- a/editor/plugins/canvas_item_editor_plugin.cpp +++ b/editor/plugins/canvas_item_editor_plugin.cpp @@ -2599,12 +2599,28 @@ void CanvasItemEditor::_gui_input_viewport(const Ref<InputEvent> &p_event) { } void CanvasItemEditor::_update_cursor() { - CursorShape c = CURSOR_ARROW; - bool should_switch = false; - if (drag_selection.size() != 0) { - float angle = drag_selection[0]->_edit_get_rotation(); - should_switch = abs(Math::cos(angle)) < Math_SQRT12; + // Compute an eventual rotation of the cursor + CursorShape rotation_array[4] = { CURSOR_HSIZE, CURSOR_BDIAGSIZE, CURSOR_VSIZE, CURSOR_FDIAGSIZE }; + int rotation_array_index = 0; + + List<CanvasItem *> selection = _get_edited_canvas_items(); + if (selection.size() == 1) { + float angle = Math::fposmod((double)selection[0]->get_global_transform_with_canvas().get_rotation(), Math_PI); + if (angle > Math_PI * 7.0 / 8.0) { + rotation_array_index = 0; + } else if (angle > Math_PI * 5.0 / 8.0) { + rotation_array_index = 1; + } else if (angle > Math_PI * 3.0 / 8.0) { + rotation_array_index = 2; + } else if (angle > Math_PI * 1.0 / 8.0) { + rotation_array_index = 3; + } else { + rotation_array_index = 0; + } } + + // Choose the correct cursor + CursorShape c = CURSOR_ARROW; switch (drag_type) { case DRAG_NONE: switch (tool) { @@ -2626,38 +2642,28 @@ void CanvasItemEditor::_update_cursor() { break; case DRAG_LEFT: case DRAG_RIGHT: + c = rotation_array[rotation_array_index]; + break; case DRAG_V_GUIDE: - if (should_switch) { - c = CURSOR_VSIZE; - } else { - c = CURSOR_HSIZE; - } + c = CURSOR_HSIZE; break; case DRAG_TOP: case DRAG_BOTTOM: + c = rotation_array[(rotation_array_index + 2) % 4]; + break; case DRAG_H_GUIDE: - if (should_switch) { - c = CURSOR_HSIZE; - } else { - c = CURSOR_VSIZE; - } + c = CURSOR_VSIZE; break; case DRAG_TOP_LEFT: case DRAG_BOTTOM_RIGHT: + c = rotation_array[(rotation_array_index + 3) % 4]; + break; case DRAG_DOUBLE_GUIDE: - if (should_switch) { - c = CURSOR_BDIAGSIZE; - } else { - c = CURSOR_FDIAGSIZE; - } + c = CURSOR_FDIAGSIZE; break; case DRAG_TOP_RIGHT: case DRAG_BOTTOM_LEFT: - if (should_switch) { - c = CURSOR_FDIAGSIZE; - } else { - c = CURSOR_BDIAGSIZE; - } + c = rotation_array[(rotation_array_index + 1) % 4]; break; case DRAG_MOVE: c = CURSOR_MOVE; diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp index 944bf9913c..8580bdf47e 100644 --- a/editor/plugins/node_3d_editor_plugin.cpp +++ b/editor/plugins/node_3d_editor_plugin.cpp @@ -6810,7 +6810,7 @@ void EditorNode3DGizmoPlugin::_bind_methods() { ClassDB::bind_method(D_METHOD("get_material", "name", "gizmo"), &EditorNode3DGizmoPlugin::get_material); //, DEFVAL(Ref<EditorNode3DGizmo>())); BIND_VMETHOD(MethodInfo(Variant::STRING, "get_name")); - BIND_VMETHOD(MethodInfo(Variant::STRING, "get_priority")); + BIND_VMETHOD(MethodInfo(Variant::INT, "get_priority")); BIND_VMETHOD(MethodInfo(Variant::BOOL, "can_be_hidden")); BIND_VMETHOD(MethodInfo(Variant::BOOL, "is_selectable_when_hidden")); diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp index 03ed54ae1b..1651de4048 100644 --- a/editor/plugins/visual_shader_editor_plugin.cpp +++ b/editor/plugins/visual_shader_editor_plugin.cpp @@ -47,6 +47,27 @@ #include "servers/display_server.h" #include "servers/rendering/shader_types.h" +struct FloatConstantDef { + String name; + float value; + String desc; +}; + +static FloatConstantDef float_constant_defs[] = { + { "E", Math_E, TTR("E constant (2.718282). Represents the base of the natural logarithm.") }, + { "Epsilon", CMP_EPSILON, TTR("Epsilon constant (0.00001). Smallest possible scalar number.") }, + { "Phi", 1.618034f, TTR("Phi constant (1.618034). Golden ratio.") }, + { "Pi/4", Math_PI / 4, TTR("Pi/4 constant (0.785398) or 45 degrees.") }, + { "Pi/2", Math_PI / 2, TTR("Pi/2 constant (1.570796) or 90 degrees.") }, + { "Pi", Math_PI, TTR("Pi constant (3.141593) or 180 degrees.") }, + { "Tau", Math_TAU, TTR("Tau constant (6.283185) or 360 degrees.") }, + { "Sqrt2", Math_SQRT2, TTR("Sqrt2 constant (1.414214). Square root of 2.") } +}; + +const int MAX_FLOAT_CONST_DEFS = sizeof(float_constant_defs) / sizeof(FloatConstantDef); + +/////////////////// + Control *VisualShaderNodePlugin::create_editor(const Ref<Resource> &p_parent_resource, const Ref<VisualShaderNode> &p_node) { if (get_script_instance()) { return get_script_instance()->call("create_editor", p_parent_resource, p_node); @@ -86,6 +107,7 @@ void VisualShaderGraphPlugin::_bind_methods() { ClassDB::bind_method("update_node_deferred", &VisualShaderGraphPlugin::update_node_deferred); ClassDB::bind_method("set_input_port_default_value", &VisualShaderGraphPlugin::set_input_port_default_value); ClassDB::bind_method("set_uniform_name", &VisualShaderGraphPlugin::set_uniform_name); + ClassDB::bind_method("update_constant", &VisualShaderGraphPlugin::update_constant); } void VisualShaderGraphPlugin::register_shader(VisualShader *p_shader) { @@ -183,10 +205,42 @@ void VisualShaderGraphPlugin::set_uniform_name(VisualShader::Type p_type, int p_ } } +int VisualShaderGraphPlugin::get_constant_index(float p_constant) const { + for (int i = 0; i < MAX_FLOAT_CONST_DEFS; i++) { + if (Math::is_equal_approx(p_constant, float_constant_defs[i].value)) { + return i + 1; + } + } + return 0; +} + +void VisualShaderGraphPlugin::update_constant(VisualShader::Type p_type, int p_node_id) { + if (p_type != visual_shader->get_shader_type() || !links.has(p_node_id) || !links[p_node_id].const_op) { + return; + } + VisualShaderNodeFloatConstant *float_const = Object::cast_to<VisualShaderNodeFloatConstant>(links[p_node_id].visual_node); + if (!float_const) { + return; + } + links[p_node_id].const_op->select(get_constant_index(float_const->get_constant())); + links[p_node_id].graph_node->set_size(Size2(-1, -1)); +} + +void VisualShaderGraphPlugin::update_node_size(int p_node_id) { + if (!links.has(p_node_id)) { + return; + } + links[p_node_id].graph_node->set_size(Size2(-1, -1)); +} + void VisualShaderGraphPlugin::register_default_input_button(int p_node_id, int p_port_id, Button *p_button) { links[p_node_id].input_ports.insert(p_port_id, { p_button }); } +void VisualShaderGraphPlugin::register_constant_option_btn(int p_node_id, OptionButton *p_button) { + links[p_node_id].const_op = p_button; +} + void VisualShaderGraphPlugin::update_uniform_refs() { for (Map<int, Link>::Element *E = links.front(); E; E = E->next()) { VisualShaderNodeUniformRef *ref = Object::cast_to<VisualShaderNodeUniformRef>(E->get().visual_node); @@ -230,7 +284,7 @@ void VisualShaderGraphPlugin::make_dirty(bool p_enabled) { } void VisualShaderGraphPlugin::register_link(VisualShader::Type p_type, int p_id, VisualShaderNode *p_visual_node, GraphNode *p_graph_node) { - links.insert(p_id, { p_type, p_visual_node, p_graph_node, p_visual_node->get_output_port_for_preview() != -1, -1, Map<int, InputPort>(), Map<int, Port>(), nullptr, nullptr }); + links.insert(p_id, { p_type, p_visual_node, p_graph_node, p_visual_node->get_output_port_for_preview() != -1, -1, Map<int, InputPort>(), Map<int, Port>(), nullptr, nullptr, nullptr }); } void VisualShaderGraphPlugin::register_output_port(int p_node_id, int p_port, TextureButton *p_button) { @@ -337,6 +391,23 @@ void VisualShaderGraphPlugin::add_node(VisualShader::Type p_type, int p_id) { } } + Ref<VisualShaderNodeFloatConstant> float_const = vsnode; + if (float_const.is_valid()) { + HBoxContainer *hbox = memnew(HBoxContainer); + + hbox->add_child(custom_editor); + OptionButton *btn = memnew(OptionButton); + hbox->add_child(btn); + register_constant_option_btn(p_id, btn); + btn->add_item(""); + for (int i = 0; i < MAX_FLOAT_CONST_DEFS; i++) { + btn->add_item(float_constant_defs[i].name); + } + btn->select(get_constant_index(float_const->get_constant())); + btn->connect("item_selected", callable_mp(VisualShaderEditor::get_singleton(), &VisualShaderEditor::_float_constant_selected), varray(p_id)); + custom_editor = hbox; + } + if (custom_editor && !vsnode->is_use_prop_slots() && vsnode->get_output_port_count() > 0 && vsnode->get_output_port_name(0) == "" && (vsnode->get_input_port_count() == 0 || vsnode->get_input_port_name(0) == "")) { //will be embedded in first port } else if (custom_editor) { @@ -2450,6 +2521,30 @@ void VisualShaderEditor::_uniform_select_item(Ref<VisualShaderNodeUniformRef> p_ undo_redo->commit_action(); } +void VisualShaderEditor::_float_constant_selected(int p_index, int p_node) { + if (p_index == 0) { + graph_plugin->update_node_size(p_node); + return; + } + + --p_index; + + ERR_FAIL_INDEX(p_index, MAX_FLOAT_CONST_DEFS); + + VisualShader::Type type = get_current_shader_type(); + Ref<VisualShaderNodeFloatConstant> node = visual_shader->get_node(type, p_node); + if (!node.is_valid()) { + return; + } + + undo_redo->create_action(TTR("Set constant")); + undo_redo->add_do_method(node.ptr(), "set_constant", float_constant_defs[p_index].value); + undo_redo->add_undo_method(node.ptr(), "set_constant", node->get_constant()); + undo_redo->add_do_method(graph_plugin.ptr(), "update_constant", type, p_node); + undo_redo->add_undo_method(graph_plugin.ptr(), "update_constant", type, p_node); + undo_redo->commit_action(); +} + void VisualShaderEditor::_member_filter_changed(const String &p_text) { _update_options_menu(); } @@ -2695,6 +2790,7 @@ void VisualShaderEditor::_bind_methods() { ClassDB::bind_method("_update_uniforms", &VisualShaderEditor::_update_uniforms); ClassDB::bind_method("_set_mode", &VisualShaderEditor::_set_mode); ClassDB::bind_method("_nodes_dragged", &VisualShaderEditor::_nodes_dragged); + ClassDB::bind_method("_float_constant_selected", &VisualShaderEditor::_float_constant_selected); ClassDB::bind_method(D_METHOD("get_drag_data_fw"), &VisualShaderEditor::get_drag_data_fw); ClassDB::bind_method(D_METHOD("can_drop_data_fw"), &VisualShaderEditor::can_drop_data_fw); @@ -3163,15 +3259,9 @@ VisualShaderEditor::VisualShaderEditor() { //CONSTANTS - add_options.push_back(AddOption("E", "Scalar", "Constants", "VisualShaderNodeFloatConstant", TTR("E constant (2.718282). Represents the base of the natural logarithm."), -1, VisualShaderNode::PORT_TYPE_SCALAR, -1, -1, Math_E)); - add_options.push_back(AddOption("Epsilon", "Scalar", "Constants", "VisualShaderNodeFloatConstant", TTR("Epsilon constant (0.00001). Smallest possible scalar number."), -1, VisualShaderNode::PORT_TYPE_SCALAR, -1, -1, CMP_EPSILON)); - add_options.push_back(AddOption("Phi", "Scalar", "Constants", "VisualShaderNodeFloatConstant", TTR("Phi constant (1.618034). Golden ratio."), -1, VisualShaderNode::PORT_TYPE_SCALAR, -1, -1, 1.618034f)); - add_options.push_back(AddOption("Pi/4", "Scalar", "Constants", "VisualShaderNodeFloatConstant", TTR("Pi/4 constant (0.785398) or 45 degrees."), -1, VisualShaderNode::PORT_TYPE_SCALAR, -1, -1, Math_PI / 4)); - add_options.push_back(AddOption("Pi/2", "Scalar", "Constants", "VisualShaderNodeFloatConstant", TTR("Pi/2 constant (1.570796) or 90 degrees."), -1, VisualShaderNode::PORT_TYPE_SCALAR, -1, -1, Math_PI / 2)); - add_options.push_back(AddOption("Pi", "Scalar", "Constants", "VisualShaderNodeFloatConstant", TTR("Pi constant (3.141593) or 180 degrees."), -1, VisualShaderNode::PORT_TYPE_SCALAR, -1, -1, Math_PI)); - add_options.push_back(AddOption("Tau", "Scalar", "Constants", "VisualShaderNodeFloatConstant", TTR("Tau constant (6.283185) or 360 degrees."), -1, VisualShaderNode::PORT_TYPE_SCALAR, -1, -1, Math_TAU)); - add_options.push_back(AddOption("Sqrt2", "Scalar", "Constants", "VisualShaderNodeFloatConstant", TTR("Sqrt2 constant (1.414214). Square root of 2."), -1, VisualShaderNode::PORT_TYPE_SCALAR, -1, -1, Math_SQRT2)); - + for (int i = 0; i < MAX_FLOAT_CONST_DEFS; i++) { + add_options.push_back(AddOption(float_constant_defs[i].name, "Scalar", "Constants", "VisualShaderNodeFloatConstant", float_constant_defs[i].desc, -1, VisualShaderNode::PORT_TYPE_SCALAR, -1, -1, float_constant_defs[i].value)); + } // FUNCTIONS add_options.push_back(AddOption("Abs", "Scalar", "Functions", "VisualShaderNodeFloatFunc", TTR("Returns the absolute value of the parameter."), VisualShaderNodeFloatFunc::FUNC_ABS, VisualShaderNode::PORT_TYPE_SCALAR)); @@ -3553,6 +3643,9 @@ public: if (p_property != "constant") { undo_redo->add_do_method(VisualShaderEditor::get_singleton()->get_graph_plugin(), "update_node_deferred", shader_type, node_id); undo_redo->add_undo_method(VisualShaderEditor::get_singleton()->get_graph_plugin(), "update_node_deferred", shader_type, node_id); + } else { + undo_redo->add_do_method(VisualShaderEditor::get_singleton()->get_graph_plugin(), "update_constant", shader_type, node_id); + undo_redo->add_undo_method(VisualShaderEditor::get_singleton()->get_graph_plugin(), "update_constant", shader_type, node_id); } undo_redo->commit_action(); diff --git a/editor/plugins/visual_shader_editor_plugin.h b/editor/plugins/visual_shader_editor_plugin.h index b4c5ff886a..d43af82238 100644 --- a/editor/plugins/visual_shader_editor_plugin.h +++ b/editor/plugins/visual_shader_editor_plugin.h @@ -72,6 +72,7 @@ private: Map<int, Port> output_ports; VBoxContainer *preview_box; LineEdit *uniform_name; + OptionButton *const_op; }; Ref<VisualShader> visual_shader; @@ -88,6 +89,8 @@ public: void register_link(VisualShader::Type p_type, int p_id, VisualShaderNode *p_visual_node, GraphNode *p_graph_node); void register_output_port(int p_id, int p_port, TextureButton *p_button); void register_uniform_name(int p_id, LineEdit *p_uniform_name); + void register_default_input_button(int p_node_id, int p_port_id, Button *p_button); + void register_constant_option_btn(int p_node_id, OptionButton *p_button); void clear_links(); void set_shader_type(VisualShader::Type p_type); bool is_preview_visible(int p_id) const; @@ -104,9 +107,11 @@ public: void set_node_size(VisualShader::Type p_type, int p_id, const Vector2 &p_size); void refresh_node_ports(VisualShader::Type p_type, int p_node); void set_input_port_default_value(VisualShader::Type p_type, int p_node_id, int p_port_id, Variant p_value); - void register_default_input_button(int p_node_id, int p_port_id, Button *p_button); void update_uniform_refs(); void set_uniform_name(VisualShader::Type p_type, int p_node_id, const String &p_name); + void update_constant(VisualShader::Type p_type, int p_node_id); + int get_constant_index(float p_constant) const; + void update_node_size(int p_node_id); VisualShader::Type get_shader_type() const; VisualShaderGraphPlugin(); @@ -331,6 +336,8 @@ class VisualShaderEditor : public VBoxContainer { void _input_select_item(Ref<VisualShaderNodeInput> input, String name); void _uniform_select_item(Ref<VisualShaderNodeUniformRef> p_uniform, String p_name); + void _float_constant_selected(int p_index, int p_node); + VisualShader::Type get_current_shader_type() const; void _add_input_port(int p_node, int p_port, int p_port_type, const String &p_name); diff --git a/editor/scene_tree_editor.cpp b/editor/scene_tree_editor.cpp index 5a504da397..3dee4a229f 100644 --- a/editor/scene_tree_editor.cpp +++ b/editor/scene_tree_editor.cpp @@ -251,7 +251,7 @@ bool SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) { if (can_rename) { //should be can edit.. String warning = p_node->get_configuration_warning(); - if (warning != String()) { + if (!warning.empty()) { item->add_button(0, get_theme_icon("NodeWarning", "EditorIcons"), BUTTON_WARNING, false, TTR("Node configuration warning:") + "\n" + p_node->get_configuration_warning()); } diff --git a/main/main.cpp b/main/main.cpp index a2ea085618..5ebd0138d3 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -1369,6 +1369,7 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph "0,33200,1,or_greater")); // No negative numbers GLOBAL_DEF("display/window/ios/hide_home_indicator", true); + GLOBAL_DEF("input_devices/pointing/ios/touch_delay", 0.150); Engine::get_singleton()->set_frame_delay(frame_delay); diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 3519038ae6..e70e3f7272 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -1044,8 +1044,10 @@ GDScript::~GDScript() { MutexLock lock(GDScriptLanguage::get_singleton()->lock); while (SelfList<GDScriptFunctionState> *E = pending_func_states.first()) { - E->self()->_clear_stack(); + // Order matters since clearing the stack may already cause + // the GDSCriptFunctionState to be destroyed and thus removed from the list. pending_func_states.remove(E); + E->self()->_clear_stack(); } } @@ -1451,8 +1453,10 @@ GDScriptInstance::~GDScriptInstance() { MutexLock lock(GDScriptLanguage::get_singleton()->lock); while (SelfList<GDScriptFunctionState> *E = pending_func_states.first()) { - E->self()->_clear_stack(); + // Order matters since clearing the stack may already cause + // the GDSCriptFunctionState to be destroyed and thus removed from the list. pending_func_states.remove(E); + E->self()->_clear_stack(); } if (script.is_valid() && owner) { diff --git a/platform/iphone/SCsub b/platform/iphone/SCsub index 49c77468ed..848fd9713a 100644 --- a/platform/iphone/SCsub +++ b/platform/iphone/SCsub @@ -18,7 +18,7 @@ iphone_lib = [ "godot_view.mm", "display_layer.mm", "godot_view_renderer.mm", - "godot_view_gesture_recognizer.m", + "godot_view_gesture_recognizer.mm", ] env_ios = env.Clone() diff --git a/platform/iphone/game_center.h b/platform/iphone/game_center.h index 1e9a68fe48..6705674ac6 100644 --- a/platform/iphone/game_center.h +++ b/platform/iphone/game_center.h @@ -48,7 +48,7 @@ class GameCenter : public Object { void return_connect_error(const char *p_error_description); public: - void connect(); + Error authenticate(); bool is_authenticated(); Error post_score(Dictionary p_score); diff --git a/platform/iphone/game_center.mm b/platform/iphone/game_center.mm index 4481775c32..b237ba6bb6 100644 --- a/platform/iphone/game_center.mm +++ b/platform/iphone/game_center.mm @@ -52,6 +52,7 @@ extern "C" { GameCenter *GameCenter::instance = NULL; void GameCenter::_bind_methods() { + ClassDB::bind_method(D_METHOD("authenticate"), &GameCenter::authenticate); ClassDB::bind_method(D_METHOD("is_authenticated"), &GameCenter::is_authenticated); ClassDB::bind_method(D_METHOD("post_score"), &GameCenter::post_score); @@ -66,34 +67,17 @@ void GameCenter::_bind_methods() { ClassDB::bind_method(D_METHOD("pop_pending_event"), &GameCenter::pop_pending_event); }; -void GameCenter::return_connect_error(const char *p_error_description) { - authenticated = false; - Dictionary ret; - ret["type"] = "authentication"; - ret["result"] = "error"; - ret["error_code"] = 0; - ret["error_description"] = p_error_description; - pending_events.push_back(ret); -} - -void GameCenter::connect() { +Error GameCenter::authenticate() { //if this class isn't available, game center isn't implemented if ((NSClassFromString(@"GKLocalPlayer")) == nil) { - return_connect_error("GameCenter not available"); - return; + return ERR_UNAVAILABLE; } GKLocalPlayer *player = [GKLocalPlayer localPlayer]; - if (![player respondsToSelector:@selector(authenticateHandler)]) { - return_connect_error("GameCenter doesn't respond to 'authenticateHandler'"); - return; - } + ERR_FAIL_COND_V(![player respondsToSelector:@selector(authenticateHandler)], ERR_UNAVAILABLE); ViewController *root_controller = (ViewController *)((AppDelegate *)[[UIApplication sharedApplication] delegate]).window.rootViewController; - if (!root_controller) { - return_connect_error("Window doesn't have root ViewController"); - return; - } + ERR_FAIL_COND_V(!root_controller, FAILED); // This handler is called several times. First when the view needs to be shown, then again // after the view is cancelled or the user logs in. Or if the user's already logged in, it's @@ -126,6 +110,8 @@ void GameCenter::connect() { pending_events.push_back(ret); }; }); + + return OK; }; bool GameCenter::is_authenticated() { diff --git a/platform/iphone/godot_view_gesture_recognizer.h b/platform/iphone/godot_view_gesture_recognizer.h index 8d84914712..1431a9fb89 100644 --- a/platform/iphone/godot_view_gesture_recognizer.h +++ b/platform/iphone/godot_view_gesture_recognizer.h @@ -39,6 +39,8 @@ @interface GodotViewGestureRecognizer : UIGestureRecognizer +@property(nonatomic, readonly, assign) NSTimeInterval delayTimeInterval; + - (instancetype)init; @end diff --git a/platform/iphone/godot_view_gesture_recognizer.m b/platform/iphone/godot_view_gesture_recognizer.mm index 377ccd52a5..99ee42ecb3 100644 --- a/platform/iphone/godot_view_gesture_recognizer.m +++ b/platform/iphone/godot_view_gesture_recognizer.mm @@ -1,5 +1,5 @@ /*************************************************************************/ -/* godot_view_gesture_recognizer.m */ +/* godot_view_gesture_recognizer.mm */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -30,8 +30,7 @@ #import "godot_view_gesture_recognizer.h" -// Using same delay interval that is used for `UIScrollView` -const NSTimeInterval kGLGestureDelayInterval = 0.150; +#include "core/project_settings.h" // Minimum distance for touches to move to fire // a delay timer before scheduled time. @@ -41,6 +40,12 @@ const CGFloat kGLGestureMovementDistance = 0.5; @interface GodotViewGestureRecognizer () +@property(nonatomic, readwrite, assign) NSTimeInterval delayTimeInterval; + +@end + +@interface GodotViewGestureRecognizer () + // Timer used to delay begin touch message. // Should work as simple emulation of UIDelayedAction @property(strong, nonatomic) NSTimer *delayTimer; @@ -60,6 +65,8 @@ const CGFloat kGLGestureMovementDistance = 0.5; self.delaysTouchesBegan = YES; self.delaysTouchesEnded = YES; + self.delayTimeInterval = GLOBAL_GET("input_devices/pointing/ios/touch_delay"); + return self; } @@ -87,7 +94,7 @@ const CGFloat kGLGestureMovementDistance = 0.5; self.delayedEvent = event; self.delayTimer = [NSTimer - scheduledTimerWithTimeInterval:kGLGestureDelayInterval + scheduledTimerWithTimeInterval:self.delayTimeInterval target:self selector:@selector(fireDelayedTouches:) userInfo:nil diff --git a/platform/iphone/os_iphone.mm b/platform/iphone/os_iphone.mm index 946fd51923..5baa5b66fc 100644 --- a/platform/iphone/os_iphone.mm +++ b/platform/iphone/os_iphone.mm @@ -128,7 +128,6 @@ void OSIPhone::initialize_modules() { #ifdef GAME_CENTER_ENABLED game_center = memnew(GameCenter); Engine::get_singleton()->add_singleton(Engine::Singleton("GameCenter", game_center)); - game_center->connect(); #endif #ifdef STOREKIT_ENABLED diff --git a/platform/linuxbsd/display_server_x11.cpp b/platform/linuxbsd/display_server_x11.cpp index e3cf992302..8b0d08d1cb 100644 --- a/platform/linuxbsd/display_server_x11.cpp +++ b/platform/linuxbsd/display_server_x11.cpp @@ -322,23 +322,19 @@ bool DisplayServerX11::_refresh_device_info() { } void DisplayServerX11::_flush_mouse_motion() { - while (true) { - if (XPending(x11_display) > 0) { - XEvent event; - XPeekEvent(x11_display, &event); - - if (XGetEventData(x11_display, &event.xcookie) && event.xcookie.type == GenericEvent && event.xcookie.extension == xi.opcode) { - XIDeviceEvent *event_data = (XIDeviceEvent *)event.xcookie.data; - - if (event_data->evtype == XI_RawMotion) { - XNextEvent(x11_display, &event); - } else { - break; - } - } else { - break; + // Block events polling while flushing motion events. + MutexLock mutex_lock(events_mutex); + + for (uint32_t event_index = 0; event_index < polled_events.size(); ++event_index) { + XEvent &event = polled_events[event_index]; + if (XGetEventData(x11_display, &event.xcookie) && event.xcookie.type == GenericEvent && event.xcookie.extension == xi.opcode) { + XIDeviceEvent *event_data = (XIDeviceEvent *)event.xcookie.data; + if (event_data->evtype == XI_RawMotion) { + XFreeEventData(x11_display, &event.xcookie); + polled_events.remove(event_index--); + continue; } - } else { + XFreeEventData(x11_display, &event.xcookie); break; } } @@ -451,12 +447,25 @@ int DisplayServerX11::mouse_get_button_state() const { void DisplayServerX11::clipboard_set(const String &p_text) { _THREAD_SAFE_METHOD_ - internal_clipboard = p_text; + { + // The clipboard content can be accessed while polling for events. + MutexLock mutex_lock(events_mutex); + internal_clipboard = p_text; + } + XSetSelectionOwner(x11_display, XA_PRIMARY, windows[MAIN_WINDOW_ID].x11_window, CurrentTime); XSetSelectionOwner(x11_display, XInternAtom(x11_display, "CLIPBOARD", 0), windows[MAIN_WINDOW_ID].x11_window, CurrentTime); } -static String _clipboard_get_impl(Atom p_source, Window x11_window, ::Display *x11_display, String p_internal_clipboard, Atom target) { +Bool DisplayServerX11::_predicate_clipboard_selection(Display *display, XEvent *event, XPointer arg) { + if (event->type == SelectionNotify && event->xselection.requestor == *(Window *)arg) { + return True; + } else { + return False; + } +} + +String DisplayServerX11::_clipboard_get_impl(Atom p_source, Window x11_window, Atom target) const { String ret; Atom type; @@ -464,23 +473,27 @@ static String _clipboard_get_impl(Atom p_source, Window x11_window, ::Display *x int format, result; unsigned long len, bytes_left, dummy; unsigned char *data; - Window Sown = XGetSelectionOwner(x11_display, p_source); + Window selection_owner = XGetSelectionOwner(x11_display, p_source); - if (Sown == x11_window) { - return p_internal_clipboard; - }; + if (selection_owner == x11_window) { + return internal_clipboard; + } - if (Sown != None) { - XConvertSelection(x11_display, p_source, target, selection, - x11_window, CurrentTime); - XFlush(x11_display); - while (true) { + if (selection_owner != None) { + { + // Block events polling while processing selection events. + MutexLock mutex_lock(events_mutex); + + XConvertSelection(x11_display, p_source, target, selection, + x11_window, CurrentTime); + + XFlush(x11_display); + + // Blocking wait for predicate to be True + // and remove the event from the queue. XEvent event; - XNextEvent(x11_display, &event); - if (event.type == SelectionNotify && event.xselection.requestor == x11_window) { - break; - }; - }; + XIfEvent(x11_display, &event, _predicate_clipboard_selection, (XPointer)&x11_window); + } // // Do not get any data, see how much data is there @@ -514,14 +527,14 @@ static String _clipboard_get_impl(Atom p_source, Window x11_window, ::Display *x return ret; } -static String _clipboard_get(Atom p_source, Window x11_window, ::Display *x11_display, String p_internal_clipboard) { +String DisplayServerX11::_clipboard_get(Atom p_source, Window x11_window) const { String ret; Atom utf8_atom = XInternAtom(x11_display, "UTF8_STRING", True); if (utf8_atom != None) { - ret = _clipboard_get_impl(p_source, x11_window, x11_display, p_internal_clipboard, utf8_atom); + ret = _clipboard_get_impl(p_source, x11_window, utf8_atom); } - if (ret == "") { - ret = _clipboard_get_impl(p_source, x11_window, x11_display, p_internal_clipboard, XA_STRING); + if (ret.empty()) { + ret = _clipboard_get_impl(p_source, x11_window, XA_STRING); } return ret; } @@ -530,11 +543,11 @@ String DisplayServerX11::clipboard_get() const { _THREAD_SAFE_METHOD_ String ret; - ret = _clipboard_get(XInternAtom(x11_display, "CLIPBOARD", 0), windows[MAIN_WINDOW_ID].x11_window, x11_display, internal_clipboard); + ret = _clipboard_get(XInternAtom(x11_display, "CLIPBOARD", 0), windows[MAIN_WINDOW_ID].x11_window); - if (ret == "") { - ret = _clipboard_get(XA_PRIMARY, windows[MAIN_WINDOW_ID].x11_window, x11_display, internal_clipboard); - }; + if (ret.empty()) { + ret = _clipboard_get(XA_PRIMARY, windows[MAIN_WINDOW_ID].x11_window); + } return ret; } @@ -1649,10 +1662,16 @@ void DisplayServerX11::window_set_ime_active(const bool p_active, WindowID p_win return; } + // Block events polling while changing input focus + // because it triggers some event polling internally. if (p_active) { - XSetICFocus(wd.xic); + { + MutexLock mutex_lock(events_mutex); + XSetICFocus(wd.xic); + } window_set_ime_position(wd.im_position, p_window); } else { + MutexLock mutex_lock(events_mutex); XUnsetICFocus(wd.xic); } } @@ -1673,7 +1692,14 @@ void DisplayServerX11::window_set_ime_position(const Point2i &p_pos, WindowID p_ spot.x = short(p_pos.x); spot.y = short(p_pos.y); XVaNestedList preedit_attr = XVaCreateNestedList(0, XNSpotLocation, &spot, nullptr); - XSetICValues(wd.xic, XNPreeditAttributes, preedit_attr, nullptr); + + { + // Block events polling during this call + // because it triggers some event polling internally. + MutexLock mutex_lock(events_mutex); + XSetICValues(wd.xic, XNPreeditAttributes, preedit_attr, nullptr); + } + XFree(preedit_attr); } @@ -1993,7 +2019,7 @@ unsigned int DisplayServerX11::_get_mouse_button_state(unsigned int p_x11_button return last_button_state; } -void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, bool p_echo) { +void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, LocalVector<XEvent> &p_events, uint32_t &p_event_index, bool p_echo) { WindowData wd = windows[p_window]; // X11 functions don't know what const is XKeyEvent *xkeyevent = p_event; @@ -2130,7 +2156,7 @@ void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, /* Phase 4, determine if event must be filtered */ // This seems to be a side-effect of using XIM. - // XEventFilter looks like a core X11 function, + // XFilterEvent looks like a core X11 function, // but it's actually just used to see if we must // ignore a deadkey, or events XIM determines // must not reach the actual gui. @@ -2164,17 +2190,16 @@ void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, // Echo characters in X11 are a keyrelease and a keypress // one after the other with the (almot) same timestamp. - // To detect them, i use XPeekEvent and check that their - // difference in time is below a threshold. + // To detect them, i compare to the next event in list and + // check that their difference in time is below a threshold. if (xkeyevent->type != KeyPress) { p_echo = false; // make sure there are events pending, // so this call won't block. - if (XPending(x11_display) > 0) { - XEvent peek_event; - XPeekEvent(x11_display, &peek_event); + if (p_event_index + 1 < p_events.size()) { + XEvent &peek_event = p_events[p_event_index + 1]; // I'm using a threshold of 5 msecs, // since sometimes there seems to be a little @@ -2189,9 +2214,9 @@ void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, KeySym rk; XLookupString((XKeyEvent *)&peek_event, str, 256, &rk, nullptr); if (rk == keysym_keycode) { - XEvent event; - XNextEvent(x11_display, &event); //erase next event - _handle_key_event(p_window, (XKeyEvent *)&event, true); + // Consume to next event. + ++p_event_index; + _handle_key_event(p_window, (XKeyEvent *)&peek_event, p_events, p_event_index, true); return; //ignore current, echo next } } @@ -2246,6 +2271,66 @@ void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, Input::get_singleton()->accumulate_input_event(k); } +void DisplayServerX11::_handle_selection_request_event(XSelectionRequestEvent *p_event) { + XEvent respond; + if (p_event->target == XInternAtom(x11_display, "UTF8_STRING", 0) || + p_event->target == XInternAtom(x11_display, "COMPOUND_TEXT", 0) || + p_event->target == XInternAtom(x11_display, "TEXT", 0) || + p_event->target == XA_STRING || + p_event->target == XInternAtom(x11_display, "text/plain;charset=utf-8", 0) || + p_event->target == XInternAtom(x11_display, "text/plain", 0)) { + // Directly using internal clipboard because we know our window + // is the owner during a selection request. + CharString clip = internal_clipboard.utf8(); + XChangeProperty(x11_display, + p_event->requestor, + p_event->property, + p_event->target, + 8, + PropModeReplace, + (unsigned char *)clip.get_data(), + clip.length()); + respond.xselection.property = p_event->property; + } else if (p_event->target == XInternAtom(x11_display, "TARGETS", 0)) { + Atom data[7]; + data[0] = XInternAtom(x11_display, "TARGETS", 0); + data[1] = XInternAtom(x11_display, "UTF8_STRING", 0); + data[2] = XInternAtom(x11_display, "COMPOUND_TEXT", 0); + data[3] = XInternAtom(x11_display, "TEXT", 0); + data[4] = XA_STRING; + data[5] = XInternAtom(x11_display, "text/plain;charset=utf-8", 0); + data[6] = XInternAtom(x11_display, "text/plain", 0); + + XChangeProperty(x11_display, + p_event->requestor, + p_event->property, + XA_ATOM, + 32, + PropModeReplace, + (unsigned char *)&data, + sizeof(data) / sizeof(data[0])); + respond.xselection.property = p_event->property; + + } else { + char *targetname = XGetAtomName(x11_display, p_event->target); + printf("No Target '%s'\n", targetname); + if (targetname) { + XFree(targetname); + } + respond.xselection.property = None; + } + + respond.xselection.type = SelectionNotify; + respond.xselection.display = p_event->display; + respond.xselection.requestor = p_event->requestor; + respond.xselection.selection = p_event->selection; + respond.xselection.target = p_event->target; + respond.xselection.time = p_event->time; + + XSendEvent(x11_display, p_event->requestor, True, NoEventMask, &respond); + XFlush(x11_display); +} + void DisplayServerX11::_xim_destroy_callback(::XIM im, ::XPointer client_data, ::XPointer call_data) { WARN_PRINT("Input method stopped"); @@ -2358,6 +2443,65 @@ void DisplayServerX11::_send_window_event(const WindowData &wd, WindowEvent p_ev } } +void DisplayServerX11::_poll_events_thread(void *ud) { + DisplayServerX11 *display_server = (DisplayServerX11 *)ud; + display_server->_poll_events(); +} + +Bool DisplayServerX11::_predicate_all_events(Display *display, XEvent *event, XPointer arg) { + // Just accept all events. + return True; +} + +void DisplayServerX11::_poll_events() { + int x11_fd = ConnectionNumber(x11_display); + fd_set in_fds; + + while (!events_thread_done) { + XFlush(x11_display); + + FD_ZERO(&in_fds); + FD_SET(x11_fd, &in_fds); + + struct timeval tv; + tv.tv_usec = 0; + tv.tv_sec = 1; + + // Wait for next event or timeout. + int num_ready_fds = select(x11_fd + 1, &in_fds, NULL, NULL, &tv); + if (num_ready_fds < 0) { + ERR_PRINT("_poll_events: select error: " + itos(errno)); + } + + // Process events from the queue. + { + MutexLock mutex_lock(events_mutex); + + // Non-blocking wait for next event + // and remove it from the queue. + XEvent ev; + while (XCheckIfEvent(x11_display, &ev, _predicate_all_events, nullptr)) { + // Check if the input manager wants to process the event. + if (XFilterEvent(&ev, None)) { + // Event has been filtered by the Input Manager, + // it has to be ignored and a new one will be received. + continue; + } + + // Handle selection request events directly in the event thread, because + // communication through the x server takes several events sent back and forth + // and we don't want to block other programs while processing only one each frame. + if (ev.type == SelectionRequest) { + _handle_selection_request_event(&(ev.xselectionrequest)); + continue; + } + + polled_events.push_back(ev); + } + } + } +} + void DisplayServerX11::process_events() { _THREAD_SAFE_METHOD_ @@ -2401,13 +2545,16 @@ void DisplayServerX11::process_events() { xi.tilt = Vector2(); xi.pressure_supported = false; - while (XPending(x11_display) > 0) { - XEvent event; - XNextEvent(x11_display, &event); + LocalVector<XEvent> events; + { + // Block events polling while flushing events. + MutexLock mutex_lock(events_mutex); + events = polled_events; + polled_events.clear(); + } - if (XFilterEvent(&event, None)) { - continue; - } + for (uint32_t event_index = 0; event_index < events.size(); ++event_index) { + XEvent &event = events[event_index]; WindowID window_id = MAIN_WINDOW_ID; @@ -2666,6 +2813,9 @@ void DisplayServerX11::process_events() { }*/ #endif if (wd.xic) { + // Block events polling while changing input focus + // because it triggers some event polling internally. + MutexLock mutex_lock(events_mutex); XSetICFocus(wd.xic); } @@ -2715,7 +2865,10 @@ void DisplayServerX11::process_events() { xi.state.clear(); #endif if (wd.xic) { - XSetICFocus(wd.xic); + // Block events polling while changing input focus + // because it triggers some event polling internally. + MutexLock mutex_lock(events_mutex); + XUnsetICFocus(wd.xic); } } break; @@ -2834,11 +2987,11 @@ void DisplayServerX11::process_events() { break; } - if (XPending(x11_display) > 0) { - XEvent tevent; - XPeekEvent(x11_display, &tevent); - if (tevent.type == MotionNotify) { - XNextEvent(x11_display, &event); + if (event_index + 1 < events.size()) { + const XEvent &next_event = events[event_index + 1]; + if (next_event.type == MotionNotify) { + ++event_index; + event = next_event; } else { break; } @@ -2969,67 +3122,7 @@ void DisplayServerX11::process_events() { // key event is a little complex, so // it will be handled in its own function. - _handle_key_event(window_id, (XKeyEvent *)&event); - } break; - case SelectionRequest: { - XSelectionRequestEvent *req; - XEvent e, respond; - e = event; - - req = &(e.xselectionrequest); - if (req->target == XInternAtom(x11_display, "UTF8_STRING", 0) || - req->target == XInternAtom(x11_display, "COMPOUND_TEXT", 0) || - req->target == XInternAtom(x11_display, "TEXT", 0) || - req->target == XA_STRING || - req->target == XInternAtom(x11_display, "text/plain;charset=utf-8", 0) || - req->target == XInternAtom(x11_display, "text/plain", 0)) { - CharString clip = clipboard_get().utf8(); - XChangeProperty(x11_display, - req->requestor, - req->property, - req->target, - 8, - PropModeReplace, - (unsigned char *)clip.get_data(), - clip.length()); - respond.xselection.property = req->property; - } else if (req->target == XInternAtom(x11_display, "TARGETS", 0)) { - Atom data[7]; - data[0] = XInternAtom(x11_display, "TARGETS", 0); - data[1] = XInternAtom(x11_display, "UTF8_STRING", 0); - data[2] = XInternAtom(x11_display, "COMPOUND_TEXT", 0); - data[3] = XInternAtom(x11_display, "TEXT", 0); - data[4] = XA_STRING; - data[5] = XInternAtom(x11_display, "text/plain;charset=utf-8", 0); - data[6] = XInternAtom(x11_display, "text/plain", 0); - - XChangeProperty(x11_display, - req->requestor, - req->property, - XA_ATOM, - 32, - PropModeReplace, - (unsigned char *)&data, - sizeof(data) / sizeof(data[0])); - respond.xselection.property = req->property; - - } else { - char *targetname = XGetAtomName(x11_display, req->target); - printf("No Target '%s'\n", targetname); - if (targetname) { - XFree(targetname); - } - respond.xselection.property = None; - } - - respond.xselection.type = SelectionNotify; - respond.xselection.display = req->display; - respond.xselection.requestor = req->requestor; - respond.xselection.selection = req->selection; - respond.xselection.target = req->target; - respond.xselection.time = req->time; - XSendEvent(x11_display, req->requestor, True, NoEventMask, &respond); - XFlush(x11_display); + _handle_key_event(window_id, (XKeyEvent *)&event, events, event_index); } break; case SelectionNotify: @@ -3424,6 +3517,10 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u XChangeProperty(x11_display, wd.x11_window, xdnd_aware, XA_ATOM, 32, PropModeReplace, (unsigned char *)&xdnd_version, 1); if (xim && xim_style) { + // Block events polling while changing input focus + // because it triggers some event polling internally. + MutexLock mutex_lock(events_mutex); + wd.xic = XCreateIC(xim, XNInputStyle, xim_style, XNClientWindow, wd.x11_window, XNFocusWindow, wd.x11_window, (char *)nullptr); if (XGetICValues(wd.xic, XNFilterEvents, &im_event_mask, nullptr) != nullptr) { WARN_PRINT("XGetICValues couldn't obtain XNFilterEvents value"); @@ -3926,12 +4023,19 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode } } + events_thread = Thread::create(_poll_events_thread, this); + _update_real_mouse_position(windows[MAIN_WINDOW_ID]); r_error = OK; } DisplayServerX11::~DisplayServerX11() { + events_thread_done = true; + Thread::wait_to_finish(events_thread); + memdelete(events_thread); + events_thread = nullptr; + //destroy all windows for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { #ifdef VULKAN_ENABLED diff --git a/platform/linuxbsd/display_server_x11.h b/platform/linuxbsd/display_server_x11.h index e894cf515c..740bf81fd9 100644 --- a/platform/linuxbsd/display_server_x11.h +++ b/platform/linuxbsd/display_server_x11.h @@ -36,6 +36,7 @@ #include "servers/display_server.h" #include "core/input/input.h" +#include "core/local_vector.h" #include "drivers/alsa/audio_driver_alsa.h" #include "drivers/alsamidi/midi_driver_alsamidi.h" #include "drivers/pulseaudio/audio_driver_pulseaudio.h" @@ -202,7 +203,11 @@ class DisplayServerX11 : public DisplayServer { MouseMode mouse_mode; Point2i center; - void _handle_key_event(WindowID p_window, XKeyEvent *p_event, bool p_echo = false); + void _handle_key_event(WindowID p_window, XKeyEvent *p_event, LocalVector<XEvent> &p_events, uint32_t &p_event_index, bool p_echo = false); + void _handle_selection_request_event(XSelectionRequestEvent *p_event); + + String _clipboard_get_impl(Atom p_source, Window x11_window, Atom target) const; + String _clipboard_get(Atom p_source, Window x11_window) const; //bool minimized; //bool window_has_focus; @@ -252,6 +257,16 @@ class DisplayServerX11 : public DisplayServer { static void _dispatch_input_events(const Ref<InputEvent> &p_event); void _dispatch_input_event(const Ref<InputEvent> &p_event); + mutable Mutex events_mutex; + Thread *events_thread = nullptr; + bool events_thread_done = false; + LocalVector<XEvent> polled_events; + static void _poll_events_thread(void *ud); + void _poll_events(); + + static Bool _predicate_all_events(Display *display, XEvent *event, XPointer arg); + static Bool _predicate_clipboard_selection(Display *display, XEvent *event, XPointer arg); + protected: void _window_changed(XEvent *event); diff --git a/scene/2d/animated_sprite_2d.cpp b/scene/2d/animated_sprite_2d.cpp index 3268544519..b3370863e1 100644 --- a/scene/2d/animated_sprite_2d.cpp +++ b/scene/2d/animated_sprite_2d.cpp @@ -660,11 +660,16 @@ StringName AnimatedSprite2D::get_animation() const { } String AnimatedSprite2D::get_configuration_warning() const { + String warning = Node2D::get_configuration_warning(); + if (frames.is_null()) { - return TTR("A SpriteFrames resource must be created or set in the \"Frames\" property in order for AnimatedSprite to display frames."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("A SpriteFrames resource must be created or set in the \"Frames\" property in order for AnimatedSprite to display frames."); } - return String(); + return warning; } void AnimatedSprite2D::set_specular_color(const Color &p_color) { diff --git a/scene/2d/canvas_modulate.cpp b/scene/2d/canvas_modulate.cpp index 56643542a8..8fb16534e8 100644 --- a/scene/2d/canvas_modulate.cpp +++ b/scene/2d/canvas_modulate.cpp @@ -78,14 +78,19 @@ String CanvasModulate::get_configuration_warning() const { return String(); } + String warning = Node2D::get_configuration_warning(); + List<Node *> nodes; get_tree()->get_nodes_in_group("_canvas_modulate_" + itos(get_canvas().get_id()), &nodes); if (nodes.size() > 1) { - return TTR("Only one visible CanvasModulate is allowed per scene (or set of instanced scenes). The first created one will work, while the rest will be ignored."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("Only one visible CanvasModulate is allowed per scene (or set of instanced scenes). The first created one will work, while the rest will be ignored."); } - return String(); + return warning; } CanvasModulate::CanvasModulate() { diff --git a/scene/2d/collision_polygon_2d.cpp b/scene/2d/collision_polygon_2d.cpp index d23398713a..3cbb0f72f8 100644 --- a/scene/2d/collision_polygon_2d.cpp +++ b/scene/2d/collision_polygon_2d.cpp @@ -230,15 +230,23 @@ bool CollisionPolygon2D::_edit_is_selected_on_click(const Point2 &p_point, doubl #endif String CollisionPolygon2D::get_configuration_warning() const { + String warning = Node2D::get_configuration_warning(); + if (!Object::cast_to<CollisionObject2D>(get_parent())) { - return TTR("CollisionPolygon2D only serves to provide a collision shape to a CollisionObject2D derived node. Please only use it as a child of Area2D, StaticBody2D, RigidBody2D, KinematicBody2D, etc. to give them a shape."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("CollisionPolygon2D only serves to provide a collision shape to a CollisionObject2D derived node. Please only use it as a child of Area2D, StaticBody2D, RigidBody2D, KinematicBody2D, etc. to give them a shape."); } if (polygon.empty()) { - return TTR("An empty CollisionPolygon2D has no effect on collision."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("An empty CollisionPolygon2D has no effect on collision."); } - return String(); + return warning; } void CollisionPolygon2D::set_disabled(bool p_disabled) { diff --git a/scene/2d/cpu_particles_2d.cpp b/scene/2d/cpu_particles_2d.cpp index dd21fcfe22..8d1162b7e3 100644 --- a/scene/2d/cpu_particles_2d.cpp +++ b/scene/2d/cpu_particles_2d.cpp @@ -254,7 +254,7 @@ bool CPUParticles2D::get_fractional_delta() const { } String CPUParticles2D::get_configuration_warning() const { - String warnings; + String warnings = Node2D::get_configuration_warning(); CanvasItemMaterial *mat = Object::cast_to<CanvasItemMaterial>(get_material().ptr()); diff --git a/scene/2d/gpu_particles_2d.cpp b/scene/2d/gpu_particles_2d.cpp index 0814fbb549..01da36ec34 100644 --- a/scene/2d/gpu_particles_2d.cpp +++ b/scene/2d/gpu_particles_2d.cpp @@ -222,7 +222,7 @@ String GPUParticles2D::get_configuration_warning() const { return TTR("GPU-based particles are not supported by the GLES2 video driver.\nUse the CPUParticles2D node instead. You can use the \"Convert to CPUParticles2D\" option for this purpose."); } - String warnings; + String warnings = Node2D::get_configuration_warning(); if (process_material.is_null()) { if (warnings != String()) { diff --git a/scene/2d/light_2d.cpp b/scene/2d/light_2d.cpp index 1e7e9f6b6a..52bf122789 100644 --- a/scene/2d/light_2d.cpp +++ b/scene/2d/light_2d.cpp @@ -310,11 +310,15 @@ void Light2D::_notification(int p_what) { } String Light2D::get_configuration_warning() const { + String warning = Node2D::get_configuration_warning(); + if (!texture.is_valid()) { - return TTR("A texture with the shape of the light must be supplied to the \"Texture\" property."); + if (!warning.empty()) + warning += "\n\n"; + warning += TTR("A texture with the shape of the light must be supplied to the \"Texture\" property."); } - return String(); + return warning; } void Light2D::set_shadow_smooth(float p_amount) { diff --git a/scene/2d/light_occluder_2d.cpp b/scene/2d/light_occluder_2d.cpp index 023cfa6d03..b2ea60f895 100644 --- a/scene/2d/light_occluder_2d.cpp +++ b/scene/2d/light_occluder_2d.cpp @@ -246,15 +246,23 @@ int LightOccluder2D::get_occluder_light_mask() const { } String LightOccluder2D::get_configuration_warning() const { + String warning = Node2D::get_configuration_warning(); + if (!occluder_polygon.is_valid()) { - return TTR("An occluder polygon must be set (or drawn) for this occluder to take effect."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("An occluder polygon must be set (or drawn) for this occluder to take effect."); } if (occluder_polygon.is_valid() && occluder_polygon->get_polygon().size() == 0) { - return TTR("The occluder polygon for this occluder is empty. Please draw a polygon."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("The occluder polygon for this occluder is empty. Please draw a polygon."); } - return String(); + return warning; } void LightOccluder2D::_bind_methods() { diff --git a/scene/2d/navigation_agent_2d.cpp b/scene/2d/navigation_agent_2d.cpp index e5cdade4a4..9949ac6228 100644 --- a/scene/2d/navigation_agent_2d.cpp +++ b/scene/2d/navigation_agent_2d.cpp @@ -271,11 +271,16 @@ void NavigationAgent2D::_avoidance_done(Vector3 p_new_velocity) { } String NavigationAgent2D::get_configuration_warning() const { + String warning = Node::get_configuration_warning(); + if (!Object::cast_to<Node2D>(get_parent())) { - return TTR("The NavigationAgent2D can be used only under a Node2D node"); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("The NavigationAgent2D can be used only under a Node2D node"); } - return String(); + return warning; } void NavigationAgent2D::update_navigation() { diff --git a/scene/2d/navigation_obstacle_2d.cpp b/scene/2d/navigation_obstacle_2d.cpp index 568023bbe2..252d7cbb96 100644 --- a/scene/2d/navigation_obstacle_2d.cpp +++ b/scene/2d/navigation_obstacle_2d.cpp @@ -106,11 +106,16 @@ Node *NavigationObstacle2D::get_navigation_node() const { } String NavigationObstacle2D::get_configuration_warning() const { + String warning = Node::get_configuration_warning(); + if (!Object::cast_to<Node2D>(get_parent())) { - return TTR("The NavigationObstacle2D only serves to provide collision avoidance to a Node2D object."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("The NavigationObstacle2D only serves to provide collision avoidance to a Node2D object."); } - return String(); + return warning; } void NavigationObstacle2D::update_agent_shape() { diff --git a/scene/2d/navigation_region_2d.cpp b/scene/2d/navigation_region_2d.cpp index 671bda558d..67e7176e6e 100644 --- a/scene/2d/navigation_region_2d.cpp +++ b/scene/2d/navigation_region_2d.cpp @@ -500,19 +500,26 @@ String NavigationRegion2D::get_configuration_warning() const { return String(); } + String warning = Node2D::get_configuration_warning(); + if (!navpoly.is_valid()) { - return TTR("A NavigationPolygon resource must be set or created for this node to work. Please set a property or draw a polygon."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("A NavigationPolygon resource must be set or created for this node to work. Please set a property or draw a polygon."); } const Node2D *c = this; while (c) { if (Object::cast_to<Navigation2D>(c)) { - return String(); + return warning; } c = Object::cast_to<Node2D>(c->get_parent()); } - - return TTR("NavigationRegion2D must be a child or grandchild to a Navigation2D node. It only provides navigation data."); + if (!warning.empty()) { + warning += "\n\n"; + } + return warning + TTR("NavigationRegion2D must be a child or grandchild to a Navigation2D node. It only provides navigation data."); } void NavigationRegion2D::_bind_methods() { diff --git a/scene/2d/parallax_layer.cpp b/scene/2d/parallax_layer.cpp index 4ed335dec8..ec1f1f40fa 100644 --- a/scene/2d/parallax_layer.cpp +++ b/scene/2d/parallax_layer.cpp @@ -136,11 +136,16 @@ void ParallaxLayer::set_base_offset_and_scale(const Point2 &p_offset, float p_sc } String ParallaxLayer::get_configuration_warning() const { + String warning = Node2D::get_configuration_warning(); + if (!Object::cast_to<ParallaxBackground>(get_parent())) { - return TTR("ParallaxLayer node only works when set as child of a ParallaxBackground node."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("ParallaxLayer node only works when set as child of a ParallaxBackground node."); } - return String(); + return warning; } void ParallaxLayer::_bind_methods() { diff --git a/scene/2d/path_2d.cpp b/scene/2d/path_2d.cpp index f2f549e851..d5994422e7 100644 --- a/scene/2d/path_2d.cpp +++ b/scene/2d/path_2d.cpp @@ -254,11 +254,16 @@ String PathFollow2D::get_configuration_warning() const { return String(); } + String warning = Node2D::get_configuration_warning(); + if (!Object::cast_to<Path2D>(get_parent())) { - return TTR("PathFollow2D only works when set as a child of a Path2D node."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("PathFollow2D only works when set as a child of a Path2D node."); } - return String(); + return warning; } void PathFollow2D::_bind_methods() { diff --git a/scene/2d/physics_body_2d.cpp b/scene/2d/physics_body_2d.cpp index 0a9de20664..22d426bdb4 100644 --- a/scene/2d/physics_body_2d.cpp +++ b/scene/2d/physics_body_2d.cpp @@ -748,7 +748,7 @@ String RigidBody2D::get_configuration_warning() const { String warning = CollisionObject2D::get_configuration_warning(); if ((get_mode() == MODE_RIGID || get_mode() == MODE_CHARACTER) && (ABS(t.elements[0].length() - 1.0) > 0.05 || ABS(t.elements[1].length() - 1.0) > 0.05)) { - if (warning != String()) { + if (!warning.empty()) { warning += "\n\n"; } warning += TTR("Size changes to RigidBody2D (in character or rigid modes) will be overridden by the physics engine when running.\nChange the size in children collision shapes instead."); diff --git a/scene/2d/remote_transform_2d.cpp b/scene/2d/remote_transform_2d.cpp index 3104436dbe..7655416ce2 100644 --- a/scene/2d/remote_transform_2d.cpp +++ b/scene/2d/remote_transform_2d.cpp @@ -186,11 +186,16 @@ void RemoteTransform2D::force_update_cache() { } String RemoteTransform2D::get_configuration_warning() const { + String warning = Node2D::get_configuration_warning(); + if (!has_node(remote_node) || !Object::cast_to<Node2D>(get_node(remote_node))) { - return TTR("Path property must point to a valid Node2D node to work."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("Path property must point to a valid Node2D node to work."); } - return String(); + return warning; } void RemoteTransform2D::_bind_methods() { diff --git a/scene/2d/skeleton_2d.cpp b/scene/2d/skeleton_2d.cpp index ea37c8dfe7..ea1d9f5930 100644 --- a/scene/2d/skeleton_2d.cpp +++ b/scene/2d/skeleton_2d.cpp @@ -136,7 +136,7 @@ int Bone2D::get_index_in_skeleton() const { String Bone2D::get_configuration_warning() const { String warning = Node2D::get_configuration_warning(); if (!skeleton) { - if (warning != String()) { + if (!warning.empty()) { warning += "\n\n"; } if (parent_bone) { @@ -147,7 +147,7 @@ String Bone2D::get_configuration_warning() const { } if (rest == Transform2D(0, 0, 0, 0, 0, 0)) { - if (warning != String()) { + if (!warning.empty()) { warning += "\n\n"; } warning += TTR("This bone lacks a proper REST pose. Go to the Skeleton2D node and set one."); diff --git a/scene/2d/visibility_notifier_2d.cpp b/scene/2d/visibility_notifier_2d.cpp index 75154c7acb..9cec589cfb 100644 --- a/scene/2d/visibility_notifier_2d.cpp +++ b/scene/2d/visibility_notifier_2d.cpp @@ -313,12 +313,17 @@ void VisibilityEnabler2D::_node_removed(Node *p_node) { } String VisibilityEnabler2D::get_configuration_warning() const { + String warning = VisibilityNotifier2D::get_configuration_warning(); + #ifdef TOOLS_ENABLED if (is_inside_tree() && get_parent() && (get_parent()->get_filename() == String() && get_parent() != get_tree()->get_edited_scene_root())) { - return TTR("VisibilityEnabler2D works best when used with the edited scene root directly as parent."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("VisibilityEnabler2D works best when used with the edited scene root directly as parent."); } #endif - return String(); + return warning; } void VisibilityEnabler2D::_bind_methods() { diff --git a/scene/3d/collision_polygon_3d.cpp b/scene/3d/collision_polygon_3d.cpp index e2d11c740a..b8a4ab74ee 100644 --- a/scene/3d/collision_polygon_3d.cpp +++ b/scene/3d/collision_polygon_3d.cpp @@ -156,15 +156,23 @@ bool CollisionPolygon3D::is_disabled() const { } String CollisionPolygon3D::get_configuration_warning() const { + String warning = Node3D::get_configuration_warning(); + if (!Object::cast_to<CollisionObject3D>(get_parent())) { - return TTR("CollisionPolygon3D only serves to provide a collision shape to a CollisionObject3D derived node. Please only use it as a child of Area3D, StaticBody3D, RigidBody3D, KinematicBody3D, etc. to give them a shape."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("CollisionPolygon3D only serves to provide a collision shape to a CollisionObject3D derived node. Please only use it as a child of Area3D, StaticBody3D, RigidBody3D, KinematicBody3D, etc. to give them a shape."); } if (polygon.empty()) { - return TTR("An empty CollisionPolygon3D has no effect on collision."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("An empty CollisionPolygon3D has no effect on collision."); } - return String(); + return warning; } bool CollisionPolygon3D::_is_editable_3d_polygon() const { diff --git a/scene/3d/collision_shape_3d.cpp b/scene/3d/collision_shape_3d.cpp index e7f3f53ca9..6ff0ce6032 100644 --- a/scene/3d/collision_shape_3d.cpp +++ b/scene/3d/collision_shape_3d.cpp @@ -124,23 +124,34 @@ void CollisionShape3D::resource_changed(RES res) { } String CollisionShape3D::get_configuration_warning() const { + String warning = Node3D::get_configuration_warning(); + if (!Object::cast_to<CollisionObject3D>(get_parent())) { - return TTR("CollisionShape3D only serves to provide a collision shape to a CollisionObject3D derived node. Please only use it as a child of Area3D, StaticBody3D, RigidBody3D, KinematicBody3D, etc. to give them a shape."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("CollisionShape3D only serves to provide a collision shape to a CollisionObject3D derived node. Please only use it as a child of Area3D, StaticBody3D, RigidBody3D, KinematicBody3D, etc. to give them a shape."); } if (!shape.is_valid()) { - return TTR("A shape must be provided for CollisionShape3D to function. Please create a shape resource for it."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("A shape must be provided for CollisionShape3D to function. Please create a shape resource for it."); } if (Object::cast_to<RigidBody3D>(get_parent())) { if (Object::cast_to<ConcavePolygonShape3D>(*shape)) { if (Object::cast_to<RigidBody3D>(get_parent())->get_mode() != RigidBody3D::MODE_STATIC) { - return TTR("ConcavePolygonShape3D doesn't support RigidBody3D in another mode than static."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("ConcavePolygonShape3D doesn't support RigidBody3D in another mode than static."); } } } - return String(); + return warning; } void CollisionShape3D::_bind_methods() { diff --git a/scene/3d/cpu_particles_3d.cpp b/scene/3d/cpu_particles_3d.cpp index ded83b75ac..c977e0d4aa 100644 --- a/scene/3d/cpu_particles_3d.cpp +++ b/scene/3d/cpu_particles_3d.cpp @@ -189,7 +189,7 @@ bool CPUParticles3D::get_fractional_delta() const { } String CPUParticles3D::get_configuration_warning() const { - String warnings; + String warnings = GeometryInstance3D::get_configuration_warning(); bool mesh_found = false; bool anim_material_found = false; diff --git a/scene/3d/gi_probe.cpp b/scene/3d/gi_probe.cpp index 1b6f9b45b9..ab30d2fec5 100644 --- a/scene/3d/gi_probe.cpp +++ b/scene/3d/gi_probe.cpp @@ -514,10 +514,15 @@ Vector<Face3> GIProbe::get_faces(uint32_t p_usage_flags) const { } String GIProbe::get_configuration_warning() const { + String warning = VisualInstance3D::get_configuration_warning(); + if (RenderingServer::get_singleton()->is_low_end()) { - return TTR("GIProbes are not supported by the GLES2 video driver.\nUse a BakedLightmap instead."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("GIProbes are not supported by the GLES2 video driver.\nUse a BakedLightmap instead."); } - return String(); + return warning; } void GIProbe::_bind_methods() { diff --git a/scene/3d/gpu_particles_3d.cpp b/scene/3d/gpu_particles_3d.cpp index 6fa0fc6ecb..da5c99a873 100644 --- a/scene/3d/gpu_particles_3d.cpp +++ b/scene/3d/gpu_particles_3d.cpp @@ -232,7 +232,7 @@ String GPUParticles3D::get_configuration_warning() const { return TTR("GPU-based particles are not supported by the GLES2 video driver.\nUse the CPUParticles3D node instead. You can use the \"Convert to CPUParticles3D\" option for this purpose."); } - String warnings; + String warnings = GeometryInstance3D::get_configuration_warning(); bool meshes_found = false; bool anim_material_found = false; diff --git a/scene/3d/light_3d.cpp b/scene/3d/light_3d.cpp index 2eb12d10fa..46c3f18c38 100644 --- a/scene/3d/light_3d.cpp +++ b/scene/3d/light_3d.cpp @@ -448,7 +448,7 @@ String OmniLight3D::get_configuration_warning() const { String warning = Light3D::get_configuration_warning(); if (!has_shadow() && get_projector().is_valid()) { - if (warning != String()) { + if (!warning.empty()) { warning += "\n\n"; } warning += TTR("Projector texture only works with shadows active."); @@ -482,15 +482,14 @@ String SpotLight3D::get_configuration_warning() const { String warning = Light3D::get_configuration_warning(); if (has_shadow() && get_param(PARAM_SPOT_ANGLE) >= 90.0) { - if (warning != String()) { + if (!warning.empty()) { warning += "\n\n"; } - warning += TTR("A SpotLight3D with an angle wider than 90 degrees cannot cast shadows."); } if (!has_shadow() && get_projector().is_valid()) { - if (warning != String()) { + if (!warning.empty()) { warning += "\n\n"; } warning += TTR("Projector texture only works with shadows active."); diff --git a/scene/3d/navigation_agent_3d.cpp b/scene/3d/navigation_agent_3d.cpp index e179261002..422b566867 100644 --- a/scene/3d/navigation_agent_3d.cpp +++ b/scene/3d/navigation_agent_3d.cpp @@ -287,11 +287,16 @@ void NavigationAgent3D::_avoidance_done(Vector3 p_new_velocity) { } String NavigationAgent3D::get_configuration_warning() const { + String warning = Node::get_configuration_warning(); + if (!Object::cast_to<Node3D>(get_parent())) { - return TTR("The NavigationAgent3D can be used only under a spatial node."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("The NavigationAgent3D can be used only under a spatial node."); } - return String(); + return warning; } void NavigationAgent3D::update_navigation() { diff --git a/scene/3d/navigation_obstacle_3d.cpp b/scene/3d/navigation_obstacle_3d.cpp index 69fd5b02fc..adbff06ed6 100644 --- a/scene/3d/navigation_obstacle_3d.cpp +++ b/scene/3d/navigation_obstacle_3d.cpp @@ -113,11 +113,16 @@ Node *NavigationObstacle3D::get_navigation_node() const { } String NavigationObstacle3D::get_configuration_warning() const { + String warning = Node::get_configuration_warning(); + if (!Object::cast_to<Node3D>(get_parent())) { - return TTR("The NavigationObstacle3D only serves to provide collision avoidance to a spatial object."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("The NavigationObstacle3D only serves to provide collision avoidance to a spatial object."); } - return String(); + return warning; } void NavigationObstacle3D::update_agent_shape() { diff --git a/scene/3d/navigation_region_3d.cpp b/scene/3d/navigation_region_3d.cpp index 15ed448a65..71bc74f433 100644 --- a/scene/3d/navigation_region_3d.cpp +++ b/scene/3d/navigation_region_3d.cpp @@ -189,19 +189,26 @@ String NavigationRegion3D::get_configuration_warning() const { return String(); } + String warning = Node3D::get_configuration_warning(); + if (!navmesh.is_valid()) { - return TTR("A NavigationMesh resource must be set or created for this node to work."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("A NavigationMesh resource must be set or created for this node to work."); } const Node3D *c = this; while (c) { - if (Object::cast_to<Navigation3D>(c)) { - return String(); - } + if (Object::cast_to<Navigation3D>(c)) + return warning; c = Object::cast_to<Node3D>(c->get_parent()); } - return TTR("NavigationRegion3D must be a child or grandchild to a Navigation3D node. It only provides navigation data."); + if (!warning.empty()) { + warning += "\n\n"; + } + return warning + TTR("NavigationRegion3D must be a child or grandchild to a Navigation3D node. It only provides navigation data."); } void NavigationRegion3D::_bind_methods() { diff --git a/scene/3d/node_3d.cpp b/scene/3d/node_3d.cpp index bd9e4f5bde..bf1445edf2 100644 --- a/scene/3d/node_3d.cpp +++ b/scene/3d/node_3d.cpp @@ -773,6 +773,7 @@ void Node3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "rotation_degrees", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_rotation_degrees", "get_rotation_degrees"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "rotation", PROPERTY_HINT_NONE, "", 0), "set_rotation", "get_rotation"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "scale", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_scale", "get_scale"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "toplevel"), "set_as_toplevel", "is_set_as_toplevel"); ADD_GROUP("Matrix", ""); ADD_PROPERTY(PropertyInfo(Variant::TRANSFORM, "transform", PROPERTY_HINT_NONE, ""), "set_transform", "get_transform"); ADD_GROUP("Visibility", ""); diff --git a/scene/3d/path_3d.cpp b/scene/3d/path_3d.cpp index bf69a8598d..d0d2d6f9f4 100644 --- a/scene/3d/path_3d.cpp +++ b/scene/3d/path_3d.cpp @@ -250,16 +250,24 @@ String PathFollow3D::get_configuration_warning() const { return String(); } + String warning = Node3D::get_configuration_warning(); + if (!Object::cast_to<Path3D>(get_parent())) { - return TTR("PathFollow3D only works when set as a child of a Path3D node."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("PathFollow3D only works when set as a child of a Path3D node."); } else { Path3D *path = Object::cast_to<Path3D>(get_parent()); if (path->get_curve().is_valid() && !path->get_curve()->is_up_vector_enabled() && rotation_mode == ROTATION_ORIENTED) { - return TTR("PathFollow3D's ROTATION_ORIENTED requires \"Up Vector\" to be enabled in its parent Path3D's Curve resource."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("PathFollow3D's ROTATION_ORIENTED requires \"Up Vector\" to be enabled in its parent Path3D's Curve resource."); } } - return String(); + return warning; } void PathFollow3D::_bind_methods() { diff --git a/scene/3d/physics_body_3d.cpp b/scene/3d/physics_body_3d.cpp index fc021e5532..9a127c5425 100644 --- a/scene/3d/physics_body_3d.cpp +++ b/scene/3d/physics_body_3d.cpp @@ -737,7 +737,7 @@ String RigidBody3D::get_configuration_warning() const { String warning = CollisionObject3D::get_configuration_warning(); if ((get_mode() == MODE_RIGID || get_mode() == MODE_CHARACTER) && (ABS(t.basis.get_axis(0).length() - 1.0) > 0.05 || ABS(t.basis.get_axis(1).length() - 1.0) > 0.05 || ABS(t.basis.get_axis(2).length() - 1.0) > 0.05)) { - if (warning != String()) { + if (!warning.empty()) { warning += "\n\n"; } warning += TTR("Size changes to RigidBody3D (in character or rigid modes) will be overridden by the physics engine when running.\nChange the size in children collision shapes instead."); diff --git a/scene/3d/remote_transform_3d.cpp b/scene/3d/remote_transform_3d.cpp index 95fce6b802..358f9346f8 100644 --- a/scene/3d/remote_transform_3d.cpp +++ b/scene/3d/remote_transform_3d.cpp @@ -180,11 +180,16 @@ void RemoteTransform3D::force_update_cache() { } String RemoteTransform3D::get_configuration_warning() const { + String warning = Node3D::get_configuration_warning(); + if (!has_node(remote_node) || !Object::cast_to<Node3D>(get_node(remote_node))) { - return TTR("The \"Remote Path\" property must point to a valid Node3D or Node3D-derived node to work."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("The \"Remote Path\" property must point to a valid Node3D or Node3D-derived node to work."); } - return String(); + return warning; } void RemoteTransform3D::_bind_methods() { diff --git a/scene/3d/sprite_3d.cpp b/scene/3d/sprite_3d.cpp index 6e38196ba6..9f4d64cb32 100644 --- a/scene/3d/sprite_3d.cpp +++ b/scene/3d/sprite_3d.cpp @@ -1074,11 +1074,16 @@ StringName AnimatedSprite3D::get_animation() const { } String AnimatedSprite3D::get_configuration_warning() const { + String warning = SpriteBase3D::get_configuration_warning(); + if (frames.is_null()) { - return TTR("A SpriteFrames resource must be created or set in the \"Frames\" property in order for AnimatedSprite3D to display frames."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("A SpriteFrames resource must be created or set in the \"Frames\" property in order for AnimatedSprite3D to display frames."); } - return String(); + return warning; } void AnimatedSprite3D::_bind_methods() { diff --git a/scene/3d/vehicle_body_3d.cpp b/scene/3d/vehicle_body_3d.cpp index 4c71ab470b..b58f313c16 100644 --- a/scene/3d/vehicle_body_3d.cpp +++ b/scene/3d/vehicle_body_3d.cpp @@ -103,11 +103,16 @@ void VehicleWheel3D::_notification(int p_what) { } String VehicleWheel3D::get_configuration_warning() const { + String warning = Node3D::get_configuration_warning(); + if (!Object::cast_to<VehicleBody3D>(get_parent())) { - return TTR("VehicleWheel3D serves to provide a wheel system to a VehicleBody3D. Please use it as a child of a VehicleBody3D."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("VehicleWheel3D serves to provide a wheel system to a VehicleBody3D. Please use it as a child of a VehicleBody3D."); } - return String(); + return warning; } void VehicleWheel3D::_update(PhysicsDirectBodyState3D *s) { diff --git a/scene/3d/world_environment.cpp b/scene/3d/world_environment.cpp index 5e73e460ed..3c12d4991e 100644 --- a/scene/3d/world_environment.cpp +++ b/scene/3d/world_environment.cpp @@ -110,22 +110,30 @@ Ref<CameraEffects> WorldEnvironment::get_camera_effects() const { } String WorldEnvironment::get_configuration_warning() const { + String warning = Node::get_configuration_warning(); + if (!environment.is_valid()) { - return TTR("WorldEnvironment requires its \"Environment\" property to contain an Environment to have a visible effect."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("WorldEnvironment requires its \"Environment\" property to contain an Environment to have a visible effect."); } if (!is_inside_tree()) { - return String(); + return warning; } List<Node *> nodes; get_tree()->get_nodes_in_group("_world_environment_" + itos(get_viewport()->find_world_3d()->get_scenario().get_id()), &nodes); if (nodes.size() > 1) { - return TTR("Only one WorldEnvironment is allowed per scene (or set of instanced scenes)."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("Only one WorldEnvironment is allowed per scene (or set of instanced scenes)."); } - return String(); + return warning; } void WorldEnvironment::_bind_methods() { diff --git a/scene/3d/xr_nodes.cpp b/scene/3d/xr_nodes.cpp index f4a514cdd6..c0015aa338 100644 --- a/scene/3d/xr_nodes.cpp +++ b/scene/3d/xr_nodes.cpp @@ -60,13 +60,18 @@ String XRCamera3D::get_configuration_warning() const { return String(); } + String warning = Camera3D::get_configuration_warning(); + // must be child node of XROrigin3D! XROrigin3D *origin = Object::cast_to<XROrigin3D>(get_parent()); if (origin == nullptr) { - return TTR("XRCamera3D must have an XROrigin3D node as its parent."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("XRCamera3D must have an XROrigin3D node as its parent."); }; - return String(); + return warning; }; Vector3 XRCamera3D::project_local_ray_normal(const Point2 &p_pos) const { @@ -362,17 +367,25 @@ String XRController3D::get_configuration_warning() const { return String(); } + String warning = Node3D::get_configuration_warning(); + // must be child node of XROrigin! XROrigin3D *origin = Object::cast_to<XROrigin3D>(get_parent()); if (origin == nullptr) { - return TTR("XRController3D must have an XROrigin3D node as its parent."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("XRController3D must have an XROrigin3D node as its parent."); }; if (controller_id == 0) { - return TTR("The controller ID must not be 0 or this controller won't be bound to an actual controller."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("The controller ID must not be 0 or this controller won't be bound to an actual controller."); }; - return String(); + return warning; }; //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -479,17 +492,25 @@ String XRAnchor3D::get_configuration_warning() const { return String(); } + String warning = Node3D::get_configuration_warning(); + // must be child node of XROrigin3D! XROrigin3D *origin = Object::cast_to<XROrigin3D>(get_parent()); if (origin == nullptr) { - return TTR("XRAnchor3D must have an XROrigin3D node as its parent."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("XRAnchor3D must have an XROrigin3D node as its parent."); }; if (anchor_id == 0) { - return TTR("The anchor ID must not be 0 or this anchor won't be bound to an actual anchor."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("The anchor ID must not be 0 or this anchor won't be bound to an actual anchor."); }; - return String(); + return warning; }; Plane XRAnchor3D::get_plane() const { @@ -512,11 +533,16 @@ String XROrigin3D::get_configuration_warning() const { return String(); } + String warning = Node3D::get_configuration_warning(); + if (tracked_camera == nullptr) { - return TTR("XROrigin3D requires an XRCamera3D child node."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("XROrigin3D requires an XRCamera3D child node."); } - return String(); + return warning; }; void XROrigin3D::_bind_methods() { diff --git a/scene/animation/animation_tree.cpp b/scene/animation/animation_tree.cpp index 466536db10..d7d55b468e 100644 --- a/scene/animation/animation_tree.cpp +++ b/scene/animation/animation_tree.cpp @@ -1288,39 +1288,31 @@ String AnimationTree::get_configuration_warning() const { String warning = Node::get_configuration_warning(); if (!root.is_valid()) { - if (warning != String()) { + if (!warning.empty()) { warning += "\n\n"; } warning += TTR("No root AnimationNode for the graph is set."); } if (!has_node(animation_player)) { - if (warning != String()) { + if (!warning.empty()) { warning += "\n\n"; } - warning += TTR("Path to an AnimationPlayer node containing animations is not set."); - return warning; - } - - AnimationPlayer *player = Object::cast_to<AnimationPlayer>(get_node(animation_player)); - - if (!player) { - if (warning != String()) { - warning += "\n\n"; - } - - warning += TTR("Path set for AnimationPlayer does not lead to an AnimationPlayer node."); - return warning; - } + } else { + AnimationPlayer *player = Object::cast_to<AnimationPlayer>(get_node(animation_player)); - if (!player->has_node(player->get_root())) { - if (warning != String()) { - warning += "\n\n"; + if (!player) { + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("Path set for AnimationPlayer does not lead to an AnimationPlayer node."); + } else if (!player->has_node(player->get_root())) { + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("The AnimationPlayer root node is not a valid node."); } - - warning += TTR("The AnimationPlayer root node is not a valid node."); - return warning; } return warning; diff --git a/scene/gui/container.cpp b/scene/gui/container.cpp index a89eef6209..470a7db2dc 100644 --- a/scene/gui/container.cpp +++ b/scene/gui/container.cpp @@ -168,7 +168,7 @@ String Container::get_configuration_warning() const { String warning = Control::get_configuration_warning(); if (get_class() == "Container" && get_script().is_null()) { - if (warning != String()) { + if (!warning.empty()) { warning += "\n\n"; } warning += TTR("Container by itself serves no purpose unless a script configures its children placement behavior.\nIf you don't intend to add a script, use a plain Control node instead."); diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index 13d7440eb3..8d1dd5afeb 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -2560,7 +2560,7 @@ String Control::get_configuration_warning() const { String warning = CanvasItem::get_configuration_warning(); if (data.mouse_filter == MOUSE_FILTER_IGNORE && data.tooltip != "") { - if (warning != String()) { + if (!warning.empty()) { warning += "\n\n"; } warning += TTR("The Hint Tooltip won't be displayed as the control's Mouse Filter is set to \"Ignore\". To solve this, set the Mouse Filter to \"Stop\" or \"Pass\"."); diff --git a/scene/gui/range.cpp b/scene/gui/range.cpp index 59e26d9e38..bdb9f408f0 100644 --- a/scene/gui/range.cpp +++ b/scene/gui/range.cpp @@ -34,7 +34,7 @@ String Range::get_configuration_warning() const { String warning = Control::get_configuration_warning(); if (shared->exp_ratio && shared->min <= 0) { - if (warning != String()) { + if (!warning.empty()) { warning += "\n\n"; } warning += TTR("If \"Exp Edit\" is enabled, \"Min Value\" must be greater than 0."); diff --git a/scene/gui/scroll_container.cpp b/scene/gui/scroll_container.cpp index b72b913af0..5a3b94b198 100644 --- a/scene/gui/scroll_container.cpp +++ b/scene/gui/scroll_container.cpp @@ -520,6 +520,8 @@ void ScrollContainer::set_follow_focus(bool p_follow) { } String ScrollContainer::get_configuration_warning() const { + String warning = Container::get_configuration_warning(); + int found = 0; for (int i = 0; i < get_child_count(); i++) { @@ -538,10 +540,12 @@ String ScrollContainer::get_configuration_warning() const { } if (found != 1) { - return TTR("ScrollContainer is intended to work with a single child control.\nUse a container as child (VBox, HBox, etc.), or a Control and set the custom minimum size manually."); - } else { - return ""; + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("ScrollContainer is intended to work with a single child control.\nUse a container as child (VBox, HBox, etc.), or a Control and set the custom minimum size manually."); } + return warning; } HScrollBar *ScrollContainer::get_h_scrollbar() { diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp index 796408a38d..564de31dca 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -1218,6 +1218,7 @@ void CanvasItem::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::COLOR, "modulate"), "set_modulate", "get_modulate"); ADD_PROPERTY(PropertyInfo(Variant::COLOR, "self_modulate"), "set_self_modulate", "get_self_modulate"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "show_behind_parent"), "set_draw_behind_parent", "is_draw_behind_parent_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "toplevel"), "set_as_toplevel", "is_set_as_toplevel"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "show_on_top", PROPERTY_HINT_NONE, "", 0), "_set_on_top", "_is_on_top"); //compatibility ADD_PROPERTY(PropertyInfo(Variant::INT, "light_mask", PROPERTY_HINT_LAYERS_2D_RENDER), "set_light_mask", "get_light_mask"); @@ -1228,8 +1229,6 @@ void CanvasItem::_bind_methods() { ADD_GROUP("Material", ""); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "material", PROPERTY_HINT_RESOURCE_TYPE, "ShaderMaterial,CanvasItemMaterial"), "set_material", "get_material"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_parent_material"), "set_use_parent_material", "get_use_parent_material"); - //exporting these things doesn't really make much sense i think - // ADD_PROPERTY(PropertyInfo(Variant::BOOL, "toplevel", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR), "set_as_toplevel", "is_set_as_toplevel"); // ADD_PROPERTY(PropertyInfo(Variant::BOOL,"transform/notify"),"set_transform_notify","is_transform_notify_enabled"); ADD_SIGNAL(MethodInfo("draw")); diff --git a/scene/main/shader_globals_override.cpp b/scene/main/shader_globals_override.cpp index 726dcb58de..86c08ca6e9 100644 --- a/scene/main/shader_globals_override.cpp +++ b/scene/main/shader_globals_override.cpp @@ -261,11 +261,16 @@ void ShaderGlobalsOverride::_notification(int p_what) { } String ShaderGlobalsOverride::get_configuration_warning() const { + String warning = Node::get_configuration_warning(); + if (!active) { - return TTR("ShaderGlobalsOverride is not active because another node of the same type is in the scene."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("ShaderGlobalsOverride is not active because another node of the same type is in the scene."); } - return String(); + return warning; } void ShaderGlobalsOverride::_bind_methods() { diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index 7fefbc3f3c..5e91e4fe42 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -3093,10 +3093,15 @@ String Viewport::get_configuration_warning() const { return TTR("This viewport is not set as render target. If you intend for it to display its contents directly to the screen, make it a child of a Control so it can obtain a size. Otherwise, make it a RenderTarget and assign its internal texture to some node for display."); }*/ + String warning = Node::get_configuration_warning(); + if (size.x == 0 || size.y == 0) { - return TTR("Viewport size must be greater than 0 to render anything."); + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("Viewport size must be greater than 0 to render anything."); } - return String(); + return warning; } void Viewport::gui_reset_canvas_sort_index() { diff --git a/scene/resources/animation.cpp b/scene/resources/animation.cpp index b8edd70712..92103f3b1d 100644 --- a/scene/resources/animation.cpp +++ b/scene/resources/animation.cpp @@ -2633,6 +2633,7 @@ void Animation::_bind_methods() { ClassDB::bind_method(D_METHOD("value_track_get_update_mode", "track_idx"), &Animation::value_track_get_update_mode); ClassDB::bind_method(D_METHOD("value_track_get_key_indices", "track_idx", "time_sec", "delta"), &Animation::_value_track_get_key_indices); + ClassDB::bind_method(D_METHOD("value_track_interpolate", "track_idx", "time_sec"), &Animation::value_track_interpolate); ClassDB::bind_method(D_METHOD("method_track_get_key_indices", "track_idx", "time_sec", "delta"), &Animation::_method_track_get_key_indices); ClassDB::bind_method(D_METHOD("method_track_get_name", "track_idx", "key_idx"), &Animation::method_track_get_name); |